Google AI Studio | Chat MarkDown-Export

Export AI Studio conversations to Markdown with intelligent mode detection, toolbar integration, and abortable processing. Features dual-mode extraction and configurable filters.

// ==UserScript==
// @name         Google AI Studio | Chat MarkDown-Export
// @namespace    http://violentmonkey.net/
// @version      2.4
// @description  Export AI Studio conversations to Markdown with intelligent mode detection, toolbar integration, and abortable processing. Features dual-mode extraction and configurable filters.
// @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        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

/**
 * AI Studio Export Script - Advanced Conversation Capture Tool
 * ============================================================
 *
 * A comprehensive userscript that exports Google AI Studio conversations to Markdown format
 * with intelligent mode detection, automated Raw Mode toggle, and high-performance scrolling.
 *
 * CREDITS & INSPIRATION
 * ---------------------
 * This script builds upon concepts from two projects:
 *
 * 1. Userscript: Google AI Studio 聊天记录导出器** (now deleted)
 *    • @author: qwerty
 *    • Uploader: https://greasyfork.org/en/users/1462896-max-tab
 *    • Script: https://greasyfork.org/en/scripts/534177-google-ai-studio-chat-record-exporter
 *
 * 2. Extension: Gemini to PDF
 *    • Developer: Hamza Wasim ([email protected])
 *    • Extension: https://chromewebstore.google.com/detail/gemini-to-pdf/blndbnmpkgfoopgmcejnhdnepfejgipe
 *    • Website: https://geminitoolbox.com/
 *    • Announcement: https://www.reddit.com/r/Bard/comments/1lshanb/built_a_free_extension_to_save_gemini_and_ai/
 *
 * CORE FEATURES
 * -------------
 * • Intelligent Mode Detection: Auto-detects Raw vs Rendered Mode
 * • Abortable Processing: Cancel extraction mid-process
 * • Toolbar Integration: Seamlessly integrated into AI Studio's toolbar
 * • Configurable Filters: Real-time control over export content
 * • High-Performance: 50ms scroll delays with smart batching
 * • Auto-Recovery: Handles dynamic UI re-rendering automatically
 * • CSP-Compliant: Full compliance with AI Studio's security policies
 *
 * USER INTERFACE
 * --------------
 * • Export Button: Toolbar icon with visual state feedback (Idle/Working/Success/Error)
 * • Settings Panel: Gear icon opens configuration with export filters
 * • Abort Control: Click button during processing to cancel operation
 * • Dynamic Positioning: Panel adapts to window changes
 *
 * CONFIGURATION
 * -------------
 * • Prefer Raw Mode: Auto-switch to Raw Mode for cleaner extraction
 * • Include User Messages: Control user prompt inclusion
 * • Include AI Responses: Control AI answer inclusion
 * • Include AI Thinking: Control AI reasoning/thought process inclusion
 *
 * TECHNICAL ARCHITECTURE
 * ----------------------
 * • MutationObserver: Watches toolbar changes for auto-recovery
 * • AbortController: Implements responsive cancellation
 * • Multi-Strategy DOM: Fallback selectors for reliable extraction
 * • Memory Efficient: DOM element keys for data association
 * • Scroll Intelligence: Advanced detection of conversation boundaries
 *
 * USAGE
 * -----
 * 1. Find export/download icon in AI Studio toolbar
 * 2. Click gear icon to configure export filters (optional)
 * 3. Click export button to begin automated capture
 * 4. Monitor progress via button state changes
 * 5. Click button again to abort if needed
 * 6. Download starts automatically when complete
 *
 * PERFORMANCE
 * -----------
 * • Ultra-Fast: 50ms scroll delays with intelligent batching
 * • Smart Detection: Multiple passes ensure complete capture
 * • Error Recovery: Automatic retry with graceful failure handling
 * • Browser Support: Compatible with Tampermonkey, Violentmonkey
 */

(function() {
    'use strict';

    //================================================================================
    // CONFIGURATION - All script settings, constants, and tunable parameters
    //================================================================================

    //--------------------------------------------------------------------------------
    // CORE EXPORT PREFERENCES - The most important settings for the user
    //--------------------------------------------------------------------------------
    // Mode Preference: Set to true to extract in "Raw Mode" (clean markdown), or false for "Rendered Mode".
    let PREFER_RAW_MODE = true;

    // Content Filtering: Control which parts of the conversation to include in the export.
    let INCLUDE_USER_MESSAGES = true;    // true to include user prompts/questions
    let INCLUDE_AI_RESPONSES = true;     // true to include the AI's main answers/replies
    let INCLUDE_AI_THINKING = false;     // true to include the AI's reasoning/thought process

    //--------------------------------------------------------------------------------
    // FILE OUTPUT SETTINGS - Customize the exported file name
    //--------------------------------------------------------------------------------
    const EXPORT_FILENAME_PREFIX = 'aistudio_chat_export_';

    //--------------------------------------------------------------------------------
    // CORE EXTRACTION BEHAVIOR - Settings that affect how content is captured
    //--------------------------------------------------------------------------------
    const THOUGHT_EXPAND_DELAY_MS = 500;       // Wait time after expanding "thinking" sections to allow content to load.
    const THOUGHT_EXPANSION_RETRY_COUNT = 3;   // Number of attempts to expand thinking sections if the first try fails.
    const THOUGHT_MIN_LENGTH = 10;             // Minimum text length for a "thinking" block to be considered valid.

    //--------------------------------------------------------------------------------
    // PERFORMANCE & TIMING - Advanced settings to balance speed and reliability
    //--------------------------------------------------------------------------------
    const SCROLL_DELAY_MS = 50;                // Main scroll delay: 50ms for "machine-gun" speed. Lower values are faster but may miss content on slow connections.
    const INITIAL_SCROLL_DELAY = 100;          // Initial delay for page content to stabilize before scrolling begins.
    const RAW_MODE_MENU_DELAY_MS = 200;        // Wait for the "Raw Mode" dropdown menu to appear after clicking "More".
    const RAW_MODE_RENDER_DELAY_MS = 300;      // Wait for the UI to re-render after toggling Raw Mode.
    const FINAL_CAPTURE_DELAY_MS = 50;         // Delay before the final data extraction pass after scrolling is complete.
    const POST_EXTRACTION_DELAY_MS = 100;      // Delay for any cleanup or final processing after extraction.
    const FINAL_COLLECTION_DELAY_MS = 300;     // Delay at each final scroll position (top, middle, bottom) to ensure all content is loaded.
    const SUCCESS_RESET_TIMEOUT_MS = 2500;     // Time in ms before the button resets from 'Success' to 'Idle' (2.5 seconds).
    const ERROR_RESET_TIMEOUT_MS = 4000;       // Time in ms before the button resets from 'Failure' to 'Idle' (4 seconds).
    const SCRIPT_INIT_DELAY_MS = 2500;         // Delay after the page loads before the script initializes (2.5 seconds).
    const UPWARD_SCROLL_DELAY_MS = 1000;       // Delay for content to load when starting a scroll-up operation from the bottom.

    //--------------------------------------------------------------------------------
    // SCROLL BEHAVIOR - Advanced parameters controlling the auto-scrolling mechanism
    //--------------------------------------------------------------------------------
    const MAX_SCROLL_ATTEMPTS = 800;           // Maximum scroll attempts before giving up to prevent infinite loops.
    const SCROLL_INCREMENT_INITIAL = 150;      // Initial scroll increment in pixels per step.
    const SCROLL_INCREMENT_LARGE = 500;        // A larger initial jump for the first scroll pass to load content faster.
    const SCROLL_INCREMENT_FACTOR = 0.85;      // Reduction factor for the scroll increment (e.g., 85% of the previous increment).
    const BOTTOM_DETECTION_TOLERANCE = 10;     // Pixel tolerance for detecting the bottom of the scroll area.
    const PROBLEMATIC_JUMP_FACTOR = 1.25;      // Factor to detect problematic scroll jumps (e.g., 125% of intended distance).
    const MIN_SCROLL_DISTANCE_THRESHOLD = 5;   // Minimum pixel distance to detect if scrolling has effectively stopped.
    const SCROLL_PARENT_SEARCH_DEPTH = 5;      // How many parent elements to search upwards to find the main scroll container.
    const SCROLL_STABILITY_CHECKS = 3;         // Number of consecutive checks to confirm scroll stability before stopping.

    //--------------------------------------------------------------------------------
    // UI TEXT & LABELS - Customizable text for UI elements
    //--------------------------------------------------------------------------------
    const BUTTON_TEXT_START_SCROLL_DOWN = "Start Export";
    const BUTTON_TEXT_STOP_SCROLL = "Stop Export";
    const BUTTON_TEXT_PROCESSING = "Exporting...";
    const SUCCESS_TEXT_EXPORT = "Export Complete!";
    const ERROR_TEXT_EXPORT = "Export Failed";

    //--------------------------------------------------------------------------------
    // UI STYLING & POSITIONING - Detailed CSS-like values for the main button
    //--------------------------------------------------------------------------------
    const BUTTON_Z_INDEX = 10000;
    const BUTTON_POSITION_BOTTOM = 20;
    const BUTTON_POSITION_LEFT = 20;
    const BUTTON_PADDING_VERTICAL = 12;
    const BUTTON_PADDING_HORIZONTAL = 20;
    const BUTTON_BORDER_RADIUS = 25;
    const BUTTON_FONT_SIZE = 14;
    const BUTTON_FONT_WEIGHT = 500;
    const BUTTON_BOX_SHADOW_BLUR = 8;
    const BUTTON_TRANSITION_DURATION = 0.2;
    const BUTTON_BOX_SHADOW_RGBA = 'rgba(0,0,0,0.2)';
    const BUTTON_DEFAULT_BG = '#1a73e8';
    const BUTTON_PROCESSING_BG = '#1a73e8';
    const BUTTON_SUCCESS_BG = '#1e8e3e';
    const BUTTON_ERROR_BG = '#d93025';
    const BUTTON_FONT_FAMILY = "'Google Sans', sans-serif";
    const BUTTON_COLOR = 'white';
    const BUTTON_BORDER = 'none';
    const BUTTON_CURSOR = 'pointer';

    //--------------------------------------------------------------------------------
    // LOG CONSOLE UI - Styling for the debug log panel
    //--------------------------------------------------------------------------------
    const LOG_ENTRY_INFO_COLOR = '#e8eaed';
    const LOG_ENTRY_SUCCESS_COLOR = '#34a853';
    const LOG_ENTRY_WARN_COLOR = '#fbbc04';
    const LOG_ENTRY_ERROR_COLOR = '#ea4335';

    // State variables
    let isScrolling = false;
    let collectedData = new Map();
    let scrollCount = 0;
    let noChangeCounter = 0;
    let scrollIncrement = SCROLL_INCREMENT_LARGE;
    let exportButtonState = 'IDLE'; // Can be 'IDLE', 'WORKING', 'SUCCESS', 'ERROR'
    let abortController;
 
     // UI Elements
    let exportButton, exportIcon;
    let settingsPanel;
    let preferRawModeCheckbox, includeUserMessagesCheckbox, includeAiResponsesCheckbox, includeAiThinkingCheckbox;

    //================================================================================
    // HELPER FUNCTIONS - Utility functions for script operation
    //================================================================================

    /**
     * Creates a Trusted Types policy to handle Content Security Policy compliance
     * for DOM manipulation in Google AI Studio's environment.
     */
    let trustedTypesPolicy = null;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
            // Policy names for userscripts often require a specific format.
            trustedTypesPolicy = window.trustedTypes.createPolicy('aistudio-export-policy#userscript', {
                createHTML: string => string
            });
        } catch (e) {
            console.warn("[AI Studio Export] Could not create a new Trusted Types policy. This might happen if the policy already exists. The script will attempt to continue.", e.message);
        }
    }

    /**
     * Creates a trusted HTML string for DOM manipulation while respecting CSP policies.
     * @param {string} htmlString - The HTML string to be processed.
     * @returns {string} - A trusted HTML string compatible with Content Security Policy.
     */
    function getTrustedHTML(htmlString) {
        if (trustedTypesPolicy) {
            return trustedTypesPolicy.createHTML(htmlString);
        }
        return htmlString;
    }

    /**
     * Creates a delay/promise for asynchronous operations.
     * @param {number} ms - Time in milliseconds to delay.
     * @returns {Promise} - Promise that resolves after the specified delay.
     */
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Generates a timestamp string in format YYYYMMDD_HHMMSS for file naming.
     * @returns {string} - Formatted timestamp string.
     */
    function getCurrentTimestamp() {
        const n = new Date();
        const YYYY = n.getFullYear();
        const MM = (n.getMonth() + 1).toString().padStart(2, '0');
        const DD = n.getDate().toString().padStart(2, '0');
        const hh = n.getHours().toString().padStart(2, '0');
        const mm = n.getMinutes().toString().padStart(2, '0');
        const ss = n.getSeconds().toString().padStart(2, '0');
        return `${YYYY}${MM}${DD}_${hh}${mm}${ss}`;
    }

    /**
     * Logs a message to the browser console with consistent formatting.
     * @param {string} message - The message to log.
     */
    function logToConsole(message) {
        console.log(`[AI Studio Export] ${message}`);
    }

    /**
     * Logs a message to both console and visual panel for consistent output.
     * This function replaces the old logToConsole calls throughout the script.
     * @param {string} message - The message to log.
     * @param {string} type - The log type: 'info', 'success', 'warn', 'error' (optional).
     */
    function log(message, type = 'info') {
        const style = `color: ${type === 'success' ? LOG_ENTRY_SUCCESS_COLOR : type === 'warn' ? LOG_ENTRY_WARN_COLOR : type === 'error' ? LOG_ENTRY_ERROR_COLOR : LOG_ENTRY_INFO_COLOR};`;
        console.log(`%c[AI Studio Export] ${message}`, style);
    }

    /**
     * Detects whether the page is currently in Raw Mode or Rendered Mode.
     * Raw Mode shows plain markdown text in .very-large-text-container
     * Rendered Mode shows formatted content in ms-cmark-node elements
     * @returns {string} - 'raw' or 'rendered'
     */
    function detectCurrentMode() {
        // Strategy 1: Check for Raw Mode indicator in a user turn
        const firstUserTurn = document.querySelector('ms-chat-turn .chat-turn-container.user');
        if (firstUserTurn) {
            const hasRawContainer = firstUserTurn.querySelector('ms-text-chunk .very-large-text-container');
            const hasCmarkNode = firstUserTurn.querySelector('ms-text-chunk ms-cmark-node');

            if (hasRawContainer && !hasCmarkNode) {
                log("Detected mode: Raw Mode", 'info');
                return 'raw';
            }
            if (hasCmarkNode && !hasRawContainer) {
                log("Detected mode: Rendered Mode", 'info');
                return 'rendered';
            }
        }

        // Strategy 2: Check URL or page state
        // (AI Studio might have a parameter or class indicating mode)
        const body = document.body;
        if (body.classList.contains('raw-mode')) {
            log("Detected mode: Raw Mode (via body class)", 'info');
            return 'raw';
        }

        // Default assumption (most common state)
        log("Could not detect mode, assuming Rendered Mode", 'warn');
        return 'rendered';
    }

    /**
     * Expands collapsed AI thinking sections to expose hidden content.
     * Uses multiple selector strategies to find and expand thinking panels.
     * @async
     * @param {Element} modelDiv - The model DOM element to expand thinking in.
     * @param {number} turnIndex - The index of the turn being processed.
     * @returns {Promise<boolean>} - True if thinking sections were expanded, false otherwise.
     */
    async function expandThinkingSections(modelDiv, turnIndex = 0) {
        let expanded = false;
        
        try {
            // Strategy 1: Find collapsed expansion panels with thought-related text
            const collapsedPanels = modelDiv.querySelectorAll('mat-expansion-panel[aria-expanded="false"]');
            for (const panel of collapsedPanels) {
                const headerText = panel.querySelector('.mat-expansion-panel-header-title')?.textContent?.toLowerCase() || '';
                const buttonText = panel.querySelector('button[aria-expanded="false"]')?.textContent?.toLowerCase() || '';
                
                if (headerText.includes('thought') || headerText.includes('thinking') ||
                    buttonText.includes('thought') || buttonText.includes('thinking')) {
                    const expandButton = panel.querySelector('button[aria-expanded="false"]');
                    if (expandButton) {
                        expandButton.click();
                        expanded = true;
                        log(`Expanded thinking section (panel method) for turn ${turnIndex}`, 'info');
                    }
                }
            }

            // Strategy 2: Find expand buttons with thought-related text
            const expandButtons = modelDiv.querySelectorAll('button');
            for (const button of expandButtons) {
                const buttonText = button.textContent?.toLowerCase() || '';
                if ((buttonText.includes('expand') || buttonText.includes('show more')) &&
                    buttonText.includes('thought')) {
                    button.click();
                    expanded = true;
                    log(`Expanded thinking section (button method) for turn ${turnIndex}`, 'info');
                }
            }

            // Strategy 3: Click any "Show more" buttons in thought chunks
            const thoughtChunks = modelDiv.querySelectorAll('ms-thought-chunk');
            for (const chunk of thoughtChunks) {
                const showMoreButton = chunk.querySelector('button[aria-expanded="false"], button:not([aria-expanded])');
                if (showMoreButton && showMoreButton.textContent?.toLowerCase().includes('more')) {
                    showMoreButton.click();
                    expanded = true;
                    log(`Expanded thinking section (chunk method) for turn ${turnIndex}`, 'info');
                }
            }

            // Wait for expansion to complete
            if (expanded) {
                await delay(THOUGHT_EXPAND_DELAY_MS);
            }

            return expanded;
        } catch (error) {
            log(`Error expanding thinking sections for turn ${turnIndex}: ${error.message}`, 'warn');
            return false;
        }
    }

    /**
     * Clean and validate HTML snippet, removing unwanted elements.
     * Uses CSP-compliant DOM parsing to handle AI Studio's strict Content Security Policy.
     * @param {string} htmlSnippet - The HTML content to be cleaned and validated.
     * @returns {string|null} - The cleaned HTML string, or null if validation fails.
     */
    function cleanAndValidateSnippet(htmlSnippet) {
        if (!htmlSnippet || "string" !== typeof htmlSnippet) {
            return null;
        }

        let docFragment;
        try {
            // Use Range.createContextualFragment to safely parse HTML, avoiding DOMParser CSP issues.
            const range = document.createRange();
            range.selectNode(document.body); // Set a context for parsing.
            docFragment = range.createContextualFragment(htmlSnippet);
        } catch (parseError) {
            console.error("[AI Studio Export] Error creating contextual fragment during cleaning:", parseError, htmlSnippet.substring(0, 100));
            return null; // Return null if parsing fails
        }

        const turnDiv = docFragment.firstElementChild;
        if (!turnDiv) {
            console.warn("[AI Studio Export] Cleaning: Snippet did not contain a valid root element.", htmlSnippet.substring(0, 100));
            return null;
        }

        // Skip HTML cleaning due to strict CSP. Return raw HTML for fallback.
        // This method is CSP-compliant and won't trigger TrustedHTML errors.
        return htmlSnippet;
    }

    /**
     * Convert HTML to Markdown format with comprehensive formatting support.
     * Handles code blocks, links, images, inline code, emphasis, lists, headings, and paragraphs.
     * @param {string} html - The HTML content to convert to Markdown.
     * @returns {string} - The converted Markdown string.
     */
    function convertToMarkdown(html) {
        if (!html) return "";
        try {
            const tempDiv = document.createElement("div");
            tempDiv.innerHTML = getTrustedHTML(html);

            const ignoreStartRegex = /^\s*<!--\s*IGNORE_WHEN_COPYING_START\s*-->\s*(\r?\n)?/im;
            const ignoreEndRegex = /^\s*<!--\s*IGNORE_WHEN_COPYING_END\s*-->\s*(\r?\n)?/im;

            const sourceElement = tempDiv.querySelector(".turn-content") || tempDiv;

            // Process code blocks
            sourceElement.querySelectorAll("ms-code-block").forEach(block => {
                const pre = block.querySelector("pre");
                if (pre) {
                    pre.innerHTML = getTrustedHTML(pre.innerHTML.replaceAll(ignoreStartRegex, "").replaceAll(ignoreEndRegex, ""));
                }
                
                const code = pre ? pre.textContent : block.textContent.replaceAll(ignoreStartRegex, "").replaceAll(ignoreEndRegex, "");
                const langMatch = code.match(/^(\w+)\s*\n/);
                const lang = langMatch ? langMatch[1] : "";
                const codeContent = langMatch ? code.substring(langMatch[0].length) : code;
                
                block.replaceWith(`\n\n\`\`\`${lang}\n${codeContent.trim()}\n\`\`\`\n\n`);
            });

            // Process links
            sourceElement.querySelectorAll("a").forEach(a => {
                a.replaceWith(`[${a.textContent || a.innerText || ""}](${a.getAttribute("href") || ""})`);
            });

            // Process images
            sourceElement.querySelectorAll("img").forEach(img => {
                img.replaceWith(`\n\n![${img.getAttribute("alt") || "image"}](${img.getAttribute("src") || ""})\n\n`);
            });

            // Process inline code
            sourceElement.querySelectorAll("code:not(pre code)").forEach(code => {
                code.replaceWith(`\`${code.textContent}\``);
            });

            // Process emphasis
            sourceElement.querySelectorAll("strong, b").forEach(el => {
                el.replaceWith(`**${el.textContent}**`);
            });
            sourceElement.querySelectorAll("em, i").forEach(el => {
                el.replaceWith(`*${el.textContent}*`);
            });

            // Process lists
            sourceElement.querySelectorAll("ul, ol").forEach(list => {
                let listMd = "\n";
                Array.from(list.children).forEach((li, index) => {
                    let liContent = li.innerHTML.replace(/<br\s*\/?>/gi, "\n");
                    const tempLiDiv = document.createElement("div");
                    tempLiDiv.innerHTML = getTrustedHTML(liContent);
                    
                    // Clean formatting within list items
                    tempLiDiv.querySelectorAll("strong, b").forEach(el => {
                        el.replaceWith(`**${el.textContent}**`);
                    });
                    tempLiDiv.querySelectorAll("em, i").forEach(el => {
                        el.replaceWith(`*${el.textContent}*`);
                    });
                    tempLiDiv.querySelectorAll("code:not(pre code)").forEach(el => {
                        el.replaceWith(`\`${el.textContent}\``);
                    });
                    tempLiDiv.querySelectorAll("a").forEach(el => {
                        el.replaceWith(`[${el.textContent}](${el.getAttribute("href")})`);
                    });

                    const content = tempLiDiv.textContent?.trim() || "";
                    listMd += `${list.tagName === "OL" ? index + 1 + "." : "*"} ${content}\n`;
                });
                list.replaceWith(listMd);
            });

            // Process headings
            sourceElement.querySelectorAll("h1,h2,h3,h4,h5,h6").forEach(h => {
                const level = parseInt(h.tagName.substring(1), 10);
                h.replaceWith(`\n\n${"#".repeat(level)} ${h.textContent}\n\n`);
            });

            // Process paragraphs
            sourceElement.querySelectorAll("p").forEach(p => {
                const next = p.nextElementSibling;
                if (!p.textContent.trim() || (next && ["H1", "H2", "H3", "H4", "H5", "H6", "UL", "OL", "PRE", "BLOCKQUOTE", "HR", "TABLE", "MS-CODE-BLOCK"].includes(next?.tagName))) {
                    return; // Skip empty paragraphs and those followed by block elements
                }
                p.after("\n\n");
            });

            let text = sourceElement.innerText || sourceElement.textContent || "";
            return text = text.replace(/(\n\s*){3,}/g, "\n\n"), text.trim();
            
        } catch (e) {
            console.error("Error converting HTML snippet to Markdown:", e, html.substring(0, 100));
            const tempDiv = document.createElement("div");
            tempDiv.innerHTML = getTrustedHTML(html);
            return tempDiv.textContent || "";
        }
    }

    //================================================================================
    // CORE FUNCTIONS - Main business logic and automation
    //================================================================================

    /**
     * Automates the Raw Mode toggle functionality in Google AI Studio.
     * This function finds and clicks the "more actions" button, then selects "Raw Mode"
     * from the dropdown menu to expose the pure Markdown content.
     * @async
     * @returns {Promise<boolean>} - True if Raw Mode was successfully toggled, false otherwise.
     */
    async function toggleRawMode() {
    log("Attempting to toggle Raw Mode...");

    // 1. Find and click the 'More' button
    const moreButton = document.querySelector('button[aria-label="View more actions"]');
    if (!moreButton) {
        log("Error: 'More actions' button not found.", 'error');
        return false;
    }
    moreButton.click();
    log("'More actions' button clicked.");

    // 2. Wait for the menu to appear and click the 'Raw Mode' button
    await delay(RAW_MODE_MENU_DELAY_MS); // Wait for menu animation

    const rawModeButton = Array.from(document.querySelectorAll('button[role="menuitem"]'))
                               .find(btn => btn.textContent.includes('Raw Mode'));

    if (!rawModeButton) {
        log("Error: 'Raw Mode' button not found in the menu.", 'error');
        // Attempt to close the menu by clicking the 'More' button again
        moreButton.click();
        return false;
    }

    rawModeButton.click();
    log("'Raw Mode' button clicked.");
    await delay(RAW_MODE_RENDER_DELAY_MS); // Wait for the UI to re-render after toggling
    return true;
}

    /**
     * Identifies and returns the main scrollable element for AI Studio conversations.
     * Uses a cascade of selectors starting with the most specific, then falling back
     * to more general approaches, finally defaulting to document.documentElement.
     * @returns {Element} - The identified scrollable element.
     */
    function getMainScrollerElement_AiStudio() {
        log("Searching for page scroll container...");
        
        // Try the proven selectors from the extension
        let scroller = document.querySelector('ms-autoscroll-container');
        if (scroller) {
            log("Found scroll container (ms-autoscroll-container)", 'success');
            return scroller;
        }

        // Fallback to chat turns parent
        const chatTurnsContainer = document.querySelector('ms-chat-turn')?.parentElement;
        if (chatTurnsContainer) {
            let parent = chatTurnsContainer;
            for (let i = 0; i < SCROLL_PARENT_SEARCH_DEPTH && parent; i++) {
                if (parent.scrollHeight > parent.clientHeight + BOTTOM_DETECTION_TOLERANCE &&
                    (window.getComputedStyle(parent).overflowY === 'auto' ||
                     window.getComputedStyle(parent).overflowY === 'scroll')) {
                    log("Found scroll container (searching parent elements)", 'success');
                    return parent;
                }
                parent = parent.parentElement;
            }
        }

        log("Warning: Using document.documentElement as fallback", 'warn');
        return document.documentElement;
    }

    /**
     * Extracts data from all visible chat turns in the current AI Studio conversation.
     * Uses DOM traversal with CSP-compliant selectors to capture user prompts,
     * AI responses, and AI thinking output. Maintains a Map of collected data.
     * @returns {boolean} - True if new data was found or existing data was updated, false otherwise.
     */
    async function extractDataIncremental_AiStudio(extractionMode = 'raw') {
        let newlyFoundCount = 0;
        let dataUpdatedInExistingTurn = false;
        const currentTurns = document.querySelectorAll('ms-chat-turn');

        for (const [index, turn] of currentTurns.entries()) {
            const turnKey = turn; // Use turn element as key (consistent with MaxTab)
            const turnContainer = turn.querySelector('.chat-turn-container.user, .chat-turn-container.model');
            if (!turnContainer) continue; // Skip only if no container found

            // ✅ Check if new turn, but DON'T skip existing ones
            let isNewTurn = !collectedData.has(turnKey);
            let extractedInfo = collectedData.get(turnKey) || {
                domOrder: index,
                type: 'unknown',
                userText: null,
                thoughtText: null,
                responseText: null
            };

            if (isNewTurn) {
                collectedData.set(turnKey, extractedInfo);
                newlyFoundCount++;
            }

            let dataWasUpdatedThisTime = false;

            // Extract based on container type (exactly like MaxTab)
            if (turnContainer.classList.contains('user')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'user';
                if (!extractedInfo.userText) {
                    let userText = null;
                    if (extractionMode === 'raw') {
                        // RAW MODE: .very-large-text-container
                        const rawContainer = turn.querySelector('ms-text-chunk .very-large-text-container');
                        if (rawContainer) {
                            userText = rawContainer.textContent.trim();
                        }
                    } else {
                        // RENDERED MODE: ms-cmark-node
                        let userNode = turn.querySelector('.turn-content ms-cmark-node');
                        if (userNode) {
                            userText = userNode.innerText.trim();
                        }
                    }
                    if (userText) {
                        extractedInfo.userText = userText;
                        dataWasUpdatedThisTime = true;
                        log(`Extracted user text from turn ${index}: "${userText.substring(0, 50)}..."`);
                    }
                }
            } else if (turnContainer.classList.contains('model')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'model';

                // ✅ NEW: Expand collapsed thinking sections BEFORE extraction
                await expandThinkingSections(turn, index);

                // Extract AI thinking output with multiple strategies
                if (!extractedInfo.thoughtText) {
                    let thoughtText = null;

                    if (extractionMode === 'raw') {
                        // RAW MODE selector
                        const rawThought = turn.querySelector('ms-thought-chunk .very-large-text-container');
                        if (rawThought) {
                            thoughtText = rawThought.textContent.trim();
                        }
                    } else {
                        // RENDERED MODE selector
                        let thoughtNode = turn.querySelector('ms-thought-chunk .mat-expansion-panel-body ms-cmark-node');
                        if (thoughtNode) {
                            thoughtText = thoughtNode.textContent.trim();
                        }
                    }

                    if (thoughtText && thoughtText.length >= THOUGHT_MIN_LENGTH) {
                        extractedInfo.thoughtText = thoughtText;
                        dataWasUpdatedThisTime = true;
                        log(`Extracted AI thinking from turn ${index}: "${thoughtText.substring(0, 50)}..."`);
                    }
                }


                // Extract AI response (exactly like MaxTab)
                if (!extractedInfo.responseText) {
                    const responseChunks = Array.from(turn.querySelectorAll('.turn-content > ms-prompt-chunk'));
                    const responseTexts = responseChunks
                        .filter(chunk => !chunk.querySelector('ms-thought-chunk'))
                        .map(chunk => {
                            if (extractionMode === 'raw') {
                                // RAW MODE selector
                                const rawContainer = chunk.querySelector('ms-text-chunk .very-large-text-container');
                                if (rawContainer) {
                                    return rawContainer.textContent.trim();
                                }
                            } else {
                                // RENDERED MODE selector
                                const cmarkNode = chunk.querySelector('ms-cmark-node');
                                if (cmarkNode) {
                                    return cmarkNode.innerText.trim();
                                }
                            }
                            return chunk.innerText.trim(); // Fallback
                        })
                        .filter(text => text);

                    if (responseTexts.length > 0) {
                        extractedInfo.responseText = responseTexts.join('\n\n');
                        dataWasUpdatedThisTime = true;
                        log(`Extracted AI response from turn ${index} with ${responseTexts.length} chunks`);
                    } else if (!extractedInfo.thoughtText) {
                        // Fallback only if no thinking was found (like MaxTab)
                        const turnContent = turn.querySelector('.turn-content');
                        if (turnContent) {
                            extractedInfo.responseText = turnContent.innerText.trim();
                            dataWasUpdatedThisTime = true;
                            log(`Extracted AI response from turn ${index} using fallback`);
                        }
                    }
                }

                // Set turn type (exactly like MaxTab)
                if (dataWasUpdatedThisTime) {
                    if (extractedInfo.thoughtText && extractedInfo.responseText) extractedInfo.type = 'model_thought_reply';
                    else if (extractedInfo.responseText) extractedInfo.type = 'model_reply';
                    else if (extractedInfo.thoughtText) extractedInfo.type = 'model_thought';
                }
            }

            // Update collected data if anything changed
            if (dataWasUpdatedThisTime) {
                collectedData.set(turnKey, extractedInfo);
                dataUpdatedInExistingTurn = true;
            }
        }

        if (currentTurns.length > 0 && collectedData.size === 0) {
            log("Warning: Chat turns exist but no data could be extracted. Please check selectors.", 'warn');
        } else {
            log(`Scroll ${scrollCount}/${MAX_SCROLL_ATTEMPTS}... Found ${collectedData.size} total records`);
        }

        return newlyFoundCount > 0 || dataUpdatedInExistingTurn;
    }

    /**
     * Performs high-speed auto-scrolling through AI Studio conversations to capture all content.
     * Uses incremental scrolling with hardcoded delays for maximum speed, detecting end conditions
     * and handling problematic scroll jumps. Performs final collection passes for completeness.
     * @async
     * @param {string} direction - The direction to scroll ('down' or 'up').
     * @returns {Promise<boolean>} - True if the scrolling completed successfully, false otherwise.
     */
    async function autoScroll_AiStudio(direction = 'down') {
        log(`Starting auto-scroll (direction: ${direction})...`);
        isScrolling = true;
        collectedData.clear();
        scrollCount = 0;
        noChangeCounter = 0;
        scrollIncrement = SCROLL_INCREMENT_INITIAL; // Reset to initial value
        
        const scroller = getMainScrollerElement_AiStudio();
        if (!scroller) {
            log('Error: Cannot find scroll area!', 'error');
            alert('Unable to find chat scroll area. Auto-scroll cannot proceed.');
            isScrolling = false;
            return false;
        }
        
        log(`Using scroll element: ${scroller.tagName}.${scroller.className.split(' ').join('.')}`);

        const isWindowScroller = (scroller === document.documentElement || scroller === document.body);
        const getScrollTop = () => isWindowScroller ? window.scrollY : scroller.scrollTop;
        const getScrollHeight = () => isWindowScroller ? document.documentElement.scrollHeight : scroller.scrollHeight;
        const getClientHeight = () => isWindowScroller ? window.innerHeight : scroller.clientHeight;

        if (direction === 'up') {
            if (isWindowScroller) { window.scrollTo({ top: getScrollHeight(), behavior: 'instant' }); }
            else { scroller.scrollTo({ top: scroller.scrollHeight, behavior: 'instant' }); }
            await delay(UPWARD_SCROLL_DELAY_MS);
        }

        // Start from top and ensure content is loaded (extension pattern)
        scroller.scrollTop = 0;
        await delay(INITIAL_SCROLL_DELAY);
        scroller.scrollTop = scroller.scrollHeight;
        await delay(INITIAL_SCROLL_DELAY);
        scroller.scrollTop = 0;
        await delay(INITIAL_SCROLL_DELAY);

        log(`Starting incremental scroll (up to ${MAX_SCROLL_ATTEMPTS} attempts)...`);
        let lastScrollTop = -1;
        let reachedEnd = false;
        let scrollMessages = ["scrolling...", "scrolling..", "scrolling.", "scrolling"];
        
        // Initial collection
        const desiredMode = PREFER_RAW_MODE ? 'raw' : 'rendered';
        const initialNewCount = await extractDataIncremental_AiStudio(desiredMode);
        log(`Initial collection: ${collectedData.size} messages`);
        
        while (scrollCount < MAX_SCROLL_ATTEMPTS && !reachedEnd && isScrolling) {
            if (abortController.signal.aborted) {
                log('Scroll aborted by user.', 'warn');
                isScrolling = false;
                break;
            }
            const currentTop = scroller.scrollTop;
            const clientHeight = scroller.clientHeight;
            const scrollHeight = scroller.scrollHeight;
            
            // End detection: Check if we've reached the bottom of the conversation
            // Allow tolerance for potential rounding issues
            if (currentTop + clientHeight >= scrollHeight - BOTTOM_DETECTION_TOLERANCE) {
                log("Reached bottom of conversation - no more content to scroll", 'success');
                reachedEnd = true;
                break;
            }

            // Calculate the next scroll target position
            // Add the increment to the current position, but don't exceed the maximum possible scroll
            let intendedScrollTarget = currentTop + scrollIncrement;
            const maxPossibleScrollTop = scrollHeight - clientHeight;
            if (intendedScrollTarget > maxPossibleScrollTop) {
                intendedScrollTarget = maxPossibleScrollTop; // Clamp to bottom
            }

            // Execute the scroll operation
            scroller.scrollTop = intendedScrollTarget;
            scrollCount++;
            
            // Machine-gun speed delay: 50ms for maximum performance
            // May miss content on very slow connections
            await delay(SCROLL_DELAY_MS);
            
            const effectiveScrollTop = scroller.scrollTop;
            const actualScrolledDistance = effectiveScrollTop - currentTop;
            
            // Detect problematic scroll jumps that might indicate UI issues
            // A jump > configured factor of intended distance suggests the UI may have re-arranged
            let isProblematicJump = false;
            if (actualScrolledDistance > PROBLEMATIC_JUMP_FACTOR * scrollIncrement && intendedScrollTarget < maxPossibleScrollTop - BOTTOM_DETECTION_TOLERANCE) {
                isProblematicJump = true;
                log(`Scroll jump detected. From: ${currentTop}, Aimed: ${intendedScrollTarget}, Got: ${effectiveScrollTop}`, 'warn');
            }

            // Detect when scrolling has effectively stopped (conversation end)
            // If we moved less than the minimum threshold and this isn't the first scroll, assume we're at the end
            if (actualScrolledDistance < MIN_SCROLL_DISTANCE_THRESHOLD && scrollCount > 1) {
                log("Scroll effectively stopped, assuming end of conversation", 'success');
                reachedEnd = true;
                break;
            }

            // Extract any newly visible messages after this scroll position
            const newlyAddedCount = await extractDataIncremental_AiStudio(desiredMode);
            
            // Update progress indicator with cycling messages for better UX
            const loadingMessage = scrollMessages[(scrollCount - 1) % scrollMessages.length];
            const indicatorMessage = `${loadingMessage} (Found ${collectedData.size} messages)`;
            
            log(indicatorMessage);

            // Update tracking variables for next iteration
            lastScrollTop = effectiveScrollTop;
        }

        // Handle different completion scenarios with detailed logging
        if (!isScrolling && scrollCount < MAX_SCROLL_ATTEMPTS && !abortController.signal.aborted) {
            log(`Scroll manually stopped by user (total ${scrollCount} attempts).`, 'warn');
        } else if (scrollCount >= MAX_SCROLL_ATTEMPTS) {
            log(`Reached maximum scroll attempts limit (${MAX_SCROLL_ATTEMPTS}). Some content may be missing.`, 'warn');
        } else if (reachedEnd) {
            log(`Scroll completed successfully after ${scrollCount} attempts. All conversation content captured.`, 'success');
        }

        // Final collection passes to ensure no content was missed
        // These additional passes capture any content that might have been loaded during scrolling
        log("Performing final collection passes to ensure completeness...");
        
        // Pass 1: Top of conversation
        scroller.scrollTop = 0;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);
        
        // Pass 2: Middle of conversation
        scroller.scrollTop = scroller.scrollHeight / 2;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);
        
        // Pass 3: Bottom of conversation
        scroller.scrollTop = scroller.scrollHeight;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);

        log(`Final data collection complete. Total records: ${collectedData.size}`, 'success');
        isScrolling = false;
        return true;
    }

    /**
     * Formats collected conversation data and triggers the download of a Markdown file.
     * Sorts data by DOM order, formats user prompts and AI responses with proper Markdown
     * structure, and generates a downloadable .md file with timestamp.
     * @returns {boolean} - True if the file was successfully created and downloaded, false otherwise.
     */
    function formatAndTriggerDownload() {
        log(`Processing ${collectedData.size} records and generating file...`);
        const finalTurnsInDom = document.querySelectorAll('ms-chat-turn');
        let sortedData = [];
        
        // Sort by DOM order - now using the turn elements as keys
        finalTurnsInDom.forEach(turnNode => {
            if (collectedData.has(turnNode)) {
                sortedData.push(collectedData.get(turnNode));
            }
        });

        log(`Final export: ${sortedData.length} records found for export`);

        if (sortedData.length === 0) {
            log('No valid records found for export!', 'error');
            alert('After scrolling, no chat records were found for export. Please check the console for details.');
            return false;
        }

        let fileContent = 'Google AI Studio Chat Records (Auto-Scroll Capture)\n';
        fileContent += '=========================================\n\n';
        fileContent += `Exported: ${new Date().toLocaleString()}\n`;
        fileContent += `Total Records: ${sortedData.length}\n\n`;

        sortedData.forEach(item => {
            let parts = [];
            
            if (INCLUDE_USER_MESSAGES && item.type === 'user' && item.userText) {
                parts.push(`--- User ---\n${item.userText}`);
            }
            
            // Include AI thinking output
            if (INCLUDE_AI_THINKING && (item.type === 'model_thought' || item.type === 'model_thought_reply')) {
                if (item.thoughtText) {
                    parts.push(`--- AI Thinking ---\n${item.thoughtText}`);
                }
            }
            
            // Include AI response
            if (INCLUDE_AI_RESPONSES && (item.type === 'model_reply' || item.type === 'model_thought_reply')) {
                if (item.responseText) {
                    parts.push(`--- AI Response ---\n${item.responseText}`);
                }
            }
            
            if (parts.length > 0) {
                const turnContent = parts.join('\n\n');
                fileContent += turnContent + '\n\n------------------------------\n\n';
            }
        });

        // Add warning if no content was exported due to filters
        if (sortedData.length > 0 && !fileContent.includes('---')) {
            log('Warning: All content was filtered out based on configuration settings', 'warn');
            alert('No content to export based on current filter settings. Please check INCLUDE_* configuration options.');
            return false;
        }

        // Clean up trailing separator
        fileContent = fileContent.replace(/\n\n------------------------------\n\n$/, '\n').trim();

        try {
            const blob = new Blob([fileContent], { type: 'text/markdown;charset=utf-8' });
            const link = document.createElement('a');
            const url = URL.createObjectURL(blob);
            link.href = url;
            link.download = `${EXPORT_FILENAME_PREFIX}${getCurrentTimestamp()}.md`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            log("File successfully generated and download triggered.", 'success');
            return true;
        } catch (e) {
            log(`File creation failed: ${e.message}`, 'error');
            alert("Error creating download file: " + e.message);
            return false;
        }
    }

    /**
     * Aborts the currently running extraction process.
     */
    function abortExtraction() {
        if (abortController) {
            abortController.abort();
        }
        isScrolling = false;
        log('Extraction manually aborted by user.', 'warn');
        setButtonState('IDLE');
    }
 
    /**
     * Main orchestrator function for the export process.
     * Handles the complete workflow including Raw Mode toggle, scrolling, data extraction,
     * and file download. Ensures proper error handling and UI state management.
     * @async
     */
    async function handleScrollExtraction() {
        abortController = new AbortController();
        setButtonState('WORKING');
 
        const currentMode = detectCurrentMode();
        const desiredMode = PREFER_RAW_MODE ? 'raw' : 'rendered';
        let needsToggle = (currentMode !== desiredMode);
        let modeWasToggled = false;
 
        try {
            if (needsToggle) {
                log(`Switching from ${currentMode} to ${desiredMode} mode...`);
                modeWasToggled = await toggleRawMode();
                if (!modeWasToggled) {
                    throw new Error(`Failed to switch to ${desiredMode} Mode.`);
                }
            } else {
                log(`Already in desired ${desiredMode} mode, no toggle needed.`);
            }
 
            if (abortController.signal.aborted) throw new Error("Aborted after mode switch.");
 
            const scrollSuccess = await autoScroll_AiStudio('down');
            if (scrollSuccess === false && !abortController.signal.aborted) {
                throw new Error("Scrolling process failed or was incomplete.");
            }
 
            if (abortController.signal.aborted) throw new Error("Aborted during scroll.");
 
            log('Scroll completed, preparing final data processing...');
            await delay(FINAL_CAPTURE_DELAY_MS);
            await extractDataIncremental_AiStudio(desiredMode);
            await delay(POST_EXTRACTION_DELAY_MS);
            
            const downloadSuccess = formatAndTriggerDownload();
            if (!downloadSuccess) {
                 throw new Error("File generation or download failed.");
            }
 
            setButtonState('SUCCESS');
 
        } catch (error) {
            if (!abortController.signal.aborted) {
                log(`Error during processing: ${error.message}`, 'error');
                // alert(`An error occurred: ${error.message}`);
                setButtonState('ERROR');
            } else {
                log('Process was aborted, cleaning up.', 'warn');
            }
        } finally {
            if (modeWasToggled) {
                log(`Switching back to ${currentMode} mode...`);
                await toggleRawMode();
            }
            isScrolling = false;
        }
    }

    //================================================================================
    // UI CREATION - User interface components and interactions
    //================================================================================

    /**
     * Sets the visual state of the export button.
     * @param {'IDLE'|'WORKING'|'SUCCESS'|'ERROR'} state The target state.
     */
    function setButtonState(state) {
        exportButtonState = state;
        if (!exportButton || !exportIcon) return;
 
        // Remove existing color classes
        exportButton.classList.remove('success', 'error');
 
        switch (state) {
            case 'IDLE':
                exportIcon.textContent = 'download';
                exportButton.title = 'Export Chat to Markdown';
                exportButton.disabled = false;
                break;
            case 'WORKING':
                exportIcon.textContent = 'cancel';
                exportIcon.style.color = '#d93025'; // Red color for stop
                exportButton.title = 'Stop Export';
                exportButton.disabled = false;
                break;
            case 'SUCCESS':
                exportIcon.textContent = 'check_circle';
                exportIcon.style.color = '#1e8e3e'; // Green color for success
                exportButton.title = 'Export Successful!';
                exportButton.disabled = true;
                setTimeout(() => setButtonState('IDLE'), SUCCESS_RESET_TIMEOUT_MS);
                break;
            case 'ERROR':
                exportIcon.textContent = 'error';
                exportIcon.style.color = '#d93025'; // Red color for error
                exportButton.title = 'Export Failed!';
                exportButton.disabled = true;
                setTimeout(() => setButtonState('IDLE'), ERROR_RESET_TIMEOUT_MS);
                break;
        }
 
        // Reset color for non-transient states
        if (state === 'IDLE') {
            exportIcon.style.color = ''; // Use default color
        }
    }
 
    /**
     * Creates and initializes the export button UI element with Google-styled appearance.
     * Positions the button fixed at the bottom-left of the page and attaches the export handler.
     */
    function createUI() {
        const toolbarRight = document.querySelector('ms-toolbar .toolbar-right');
        if (!toolbarRight || document.getElementById('export-button-container')) {
            return;
        }
 
        log('Toolbar found, creating export button...');
 
        // Create container for the button group
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'export-button-container';
        buttonContainer.style.cssText = 'display: flex; align-items: center; margin: 0 4px; position: relative; z-index: 2147483647;';
 
        // Create main export button
        exportButton = document.createElement('button');
        exportButton.id = 'aistudio-export-button';
        exportButton.setAttribute('ms-button', '');
        exportButton.setAttribute('variant', 'icon-borderless');
        exportButton.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon';
        
        exportButton.addEventListener('click', () => {
            if (exportButtonState === 'IDLE') {
                handleScrollExtraction();
            } else if (exportButtonState === 'WORKING') {
                abortExtraction();
            }
        });
 
        exportIcon = document.createElement('span');
        exportIcon.className = 'material-symbols-outlined notranslate ms-button-icon-symbol';
        exportButton.appendChild(exportIcon);
 
        // Create settings button
        const settingsButton = document.createElement('button');
        settingsButton.id = 'aistudio-settings-button';
        settingsButton.title = 'Export Settings';
        settingsButton.setAttribute('ms-button', '');
        settingsButton.setAttribute('variant', 'icon-borderless');
        settingsButton.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon';
        
        const settingsIcon = document.createElement('span');
        settingsIcon.className = 'material-symbols-outlined notranslate ms-button-icon-symbol';
        settingsIcon.textContent = 'settings';
        settingsButton.appendChild(settingsIcon);

        // Append buttons to container
        buttonContainer.appendChild(exportButton);
        buttonContainer.appendChild(settingsButton);

        // Create settings panel with ALTERNATIVE STRATEGY 1: position: fixed
        // This moves the panel out of the toolbar hierarchy entirely
        settingsPanel = document.createElement('div');
        settingsPanel.id = 'aistudio-settings-panel';
        settingsPanel.style.cssText = `
            position: fixed;
            top: 52px;
            right: 16px;
            width: 240px;
            background: #2d2e30;
            border: 1px solid #5f6368;
            border-radius: 8px;
            z-index: 2147483647;
            font-family: 'Google Sans', sans-serif;
            font-size: 14px;
            display: none;
            flex-direction: column;
            box-sizing: border-box;
            padding-top: 16px;
            padding-right: 16px;
            padding-bottom: 16px;
            padding-left: 16px;
            box-shadow: 0 8px 16px rgba(0,0,0,0.3);
            color: #e8eaed;
            isolation: isolate;
            transform: translateZ(0);
        `;

        function createCheckbox(label, configVar, callback) {
            const wrapper = document.createElement('div');
            wrapper.style.cssText = 'display: flex; align-items: center; margin-bottom: 10px;';
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = configVar;
            // Generate a unique ID that won't conflict between chat sessions
            const timestamp = Date.now();
            const randomId = Math.floor(Math.random() * 10000);
            checkbox.id = `export-checkbox-${label.replace(/\s+/g, '-')}-${timestamp}-${randomId}`;
            const labelEl = document.createElement('label');
            labelEl.textContent = label;
            labelEl.style.marginLeft = '10px';
            labelEl.style.cursor = 'pointer'; // Visual hint that it's clickable
            labelEl.setAttribute('for', checkbox.id);
            
            // Store a reference to the callback for both label and checkbox
            const handleChange = (checked) => {
                checkbox.checked = checked;
                callback(checked);
            };
            
            // Make the label directly toggle the checkbox
            labelEl.addEventListener('click', (event) => {
                event.preventDefault(); // Prevent default behavior
                handleChange(!checkbox.checked);
            });
            
            // Also ensure the checkbox itself works
            checkbox.addEventListener('change', (event) => callback(event.target.checked));
            
            wrapper.appendChild(checkbox);
            wrapper.appendChild(labelEl);
            settingsPanel.appendChild(wrapper);
            return checkbox;
        }

        preferRawModeCheckbox = createCheckbox('Prefer Raw Mode', PREFER_RAW_MODE, (val) => PREFER_RAW_MODE = val);
        includeUserMessagesCheckbox = createCheckbox('Include User Messages', INCLUDE_USER_MESSAGES, (val) => INCLUDE_USER_MESSAGES = val);
        includeAiResponsesCheckbox = createCheckbox('Include AI Responses', INCLUDE_AI_RESPONSES, (val) => INCLUDE_AI_RESPONSES = val);
        includeAiThinkingCheckbox = createCheckbox('Include AI Thinking', INCLUDE_AI_THINKING, (val) => INCLUDE_AI_THINKING = val);
        
        // Remove margin-bottom from the last checkbox to prevent extra space at the bottom
        const lastWrapper = settingsPanel.lastElementChild;
        if (lastWrapper) {
            lastWrapper.style.marginBottom = '0';
        }
        
        // ALTERNATIVE STRATEGY 1: Append panel to body instead of buttonContainer
        // This completely removes it from the toolbar stacking context
        document.body.appendChild(settingsPanel);
        
        // Update position dynamically based on button location
        const updatePanelPosition = () => {
            const buttonRect = settingsButton.getBoundingClientRect();
            settingsPanel.style.top = `${buttonRect.bottom + 4}px`;
            settingsPanel.style.right = `${window.innerWidth - buttonRect.right}px`;
        };

        // Settings button logic
        settingsButton.addEventListener('click', (event) => {
            event.stopPropagation();
            const isHidden = settingsPanel.style.display === 'none' || settingsPanel.style.display === '';
            
            if (isHidden) {
                updatePanelPosition(); // Update position before showing
            }
            
            settingsPanel.style.display = isHidden ? 'flex' : 'none';
        });
        
        // Update position on scroll/resize
        window.addEventListener('scroll', () => {
            if (settingsPanel.style.display === 'flex') {
                updatePanelPosition();
            }
        });
        window.addEventListener('resize', () => {
            if (settingsPanel.style.display === 'flex') {
                updatePanelPosition();
            }
        });

        // Close panel on outside click
        document.addEventListener('click', (event) => {
            if (!settingsPanel.contains(event.target) && !settingsButton.contains(event.target)) {
                settingsPanel.style.display = 'none';
            }
        });

        // Inject into toolbar
        const moreButton = toolbarRight.querySelector('button[iconname="more_vert"]');
        if (moreButton) {
            toolbarRight.insertBefore(buttonContainer, moreButton);
        } else {
            toolbarRight.appendChild(buttonContainer);
        }

        log("Toolbar UI initialization complete.", 'success');
        setButtonState('IDLE'); // Set the initial state of the button
    }

    //================================================================================
    // MAIN EXECUTION - Script initialization and startup
    //================================================================================

    /**
     * Main entry point function that initializes the AI Studio Export script.
     * Creates the UI and sets up the script for user interaction.
     */
    function initialize() {
        log("Initializing AI Studio Export...");
        // Initial attempt to create the UI in case the toolbar is already present.
        createUI();

        // Set up an observer to re-create the UI if the toolbar is ever re-rendered
        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 && (node.tagName === 'MS-TOOLBAR' || node.querySelector('ms-toolbar'))) {
                            createUI();
                            return; // We found the toolbar, no need to keep searching
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        log("MutationObserver is now watching for toolbar changes.");
    }

    initialize();

})();