Gemini Chat Exporter

Export Gemini conversations as Markdown with accurate formatting and timestamps.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name                 Gemini Chat Exporter
// @name:zh-CN           Gemini 对话导出工具
// @description          Export Gemini conversations as Markdown with accurate formatting and timestamps.
// @description:zh-CN    深度优化排版,高保真还原 Gemini 聊天的标题、代码块、表格和公式。
// @namespace            https://github.com/AstridStark25963/gemini-chat-exporter/
// @version              1.2.0
// @author               AstridStark25963
// @license              MIT
// @icon                 data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgNTEyIDE4OCI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJsb2dvc0dvb2dsZUdlbWluaTAiIGN4PSI4NS43MzglIiBjeT0iMjUuMzU0JSIgcj0iMTAzLjE1NCUiIGZ4PSI4NS43MzglIiBmeT0iMjUuMzU0JSIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCgtLjg2ODg3IC40NzkxNSAtLjM5Mjc2IC0uNjY3MjMgMS43MDIgLjAxMikiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1QkFFRkYiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM5Q0JGRkYiLz48L3JhZGlhbEdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBpZD0ibG9nb3NHb29nbGVHZW1pbmkxIiBjeD0iNjEuODc5JSIgY3k9IjI2LjY4MyUiIHI9IjgwLjYxMiUiIGZ4PSI2MS44NzklIiBmeT0iMjYuNjgzJSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKC0xIC0uOTE5NSlyb3RhdGUoLTgxLjUyNiAtLjMyMyAuNzA2KSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzQwOURGRiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzY0QjBGRiIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJsb2dvc0dvb2dsZUdlbWluaTIiIGN4PSI1My4xODQlIiBjeT0iMTkuMDIxJSIgcj0iMTEwLjc4OSUiIGZ4PSI1My4xODQlIiBmeT0iMTkuMDIxJSIgZ3JhZGllbnRUcmFuc2Zvcm09InNjYWxlKC0uNjgwMSAtMSlyb3RhdGUoLTc2LjE5NyAtLjM2OCAuODM4KSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzE3N0NGRiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzREQTRGRiIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJsb2dvc0dvb2dsZUdlbWluaTMiIGN4PSItMTgyLjY2NSUiIGN5PSIxMC44NjklIiByPSI1MjEuNDA0JSIgZng9Ii0xODIuNjY1JSIgZnk9IjEwLjg2OSUiIGdyYWRpZW50VHJhbnNmb3JtPSJzY2FsZSgxIC4xNzk2KXJvdGF0ZSg2NS40MTMgLTIuMjEzIC4zNTcpIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMUM3QUZGIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNzZBOUZGIi8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjOEZCOUZGIi8+PC9yYWRpYWxHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImxvZ29zR29vZ2xlR2VtaW5pNCIgeDE9IjQ4Ljg4NyUiIHgyPSI0OC44ODclIiB5MT0iOC44MDklIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzA3NkVGRiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzNFOTNGRiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsb2dvc0dvb2dsZUdlbWluaTUiIHgxPSIxMy4yMTclIiB4Mj0iNzguNTk4JSIgeTE9IjAlIiB5Mj0iOTQuMjAxJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzA3NkVGRiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzY5QTNGRiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGZpbGw9InVybCgjbG9nb3NHb29nbGVHZW1pbmkwKSIgZD0iTTEyNS45MzkgMTI2LjY0YzAgMTcuMzk2LTUuMTYgMzEuMjU0LTE1LjQ4MiA0MS41NzVjLTExLjU5NiAxMi4yOTItMjYuODQ2IDE4LjQzOC00NS43NDggMTguNDM4Yy0xOC4wOSAwLTMzLjM5OC02LjI2Mi00NS45MjMtMTguNzg2QzYuMjYyIDE1NS4zNDIgMCAxMzkuOTE5IDAgMTIxLjU5N2MwLTE4LjMyMyA2LjI2Mi0zMy43NDcgMTguNzg2LTQ2LjI3QzMxLjMxMSA2Mi44MDEgNDYuNjE4IDU2LjU0IDY0LjcxIDU2LjU0YzkuMTYxIDAgMTcuOCAxLjYyMyAyNS45MTggNC44N2M4LjExOCAzLjI0NyAxNC43ODYgNy44MjggMjAuMDA0IDEzLjc0MmwtMTEuNDggMTEuNDhjLTMuODI3LTQuNjM4LTguNzg1LTguMjYyLTE0Ljg3My0xMC44NzFjLTYuMDg4LTIuNjEtMTIuNjExLTMuOTE0LTE5LjU3LTMuOTE0Yy0xMy41NjcgMC0yNS4wNDggNC42OTctMzQuNDQxIDE0LjA5Yy05LjI3NyA5LjUxLTEzLjkxNiAyMS4zOTYtMTMuOTE2IDM1LjY2YzAgMTQuMjYzIDQuNjM5IDI2LjE1IDEzLjkxNiAzNS42NTljOS4zOTMgOS4zOTMgMjAuODc0IDE0LjA5IDM0LjQ0MiAxNC4wOWMxMi40MDggMCAyMi43My0zLjQ4IDMwLjk2My0xMC40MzdjOC4yMzMtNi45NTggMTIuOTg4LTE2LjUyNSAxNC4yNjMtMjguNzAySDY0Ljcxdi0xNC45Nmg2MC4zNmMuNTggMy4yNDguODcgNi4zNzkuODcgOS4zOTQiLz48cGF0aCBmaWxsPSJ1cmwoI2xvZ29zR29vZ2xlR2VtaW5pMSkiIGQ9Ik0xNzYuMTcgOTYuMjA1YzEyLjc2OCAwIDIyLjkzIDQuMTMgMzAuNDg1IDEyLjM4N2M3LjU1NiA4LjI1OSAxMS4zMzQgMTkuODI2IDExLjMzNCAzNC43MDNsLS4xNzYgMS43NTdoLTY3LjY0OGMuMjM1IDguNDM0IDMuMDQ2IDE1LjIyOCA4LjQzNCAyMC4zODJjNS4zODkgNS4xNTQgMTEuODMxIDcuNzMgMTkuMzI4IDcuNzNjMTAuMzA4IDAgMTguMzktNS4xNTMgMjQuMjQ4LTE1LjQ2MWwxNC40MDggNy4wMjhjLTMuODY2IDcuMjYzLTkuMjI1IDEyLjk0NC0xNi4wNzcgMTcuMDQ0Yy02Ljg1MyA0LjEtMTQuNjEzIDYuMTUtMjMuMjgyIDYuMTVjLTEyLjY1IDAtMjMuMDc2LTQuMzM1LTMxLjI3Ni0xMy4wMDNzLTEyLjI5OS0xOS42Mi0xMi4yOTktMzIuODU3YzAtMTMuMTIgMy45ODMtMjQuMDQzIDExLjk0OC0zMi43N2M3Ljk2NS04LjcyNyAxOC4xNTctMTMuMDkgMzAuNTczLTEzLjA5bS0uMzUxIDE0Ljc2Yy02LjA5MSAwLTExLjMzMyAxLjg3NC0xNS43MjYgNS42MjJjLTQuMzkzIDMuNzQ5LTcuMjkyIDguNzg1LTguNjk4IDE1LjExaDQ5LjM3NGMtLjQ2OC01Ljk3My0yLjg5OS0xMC45MjItNy4yOTItMTQuODQ2Yy00LjM5Mi0zLjkyNC0xMC4yNzktNS44ODctMTcuNjU4LTUuODg3Ii8+PHBhdGggZmlsbD0idXJsKCNsb2dvc0dvb2dsZUdlbWluaTIpIiBkPSJNMjQ0LjQ5MyAxODQuODQzaC0xNi4xMTZWOTkuMDA4aDE1LjQxNnYxMS45MTJoLjdjMi40NTMtNC4yMDQgNi4yMTktNy43MDggMTEuMjk5LTEwLjUxYzUuMDgtMi44MDQgMTAuMTMtNC4yMDUgMTUuMTUzLTQuMjA1YzYuMzA2IDAgMTEuODUzIDEuNDYgMTYuNjQxIDQuMzhjNC43ODggMi45MTkgOC4yOTIgNi45NDggMTAuNTEgMTIuMDg2YzcuMTI0LTEwLjk3NyAxNi45OTMtMTYuNDY2IDI5LjYwNS0xNi40NjZjOS45MjcgMCAxNy41NzYgMy4wMzYgMjIuOTQ4IDkuMTFjNS4zNzIgNi4wNzIgOC4wNTggMTQuNzE0IDguMDU4IDI1LjkyNXY1My42MDNoLTE2LjExNnYtNTEuMTVjMC04LjA1OS0xLjQ2LTEzLjg2OS00LjM4LTE3LjQzYy0yLjkxOS0zLjU2Mi03LjgyNC01LjM0My0xNC43MTQtNS4zNDNjLTYuMTkgMC0xMS4zODYgMi42MjctMTUuNTkgNy44ODNjLTQuMjA1IDUuMjU1LTYuMzA3IDExLjQ0NC02LjMwNyAxOC41Njh2NDcuNDcyaC0xNi4xMTZ2LTUxLjE1YzAtOC4wNTktMS40Ni0xMy44NjktNC4zOC0xNy40M2MtMi45MTktMy41NjItNy44MjQtNS4zNDMtMTQuNzE0LTUuMzQzYy02LjE5IDAtMTEuMzg2IDIuNjI3LTE1LjU5IDcuODgzYy00LjIwNSA1LjI1NS02LjMwNyAxMS40NDQtNi4zMDcgMTguNTY4eiIvPjxwYXRoIGZpbGw9InVybCgjbG9nb3NHb29nbGVHZW1pbmk0KSIgZD0iTTM5My4yNjMgNjkuMjE2YzAgMy4xNTgtMS4xMTIgNS44NDgtMy4zMzQgOC4wN2MtMi4yMjMgMi4yMjMtNC45MTMgMy4zMzUtOC4wNzEgMy4zMzVjLTMuMTU4IDAtNS44NDktMS4xMTItOC4wNzEtMy4zMzRjLTIuMjIzLTIuMjIzLTMuMzM0LTQuOTEzLTMuMzM0LTguMDcxYzAtMy4xNTggMS4xMTEtNS44NDkgMy4zMzQtOC4wNzFjMi4yMjItMi4yMjMgNC45MTMtMy4zMzQgOC4wNy0zLjMzNGMzLjE2IDAgNS44NSAxLjExMSA4LjA3MiAzLjMzNGMyLjIyMiAyLjIyMiAzLjMzNCA0LjkxMyAzLjMzNCA4LjA3bS0zLjMzNCAyOS42NTJ2ODUuOTc1aC0xNi4xNDJWOTguODY4eiIvPjxwYXRoIGZpbGw9InVybCgjbG9nb3NHb29nbGVHZW1pbmkzKSIgZD0iTTUxMiA2OS4yMTZjMCAzLjE1OC0xLjExMSA1Ljg0OC0zLjMzNCA4LjA3Yy0yLjIyMiAyLjIyMy00LjkxMyAzLjMzNS04LjA3IDMuMzM1Yy0zLjE2IDAtNS44NS0xLjExMi04LjA3Mi0zLjMzNGMtMi4yMjItMi4yMjMtMy4zMzQtNC45MTMtMy4zMzQtOC4wNzFjMC0zLjE1OCAxLjExMi01Ljg0OSAzLjMzNC04LjA3MWMyLjIyMy0yLjIyMyA0LjkxMy0zLjMzNCA4LjA3MS0zLjMzNGMzLjE1OCAwIDUuODQ5IDEuMTExIDguMDcxIDMuMzM0YzIuMjIzIDIuMjIyIDMuMzM0IDQuOTEzIDMuMzM0IDguMDdtLTMuMzM0IDI5LjY1MnY4NS45NzVoLTE2LjE0MlY5OC44Njh6Ii8+PHBhdGggZmlsbD0idXJsKCNsb2dvc0dvb2dsZUdlbWluaTUpIiBkPSJNNDA0LjAwNCA5OS4wMDhoMTUuNDE1djExLjkxMmguN2MyLjQ1My00LjIwNCA2LjIyLTcuNzA4IDExLjMtMTAuNTFjNS4wOC0yLjgwNCAxMC4zNjQtNC4yMDUgMTUuODUzLTQuMjA1YzEwLjUxIDAgMTguNTk3IDMuMDA3IDI0LjI2MSA5LjAyMmM1LjY2NCA2LjAxNCA4LjQ5NiAxNC41NjggOC40OTYgMjUuNjYzdjUzLjk1M2gtMTYuMTE2di01Mi45MDJjLS4zNS0xNC4wMTQtNy40MTYtMjEuMDIxLTIxLjE5Ni0yMS4wMjFjLTYuNDIzIDAtMTEuNzk1IDIuNTk4LTE2LjExNiA3Ljc5NXMtNi40ODEgMTEuNDE1LTYuNDgxIDE4LjY1NnY0Ny40NzJoLTE2LjExNnoiLz48cGF0aCBmaWxsPSIjMDc2RUZGIiBkPSJNMzQ4LjM3NCA3Mi43NmMtMi44NDYtMTguNzg4LTE3LjU5Mi0zMy41MzMtMzYuMzgtMzYuMzhjMTguNzg4LTIuODQ3IDMzLjUzNC0xNy41OTMgMzYuMzgtMzYuMzhjMi44NDcgMTguNzg3IDE3LjU5MyAzMy41MzMgMzYuMzggMzYuMzhjLTE4Ljc4NyAyLjg0Ny0zMy41MzMgMTcuNTkyLTM2LjM4IDM2LjM4Ii8+PC9zdmc+
// @include              *://gemini.google.com/*
// @run-at               document-idle
// @grant                GM_addStyle
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(`
        .gemini-export-circle-btn {
            position: fixed; right: 25px; bottom: 80px; z-index: 999999 !important;
            width: 48px; height: 48px; border-radius: 50%;
            background: #1a73e8; color: #fff; border: none;
            display: flex; align-items: center; justify-content: center;
            cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            transition: all 0.2s ease-in-out;
        }
        .gemini-export-circle-btn:hover:not(:disabled) {
            background: #1557b0; transform: scale(1.1);
        }
        .gemini-export-circle-btn:disabled {
            background: #5f6368; cursor: not-allowed; transform: scale(1); opacity: 0.8;
        }
        .gemini-export-icon { width: 24px; height: 24px; stroke: currentColor; fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
        
        @keyframes gemini-spin {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
        .gemini-loading-icon {
            animation: gemini-spin 1s linear infinite;
        }
    `);

    const MarkdownParser = {
        parseNode: function(node) {
            if (node.nodeType === 3) return node.nodeValue;
            if (node.nodeType === 1) {
                const text = node.textContent.trim();
                
                if (text === "Export to Sheets" || text === "Export to Google Sheets" || text === "content_copy") return "";

                if (node.classList.contains('generated-image-controls') || node.classList.contains('export-sheets-button-container')) return "";

                if (node.tagName === 'PROCESSING-STATE' || node.classList.contains('extension-processing-state')) return "";

                if (node.tagName === 'SOURCES-LIST' || node.classList.contains('sources-list')) return "";

                if (node.tagName === 'USER-NOTICE' || node.classList.contains('user-notice')) return "";

                if (node.classList.contains('model-thoughts')) {
                    const contentNode = node.querySelector('[data-test-id="thoughts-content"]');
                    if (contentNode) {
                        const thoughtText = this.parseChildren(contentNode).trim();
                        const formatted = thoughtText.split('\n').map(line => `> ${line}`).join('\n');
                        return `> **🤔 Gemini Thinking:**\n>\n${formatted}\n\n`;
                    }
                    return "";
                }

                if (node.tagName === 'YOUTUBE-BLOCK') {
                    const titleEl = node.querySelector('.tool-attribution-title span');
                    const subtitleEl = node.querySelector('.tool-attribution-label');
                    const iframe = node.querySelector('iframe');
                    
                    if (titleEl && iframe) {
                        const title = titleEl.textContent.trim();
                        const subtitle = subtitleEl ? subtitleEl.textContent.trim() : "";
                        const src = iframe.getAttribute('src') || "";
                        const match = src.match(/\/embed\/([^?&]+)/);
                        const videoId = match ? match[1] : "";
                        const url = videoId ? `https://www.youtube.com/watch?v=${videoId}` : "";
                        const titleLink = url ? `[${title}](${url})` : title;

                        return `\n> ▶️ **YouTube**\n>\n> - **${titleLink}**\n>   ${subtitle}\n>\n`;
                    }
                }

                if (node.tagName === 'ACTION-CARD') {
                    const serviceNameEl = node.querySelector('.tool-display-name');
                    
                    if (serviceNameEl) {
                        const serviceName = serviceNameEl.textContent.trim();
                        let emoji = "📱";
                        let baseUrlTemplate = "";

                        if (serviceName === "YouTube Music") {
                            emoji = "🎵";
                            baseUrlTemplate = "https://music.youtube.com/search?q=";
                        } else if (serviceName === "Google Keep") {
                            emoji = "💡";
                            baseUrlTemplate = "https://keep.google.com/#search/text/";
                        } else if (serviceName === "Google Tasks") {
                            emoji = "✅";
                            baseUrlTemplate = "tasks";
                        } else if (serviceName === "Google Calendar") {
                            emoji = "📅";
                            baseUrlTemplate = "calendar";
                        }

                        let mdString = `\n> ${emoji} **${serviceName}**\n> \n`;
                        
                        const actions = node.querySelectorAll('action');
                        actions.forEach(action => {
                            const titleEl = action.querySelector('.primary-text');
                            
                            if (titleEl) {
                                const title = this.parseChildren(titleEl).trim();
                                
                                let searchUrl = "";
                                if (baseUrlTemplate === "tasks") {
                                    searchUrl = "https://tasks.google.com/";
                                } else if (baseUrlTemplate === "calendar") {
                                    searchUrl = "https://calendar.google.com/";
                                } else if (baseUrlTemplate) {
                                    searchUrl = baseUrlTemplate + encodeURIComponent(title);
                                }

                                const titleLink = searchUrl ? `[${title}](${searchUrl})` : title;
                                mdString += `> - **${titleLink}**\n`;
                                
                                const detailNodes = action.querySelectorAll('.secondary-text, .location, .notes');
                                detailNodes.forEach(detailEl => {
                                    const detailText = this.parseChildren(detailEl).trim();
                                    if (detailText) {
                                        mdString += `>   ${detailText}\n`;
                                    }
                                });
                            }
                        });
                        
                        mdString += `\n`;
                        return mdString;
                    }
                }

                const mathTex = node.getAttribute('data-math');
                if (mathTex) {
                    const isBlock = node.classList.contains('math-display') || node.tagName === 'DIV'; 
                    return isBlock ? `\n$$\n${mathTex}\n$$\n` : `$${mathTex}$`;
                }
                
                if (node.tagName === 'ANNOTATION' && node.getAttribute("encoding") === "application/x-tex") {
                     return `$${node.textContent.trim()}$`;
                }
            }

            switch (node.nodeName) {
                case "H1": case "H2": case "H3":
                    const level = node.nodeName.substring(1);
                    const hText = this.parseChildren(node).trim();
                    if (!hText || hText === "Gemini said" || hText === "You said") return "";
                    return `\n${"#".repeat(parseInt(level) + 2)} ${hText}\n`;
                
                case "P": return `\n${this.parseChildren(node).trim()}\n`;
                case "STRONG": case "B": return `**${this.parseChildren(node).trim()}**`;
                case "EM": case "I": return `*${this.parseChildren(node).trim()}*`;
                
                case "A":
                    const href = node.getAttribute("href");
                    const linkText = this.parseChildren(node).trim();
                    if (!href || href.startsWith('javascript:') || href === "#") return linkText;
                    return `[${linkText}](${href})`;
                
                case "CODE":
                    return node.closest('pre') ? node.textContent : `\`${node.textContent}\``;
                
                case "CODE-BLOCK":
                    const langSpan = node.querySelector('span');
                    const lang = langSpan ? langSpan.textContent.trim() : "";
                    const codeContent = node.querySelector('pre')?.textContent || "";
                    return `\n\`\`\`${lang}\n${codeContent.trim()}\n\`\`\`\n`;
                case "PRE":
                    if (node.closest('code-block')) return "";
                    return `\n\`\`\`\n${node.textContent.trim()}\n\`\`\`\n`;
                
                case "UL": return `\n${this.parseChildren(node)}\n`;
                case "OL":
                    let olText = "\n";
                    Array.from(node.children).forEach((li, i) => {
                        if(li.nodeName === "LI") olText += `${i+1}. ${this.parseChildren(li).trim()}\n`;
                    });
                    return olText;
                case "LI": return `- ${this.parseChildren(node).trim()}\n`;
                case "BLOCKQUOTE": return `\n> ${node.textContent.trim()}\n`;
                
                case "TABLE": return `\n${this.parseTable(node)}\n`;
                
                case "BR": return "\n";
                case "SCRIPT": case "STYLE": return "";
                case "MAT-ICON": return "";
                
                default: return this.parseChildren(node);
            }
        },
        parseChildren: function(node) {
            let res = "";
            node.childNodes.forEach(child => res += this.parseNode(child));
            return res;
        },
        parseTable: function(table) {
            let md = "";
            const rows = Array.from(table.querySelectorAll("tr"));
            rows.forEach((tr, i) => {
                const cells = Array.from(tr.querySelectorAll("th, td")).map(c => {
                    let cellContent = this.parseChildren(c).trim();
                    return cellContent.replace(/\|/g, "\\|").replace(/\n/g, "<br>"); 
                });
                
                md += `| ${cells.join(" | ")} |\n`;
                if (i === 0) md += `| ${cells.map(() => "---").join(" | ")} |\n`;
            });
            return md;
        }
    };

    const Manager = {
        getChatTitle: function() {
            const titleEl = document.querySelector('[data-test-id="conversation-title"]');
            return titleEl?.textContent.trim() || "Gemini Chat";
        },

        getFileName: function(title) {
            const now = new Date();
            const ts = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}_${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}${String(now.getSeconds()).padStart(2,'0')}`;
            return `${title.replace(/[\/\\?%*:|"<>]/g, '_')}_${ts}.md`;
        },

        getScroller: function() {
            const msg = document.querySelector('user-query-content');
            if (msg) {
                let el = msg.parentElement;
                while (el && el.tagName !== 'BODY') {
                    const style = window.getComputedStyle(el);
                    if (style.overflowY === 'auto' || style.overflowY === 'scroll') return el;
                    el = el.parentElement;
                }
            }
            return document.documentElement;
        },

        autoScrollToTop: async function() {
            const scroller = this.getScroller();
            if (!scroller) return;
            
            return new Promise((resolve) => {
                let lastHeight = scroller.scrollHeight;
                let stableCount = 0;
                const maxStableCount = 10;
                const checkInterval = 200;

                const timer = setInterval(() => {
                    scroller.scrollTop = 0;
                    const currentHeight = scroller.scrollHeight;

                    if (currentHeight > lastHeight) {
                        lastHeight = currentHeight;
                        stableCount = 0;
                    } else {
                        stableCount++;
                    }

                    if (stableCount >= maxStableCount) {
                        clearInterval(timer);
                        resolve();
                    }
                }, checkInterval);
            });
        },

        run: async function() {
            const btn = document.getElementById("gemini-export-final");
            const originalIcon = `<svg class="gemini-export-icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>`;
            
            btn.disabled = true;
            btn.innerHTML = `<svg class="gemini-export-icon gemini-loading-icon" viewBox="0 0 24 24"><path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg>`;
            
            try {
                await this.autoScrollToTop();

                const queries = document.querySelectorAll("user-query-content");
                const responses = document.querySelectorAll("model-response");
                
                if (!queries.length) {
                    alert("Didn't find any conversations to export. Please make sure you're on a conversation page and try again.");
                    return;
                }

                const chatTitle = this.getChatTitle();
                let output = `# ${chatTitle}\n\n---\n\n`;

                for (let i = 0; i < queries.length; i++) {
                    const q = MarkdownParser.parseChildren(queries[i]).replace(/You said/g, "").trim();
                    output += `## 👤 User\n${q}\n\n`;

                    if (responses[i]) {
                        const a = MarkdownParser.parseChildren(responses[i]).replace(/Gemini said/g, "").trim();
                        output += `## 🤖 Gemini\n${a}\n\n---\n\n`;
                    }
                }

                const blob = new Blob([output], { type: "text/markdown" });
                const a = document.createElement("a");
                a.href = URL.createObjectURL(blob);
                a.download = this.getFileName(chatTitle);
                a.click();

            } catch (e) {
                console.error("Export failed:", e);
                alert("Export failed. Please check the console for more details.");
            } finally {
                btn.disabled = false;
                btn.innerHTML = originalIcon;
            }
        }
    };

    function init() {
        if (document.getElementById("gemini-export-final")) return;
        const btn = document.createElement("button");
        btn.id = "gemini-export-final";
        btn.className = "gemini-export-circle-btn";
        btn.innerHTML = `<svg class="gemini-export-icon" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>`;
        btn.onclick = () => Manager.run();
        document.documentElement.appendChild(btn);
    }

    if (window.trustedTypes && !window.trustedTypes.defaultPolicy) {
        window.trustedTypes.createPolicy('default', { createHTML: (s) => s });
    }
    setInterval(init, 2000);
})();