// ==UserScript== // @name Google AI Studio | Conversation/Chat Markdown-Export/Download (API-Based) // @namespace Violentmonkey // @version 1.5 // @description Exports the full conversation to Markdown. Uses a recursive scanner to find the chat history regardless of API changes. // @author Vibe-Coded by Piknockyou // @license MIT // @match https://aistudio.google.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com // @grant none // @run-at document-start // @downloadURL https://update.greasyfork.icu/scripts/557309/Google%20AI%20Studio%20%7C%20ConversationChat%20Markdown-ExportDownload%20%28API-Based%29.user.js // @updateURL https://update.greasyfork.icu/scripts/557309/Google%20AI%20Studio%20%7C%20ConversationChat%20Markdown-ExportDownload%20%28API-Based%29.meta.js // ==/UserScript== (function() { 'use strict'; //================================================================================ // GLOBAL STATE //================================================================================ let capturedChatData = null; let downloadButton = null; let downloadIcon = null; function log(msg, type = 'info') { const color = type === 'success' ? '#34a853' : type === 'error' ? '#ea4335' : '#e8eaed'; console.log(`%c[AI Studio Export] ${msg}`, `color: ${color}; font-weight: bold;`); } //================================================================================ // CORE API INTERCEPTOR //================================================================================ const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(method, url) { this._url = url; return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { this.addEventListener('load', function() { if (this._url && this._url.includes('ResolveDriveResource')) { try { const rawText = this.responseText.replace(/^\)\]\}'/, '').trim(); const json = JSON.parse(rawText); if (Array.isArray(json) && json.length > 0) { log('Data intercepted. Size: ' + rawText.length + ' chars.', 'success'); capturedChatData = json; } } catch (err) { log('Interceptor Error: ' + err.message, 'error'); } } }); return originalSend.apply(this, arguments); }; //================================================================================ // RECURSIVE SEARCH LOGIC (The Fix) //================================================================================ function isTurn(arr) { if (!Array.isArray(arr)) return false; return arr.includes('user') || arr.includes('model'); } function findHistoryRecursive(node, depth = 0) { if (depth > 4) return null; if (!Array.isArray(node)) return null; const firstFew = node.slice(0, 5); const childrenAreTurns = firstFew.some(child => isTurn(child)); if (childrenAreTurns) { log(`Found History at depth ${depth}. Contains ${node.length} items.`); return node; } for (const child of node) { if (Array.isArray(child)) { const result = findHistoryRecursive(child, depth + 1); if (result) return result; } } return null; } function extractTextFromTurn(turn) { let candidates = []; function scan(item, d=0) { if (d > 3) return; if (typeof item === 'string' && item.length > 1) { if (!['user', 'model', 'function'].includes(item)) candidates.push(item); } else if (Array.isArray(item)) { item.forEach(sub => scan(sub, d+1)); } } scan(turn.slice(0, 3)); return candidates.sort((a, b) => b.length - a.length)[0] || ""; } //================================================================================ // PARSING & DOWNLOAD //================================================================================ function processAndDownload() { if (!capturedChatData) { log('No data available yet. Nothing to download.', 'error'); updateButtonState('ERROR'); return; } try { const root = capturedChatData[0]; let title = `AI_Studio_Export_${new Date().toISOString().slice(0,10)}`; if (Array.isArray(root[4]) && typeof root[4][0] === 'string') title = root[4][0]; const safeFilename = title.replace(/[<>:"/\\|?*]/g, '_').trim().substring(0, 100) + ".md"; const historyArray = findHistoryRecursive(root); if (!historyArray) { console.warn("Recursive search failed. dumping root:", root); throw new Error("Could not locate chat history in API response."); } let mdContent = `# ${title}\n\n`; historyArray.forEach((turn) => { const isUser = turn.includes('user'); const isModel = turn.includes('model'); const displayName = isUser ? 'USER' : (isModel ? 'MODEL' : 'UNKNOWN'); let text = extractTextFromTurn(turn); if (text) { mdContent += `### **${displayName}**\n\n${text}\n\n---\n\n`; } }); const blob = new Blob([mdContent], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = safeFilename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); updateButtonState('SUCCESS'); } catch (e) { log('Parsing failed: ' + e.message, 'error'); updateButtonState('ERROR'); } } //================================================================================ // UI INTEGRATION //================================================================================ function updateButtonState(state) { if (!downloadButton || !downloadIcon) return; downloadIcon.style.color = '#34a853'; // Always green by default downloadButton.style.opacity = '1'; downloadButton.style.cursor = 'pointer'; downloadButton.disabled = false; downloadButton.title = 'Download Full Chat (.md)'; switch (state) { case 'SUCCESS': downloadIcon.textContent = 'check_circle'; downloadButton.title = 'Export Successful!'; setTimeout(() => updateButtonState('READY'), 3000); break; case 'ERROR': downloadIcon.textContent = 'error'; downloadIcon.style.color = '#ea4335'; downloadButton.title = 'No data available'; alert('No chat data available yet.\n\nPlease refresh the page and try again.'); setTimeout(() => updateButtonState('READY'), 3000); break; default: // READY or any other state downloadIcon.textContent = 'download'; break; } } function createUI() { const toolbarRight = document.querySelector('ms-toolbar .toolbar-right'); if (!toolbarRight || document.getElementById('aistudio-api-export-btn')) return; log('Injecting Native Toolbar Button...'); const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display: flex; align-items: center; margin: 0 4px; position: relative;'; downloadButton = document.createElement('button'); downloadButton.id = 'aistudio-api-export-btn'; downloadButton.setAttribute('ms-button', ''); downloadButton.setAttribute('variant', 'icon-borderless'); downloadButton.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon'; downloadButton.style.cursor = 'pointer'; downloadIcon = document.createElement('span'); downloadIcon.className = 'material-symbols-outlined notranslate ms-button-icon-symbol'; downloadIcon.textContent = 'download'; downloadButton.appendChild(downloadIcon); buttonContainer.appendChild(downloadButton); const moreButton = toolbarRight.querySelector('button[iconname="more_vert"]'); if (moreButton) { toolbarRight.insertBefore(buttonContainer, moreButton); } else { toolbarRight.appendChild(buttonContainer); } downloadButton.addEventListener('click', processAndDownload); updateButtonState('READY'); } function initialize() { createUI(); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { const toolbar = document.querySelector('ms-toolbar .toolbar-right'); if (toolbar) createUI(); } } }); observer.observe(document.body, { childList: true, subtree: true }); } //================================================================================ // NAVIGATION HANDLER (Clears data on chat switch/new prompt) //================================================================================ function clearCapturedData() { capturedChatData = null; // Button stays green - user can click anytime } const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function() { clearCapturedData(); return originalPushState.apply(this, arguments); }; history.replaceState = function() { clearCapturedData(); return originalReplaceState.apply(this, arguments); }; window.addEventListener('popstate', clearCapturedData); if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();