Download a conversation from Gemini to a markdown file.
// ==UserScript== // @name Gemini Download Conversation // @namespace https://github.com/DavidLJz/userscripts // @version 0.1.2 // @time 2025-06-08 19:00:00 // @description Download a conversation from Gemini to a markdown file. // @author DavidLJz // @license MIT // @match https://gemini.google.com/app/* // @match https://gemini.google.com/app // @run-at document-end // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net // @grant none // ==/UserScript== (async function () { 'use strict'; const logger = (msg, level = 'log') => { console[level](`[Gemini Download Conversation] ${msg}`); } logger('Script started'); // Add this line to verify script execution const getCopyButton = () => { const icon = document.querySelector('.mat-menu-above mat-icon[data-mat-icon-name="content_copy"]') if (!icon) { throw new Error('Copy button not found.'); } const btn = icon.closest('button'); if (!btn) { throw new Error('Button not found.'); } return btn; } const getConversationIdFromUrl = () => { const url = window.location.href; // https://gemini.google.com/app/xxxx const urlParts = url.split('/'); if (urlParts.length < 5) { throw new Error('Invalid URL.'); } return urlParts[4]; } const downloadMarkdownFile = (filename, text) => { const blob = new Blob([text], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); logger('Conversation saved.'); URL.revokeObjectURL(url); a.remove(); } const getUserQuery = (modelResponse) => { const conversationContainer = modelResponse.closest('.conversation-container'); if (!conversationContainer) { logger('Conversation container not found.', 'error'); return ''; } const userQuery = conversationContainer.querySelector('user-query'); if (!userQuery) { logger('User query not found.', 'error'); return ''; } const text = userQuery.textContent.trim().replace(/\n{2,}/g, '\n\n') if (!text) { logger('User query text not found.', 'error'); return ''; } return `**User:** ${text}\n\n`; } const saveConversation = async (chatWindow) => { if (!chatWindow) { logger('Chat window not found. Exiting.', 'error'); return; } logger('Saving conversation...'); const modelResponses = chatWindow.querySelectorAll('model-response') if (!modelResponses.length) { alert('No modelResponses found. Exiting.'); logger('No modelResponses found. Exiting.', 'error'); return; } const clickEvent = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); const fullTextList = []; for (let index = 0; index < modelResponses.length; index++) { const modelResponse = modelResponses[index]; logger(`Saving message ${index + 1} of ${modelResponses.length}...`); try { let text = getUserQuery(modelResponse); modelResponse.querySelector('.more-menu-button').dispatchEvent(clickEvent); getCopyButton().dispatchEvent(clickEvent); // Wait for message to be copied and then check the clipboard await new Promise(resolve => setTimeout(resolve, 1250)); const clipboardText = (await navigator.clipboard.readText()).trim() if (!clipboardText) { throw new Error('No text copied.'); } text += `**Gemini**: ${clipboardText}`; fullTextList.push(text); logger(`Message ${index + 1} of ${modelResponses.length} copied: ${text.length > 50 ? text.substring(0, 50) + '...' : text}`); } catch (error) { // Handle NotAllowedError: Failed to execute 'readText' on 'Clipboard': Document is not focused. if (error.name === 'NotAllowedError') { alert('Please do not switch tabs or windows while the script is running. Exiting.'); logger('Document is not focused. Exiting.', 'error'); return; } logger(`Error copying message ${index + 1} of ${modelResponses.length}.`, 'error'); logger(error, 'error'); } } if (!fullTextList.length) { logger('No messages copied. Exiting.', 'error'); return; } const conversationId = getConversationIdFromUrl(); const separator = '\n\n---\n\n'; const fullText = fullTextList.join(separator); logger('All messages copied. Saving to file...'); downloadMarkdownFile(`gemini-${conversationId}.md`, fullText); logger('Exiting.'); } const getChatWindow = async () => { let chatWindow = null; while (!chatWindow) { logger('Waiting for chat window...'); chatWindow = document.querySelector('chat-window infinite-scroller'); if (chatWindow) { logger('Chat window found.'); break; } await new Promise(resolve => setTimeout(resolve, 1000)); } return chatWindow; } const chatWindow = await getChatWindow(); if (!chatWindow) { logger('Chat window not found. Exiting.', 'error'); return; } const floatingTriggerButton = document.createElement('button'); floatingTriggerButton.textContent = 'Save Conversation'; floatingTriggerButton.style.position = 'fixed'; floatingTriggerButton.style.bottom = '10px'; floatingTriggerButton.style.right = '10px'; floatingTriggerButton.style.zIndex = '9999'; floatingTriggerButton.style.padding = '10px'; floatingTriggerButton.style.border = 'none'; floatingTriggerButton.style.backgroundColor = 'green'; floatingTriggerButton.style.color = 'white'; floatingTriggerButton.style.cursor = 'pointer'; logger('Chat window found. Waiting for user click to start saving...'); floatingTriggerButton.addEventListener('click', () => { logger('User clicked. Starting to save conversation...'); // Scroll until all messages are loaded const scrollInterval = setInterval(() => { chatWindow.scrollTop = 0; if (chatWindow.scrollTop === 0) { clearInterval(scrollInterval); logger('All messages loaded.'); saveConversation(chatWindow); } }, 1000); }); document.body.appendChild(floatingTriggerButton); })();