AI Studio - Advanced Control Suite (History, UI, Lag Fix)

Advanced control for Google AI Studio: Chat history modes (Exchanges, Vibe), UI Hiding (Sidebars, System Instr.), Input Lag Fix, Dark Theme Popup.

// ==UserScript==
// @name         AI Studio - Advanced Control Suite (History, UI, Lag Fix)
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Advanced control for Google AI Studio: Chat history modes (Exchanges, Vibe), UI Hiding (Sidebars, System Instr.), Input Lag Fix, Dark Theme Popup.
// @author       so it goes...again & Gemini
// @match        https://aistudio.google.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---

    // --- !!! CRITICAL SELECTORS - VERIFY THESE CAREFULLY !!! ---
    const LEFT_SIDEBAR_SELECTOR = 'ms-navbar';// Confirmed from previous snippet
    const RIGHT_SIDEBAR_SELECTOR = 'ms-run-settings';/* !!! VERIFY THIS SELECTOR !!! */ // Verify when OPEN
    const SYSTEM_INSTRUCTIONS_SELECTOR = 'ms-system-instructions';// Assumed Correct - Verify if possible
    const CHAT_INPUT_SELECTOR = 'textarea[aria-label="Type something"]'; // <<< CONFIRMED from snippet
    const RUN_BUTTON_SELECTOR = 'button.run-button[aria-label="Run"]';// <<< CONFIRMED from snippet
    const OVERALL_LAYOUT_SELECTOR = 'body > app-root > ms-app > div';// <<< Best guess, update if needed
    const CHAT_CONTAINER_SELECTOR = 'ms-autoscroll-container';// Stable
    const USER_TURN_SELECTOR = 'ms-chat-turn:has([data-turn-role="User"])'; // Stable
    const AI_TURN_SELECTOR = 'ms-chat-turn:has([data-turn-role="Model"])'; // Stable
    const BUTTON_CONTAINER_SELECTOR = 'div.right-side';// Stable
    // --- END CRITICAL SELECTORS ---

    const SCRIPT_BUTTON_ID = 'advanced-control-toggle-button';
    const POPUP_ID = 'advanced-control-popup';
    const FAKE_INPUT_ID = 'advanced-control-fake-input';
    const FAKE_RUN_BUTTON_ID = 'advanced-control-fake-run-button';
    const LAYOUT_HIDE_CLASS = 'adv-controls-hide-ui'; // Class added to OVERALL_LAYOUT_SELECTOR

    // Settings Keys
    const SETTINGS_KEY = 'aiStudioAdvancedControlSettings_v4'; // New key for this version

    // Default Settings
    const DEFAULT_SETTINGS = {
        mode: 'manual',// 'off' | 'manual' | 'auto' | 'vibe'
        numTurnsToShow: 2,// Number of exchanges (Manual/Auto) or AI turns (unused in Vibe v4)
        hideSidebars: false,// User preference for hiding sidebars
        hideSystemInstructions: false, // User preference for hiding sys instructions
        useLagFixInput: false,// User preference for the input lag fix
    };

    // --- State ---
    let settings = { ...DEFAULT_SETTINGS };
    let isCurrentlyHidden = false; // Chat history hidden state
    let scriptToggleButton = null;
    let popupElement = null;
    let chatObserver = null;
    let debounceTimer = null;
    let realChatInput = null; // Cache the real input element for lag fix
    let realRunButton = null; // Cache the real run button for lag fix
    let fakeChatInput = null; // Cache the fake input element

    // --- Icons ---
    const ICON_VISIBLE = 'visibility';
    const ICON_HIDDEN = 'visibility_off';
    const ICON_VIBE = 'neurology'; // Or choose another icon for Vibe button

        // --- Core Logic: Chat History Hiding ---
    function applyChatVisibilityRules() {
        console.log("AC Script: Applying chat visibility. Mode:", settings.mode, "Num:", settings.numTurnsToShow);
        const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
        if (!chatContainer) {
             console.warn("AC Script: Chat container not found for visibility rules.");
             return;
        }

        const allUserTurns = Array.from(chatContainer.querySelectorAll(USER_TURN_SELECTOR));
        const allAiTurns = Array.from(chatContainer.querySelectorAll(AI_TURN_SELECTOR));
        // Query all turns together for simplicity in show/hide all scenarios and iteration
        const allTurns = Array.from(chatContainer.querySelectorAll(`${USER_TURN_SELECTOR}, ${AI_TURN_SELECTOR}`));

        let turnsToShow = [];
        let localDidHideSomething = false;

        // Helper to set display style idempotently
        const setDisplay = (element, visible) => {
            const targetDisplay = visible ? '' : 'none';
            if (element.style.display !== targetDisplay) {
                element.style.display = targetDisplay;
            }
        };

        switch (settings.mode) {
            case 'off':
                // --- Show All ---
                allTurns.forEach(turn => setDisplay(turn, true));
                localDidHideSomething = false;
                break; // End of 'off' case

            case 'vibe':
                // --- VIBE Mode: Show only the very last AI turn, hide all user turns ---
                allUserTurns.forEach(turn => {
                    setDisplay(turn, false);
                });
                 // If any user turns exist, we definitely hid something (or tried to)
                 if (allUserTurns.length > 0) localDidHideSomething = true;

                // Now handle AI turns
                if (allAiTurns.length > 0) {
                    const lastAiTurn = allAiTurns[allAiTurns.length - 1];
                    allAiTurns.forEach(turn => {
                        const shouldBeVisible = (turn === lastAiTurn);
                        setDisplay(turn, shouldBeVisible);
                        // If we hide any AI turn (i.e., not the last one), mark as hidden
                        if (!shouldBeVisible) localDidHideSomething = true;
                    });
                }
                // No 'else' needed - if no AI turns, nothing to show.

                break; // End of 'vibe' case

            case 'manual':
            case 'auto':{
                // --- Manual/Auto Mode: Show last N *exchanges* (User+AI pairs) ---
                const numExchangesToShow = settings.numTurnsToShow;

                if (numExchangesToShow <= 0) { // Show all if 0 or less
                    allTurns.forEach(turn => setDisplay(turn, true));
                    localDidHideSomething = false;
                } else {
                    let exchangesFound = 0;
                    turnsToShow = []; // Stores the elements that should be visible
                    // Iterate backwards through all turns to find pairs/exchanges
                    for (let i = allTurns.length - 1; i >= 0; i--) {
                        const currentTurn = allTurns[i];

                        if (currentTurn.matches(AI_TURN_SELECTOR)) {
                            // Found an AI turn
                            exchangesFound++; // Count this as (part of) an exchange
                            turnsToShow.unshift(currentTurn); // Definitely show the AI turn

                            // Look for the User turn immediately before it
                            if (i > 0 && allTurns[i - 1].matches(USER_TURN_SELECTOR)) {
                                turnsToShow.unshift(allTurns[i - 1]); // Show the preceding User turn
                                i--; // Decrement i again to skip this User turn in the next iteration
                            }
                            // If no preceding user turn, it's an "orphan" AI start, still counts as 1 exchange

                        } else if (currentTurn.matches(USER_TURN_SELECTOR)) {
                            // Found a User turn without a following AI (maybe the very last prompt)
                             exchangesFound++; // Count this incomplete exchange
                             turnsToShow.unshift(currentTurn); // Show this User turn
                        }

                        // Stop if we have found enough exchanges
                        if (exchangesFound >= numExchangesToShow) {
                            break;
                        }
                    } // End backwards loop

                    // Now apply visibility based on the collected turnsToShow list
                    allTurns.forEach(turn => {
                        const shouldBeVisible = turnsToShow.includes(turn);
                        setDisplay(turn, shouldBeVisible);
                        if (!shouldBeVisible) localDidHideSomething = true; // If any turn is hidden
                    });
                }
                break; // End of 'manual'/'auto' case
            }
        } // End switch

        // --- Update button icon state ---
        if (isCurrentlyHidden !== localDidHideSomething) {
            isCurrentlyHidden = localDidHideSomething;
            updateScriptToggleButtonAppearance(); // Assumes this function exists elsewhere
            console.log(`AC Script: Chat visibility updated. Currently hidden: ${isCurrentlyHidden}`);
        }
    } // End applyChatVisibilityRules
    // --- Core Logic: UI Element Hiding ---
    function applyLayoutRules() {
        const layoutContainer = document.querySelector(OVERALL_LAYOUT_SELECTOR);
        if (!layoutContainer) {
            console.warn("AC Script: Overall layout container not found:", OVERALL_LAYOUT_SELECTOR);
            return;
        }

        const forceHide = settings.mode === 'vibe'; // Vibe mode forces UI hidden
        const shouldHideSidebars = forceHide || settings.hideSidebars;
        const shouldHideSysInstructions = forceHide || settings.hideSystemInstructions;
        const shouldApplyLagFix = forceHide || settings.useLagFixInput;

        // Toggle main class on layout container
        layoutContainer.classList.toggle(`${LAYOUT_HIDE_CLASS}-sidebars`, shouldHideSidebars);
        layoutContainer.classList.toggle(`${LAYOUT_HIDE_CLASS}-sysinstruct`, shouldHideSysInstructions);

        // Activate/Deactivate Lag Fix Input
        toggleLagFixInput(shouldApplyLagFix);

        console.log(`AC Script: Applied Layout Rules. Mode: ${settings.mode}, Hide Sidebars: ${shouldHideSidebars}, Hide SysInstruct: ${shouldHideSysInstructions}, LagFix: ${shouldApplyLagFix}`);

        // Update UI state in popup if open
        if (popupElement?.style.display === 'block') {
            updatePopupUIState();
        }
    }


    // --- Settings Management ---
    async function loadSettings() {
        const storedSettings = await GM_getValue(SETTINGS_KEY, DEFAULT_SETTINGS);
        settings = { ...DEFAULT_SETTINGS, ...storedSettings };
        isCurrentlyHidden = false; // Reset runtime state
        console.log("AC Script: Settings loaded:", settings);
    }

    async function saveSettings() {
        // Make sure to save all persistent settings
        const settingsToSave = {
            mode: settings.mode,
            numTurnsToShow: settings.numTurnsToShow,
            hideSidebars: settings.hideSidebars,
            hideSystemInstructions: settings.hideSystemInstructions,
            useLagFixInput: settings.useLagFixInput
        };
        await GM_setValue(SETTINGS_KEY, settingsToSave);
        console.log("AC Script: Settings saved:", settingsToSave);
    }

    // Update setting, save, and apply relevant rules
    function updateSetting(key, value) {
        if (settings[key] === value) return; // No change

        console.log(`AC Script: Setting ${key} changing to ${value}`);
        const previousMode = settings.mode;
        settings[key] = value;

        let needsChatRules = false;
        let needsLayoutRules = false;
        let needsObserverReinit = false;
        let needsPopupClose = false;

        if (key === 'mode') {
            needsChatRules = true;
            needsLayoutRules = true; // Mode change (esp. Vibe) affects layout
            needsObserverReinit = (value === 'auto' || previousMode === 'auto');
            needsPopupClose = true; // Close popup on mode change (radio or vibe button)
        } else if (key === 'numTurnsToShow') {
            if (settings.mode === 'manual' || settings.mode === 'auto') {
                needsChatRules = true; // Apply if relevant mode is active
            }
        } else if (key === 'hideSidebars' || key === 'hideSystemInstructions' || key === 'useLagFixInput') {
            needsLayoutRules = true; // These directly affect layout/input fix
        }

        saveSettings(); // Save any change

        if (needsLayoutRules) {
            applyLayoutRules(); // Apply layout changes (handles lag fix toggle too)
        }
        if (needsChatRules) {
            // Delay slightly after potential layout changes
            setTimeout(applyChatVisibilityRules, 50);
        }
        if (needsObserverReinit) {
            initChatObserver();
        }

        // Update popup UI state if it's open, *before* closing it
        if (popupElement?.style.display === 'block') {
            updatePopupUIState();
        }

        if (needsPopupClose) {
            hidePopup();
        }
    }

    // --- UI Elements (Button & Popup) ---
    function updateScriptToggleButtonAppearance() {
        if (!scriptToggleButton) return;
        const iconSpan = scriptToggleButton.querySelector('.material-symbols-outlined');
        if (iconSpan) {
            iconSpan.textContent = isCurrentlyHidden ? ICON_HIDDEN : ICON_VISIBLE;
        }
        const tooltipText = isCurrentlyHidden ? 'Chat history hidden (Click for options)' : 'Chat history visible (Click for options)';
        scriptToggleButton.setAttribute('aria-label', tooltipText);
        scriptToggleButton.setAttribute('mattooltip', tooltipText); // Attempt to update tooltip
        // Update Greasemonkey menu command text
        GM_registerMenuCommand(isCurrentlyHidden ? 'Show All History (via settings)' : 'Hide History (via settings)', togglePopup);
    }

    function createScriptToggleButton() {
        if (document.getElementById(SCRIPT_BUTTON_ID)) {
            scriptToggleButton = document.getElementById(SCRIPT_BUTTON_ID);
            updateScriptToggleButtonAppearance(); // Ensure icon is correct
            return;
        }
        const buttonContainer = document.querySelector(BUTTON_CONTAINER_SELECTOR);
        if (!buttonContainer) {
            console.error("AC Script: Could not find button container:", BUTTON_CONTAINER_SELECTOR); return;
        }
        console.log("AC Script: Creating settings button.");
        scriptToggleButton = document.createElement('button');
        scriptToggleButton.id = SCRIPT_BUTTON_ID;
        scriptToggleButton.className = 'mdc-icon-button mat-mdc-icon-button mat-unthemed mat-mdc-button-base gmat-mdc-button advanced-control-button';
        scriptToggleButton.style.marginLeft = '4px'; scriptToggleButton.style.marginRight = '4px';
        scriptToggleButton.style.order = '-1'; // Place first

        // --- FIX for TrustedHTML: Build elements manually ---
        const spanRipple = document.createElement('span');
        spanRipple.className = 'mat-mdc-button-persistent-ripple mdc-icon-button__ripple';
        scriptToggleButton.appendChild(spanRipple);

        const icon = document.createElement('span');
        icon.className = 'material-symbols-outlined notranslate';
        icon.setAttribute('aria-hidden', 'true');
        // Icon textContent (visibility/visibility_off) will be set by updateScriptToggleButtonAppearance
        scriptToggleButton.appendChild(icon);

        const focusIndicator = document.createElement('span');
        focusIndicator.className = 'mat-focus-indicator';
        scriptToggleButton.appendChild(focusIndicator);

        const touchTarget = document.createElement('span');
        touchTarget.className = 'mat-mdc-button-touch-target';
        scriptToggleButton.appendChild(touchTarget);
        // --- END FIX ---

        scriptToggleButton.addEventListener('click', togglePopup);
        buttonContainer.insertBefore(scriptToggleButton, buttonContainer.firstChild);
        updateScriptToggleButtonAppearance(); // Set initial icon/tooltip
        console.log("AC Script: Settings button added into", BUTTON_CONTAINER_SELECTOR);
    }
    function createPopupHtml() {
        const popup = document.createElement('div');
        popup.id = POPUP_ID;
        popup.className = 'advanced-control-popup';

        // --- Header ---
        const header = document.createElement('div');
        header.className = 'popup-header';

        const headerSpan = document.createElement('span');
        headerSpan.textContent = 'Advanced Controls'; // Use textContent
        header.appendChild(headerSpan);

        const closeButton = document.createElement('button');
        closeButton.type = 'button';
        closeButton.className = 'close-popup-button';
        closeButton.setAttribute('aria-label', 'Close settings');
        closeButton.textContent = '×'; // Use textContent
        closeButton.addEventListener('click', hidePopup);
        header.appendChild(closeButton);

        popup.appendChild(header);

        // --- Content Area ---
        const content = document.createElement('div');
        content.className = 'popup-content';

        // --- Vibe Mode Button ---
        const vibeButtonContainer = document.createElement('div');
        vibeButtonContainer.className = 'popup-section vibe-section';
        const vibeButton = document.createElement('button');
        vibeButton.id = 'vibe-mode-button';
        vibeButton.className = 'vibe-button';
        // Build button content manually
        const vibeIconSpan = document.createElement('span');
        vibeIconSpan.className = 'material-symbols-outlined';
        vibeIconSpan.textContent = ICON_VIBE;
        vibeButton.appendChild(vibeIconSpan);
        vibeButton.appendChild(document.createTextNode(' Activate VIBE Mode')); // Add text node
        vibeButton.addEventListener('click', () => updateSetting('mode', 'vibe'));
        vibeButtonContainer.appendChild(vibeButton);
        content.appendChild(vibeButtonContainer);

        // --- History Hiding Mode ---
        const historyGroup = document.createElement('fieldset');
        historyGroup.className = 'popup-section history-section';
        const historyLegend = document.createElement('legend');
        historyLegend.textContent = 'Chat History Mode:';
        historyGroup.appendChild(historyLegend);

        const modes = ['off', 'manual', 'auto'];
        const modeLabels = { off: 'Off (Show All)', manual: 'Manual Hide', auto: 'Auto Hide' };
        modes.forEach(modeValue => {
            const div = document.createElement('div');
            div.className = 'popup-setting radio-setting';

            const input = document.createElement('input');
            input.type = 'radio';
            input.name = 'history-mode-radio';
            input.id = `mode-${modeValue}-radio`;
            input.value = modeValue;
            input.addEventListener('change', (e) => {
                if (e.target.checked) updateSetting('mode', e.target.value);
            });

            const label = document.createElement('label');
            label.htmlFor = `mode-${modeValue}-radio`;
            label.textContent = modeLabels[modeValue];

            div.appendChild(input);
            div.appendChild(label);
            historyGroup.appendChild(div);
        });
        content.appendChild(historyGroup);

        // --- Number of Exchanges ---
        const numTurnsSetting = document.createElement('div');
        numTurnsSetting.className = 'popup-setting number-setting';

        const numLabel = document.createElement('label');
        numLabel.htmlFor = 'num-turns-input';
        numLabel.textContent = 'Keep Last:';
        numTurnsSetting.appendChild(numLabel);

        const numInput = document.createElement('input');
        numInput.type = 'number';
        numInput.id = 'num-turns-input';
        numInput.min = '0';
        numInput.addEventListener('change', (e) => {
            const num = parseInt(e.target.value, 10);
            const newValue = (!isNaN(num) && num >= 0) ? num : DEFAULT_SETTINGS.numTurnsToShow;
            updateSetting('numTurnsToShow', newValue);
            if (e.target.value !== newValue.toString()) e.target.value = newValue;
        });
        numTurnsSetting.appendChild(numInput);

        const numDescSpan = document.createElement('span');
        numDescSpan.id = 'num-turns-description';
        numDescSpan.textContent = 'Exchanges'; // Initial value
        numTurnsSetting.appendChild(numDescSpan);

        content.appendChild(numTurnsSetting);


        // --- UI Hiding Toggles ---
        const uiToggleGroup = document.createElement('fieldset');
        uiToggleGroup.className = 'popup-section ui-toggles-section';
        const uiLegend = document.createElement('legend');
        uiLegend.textContent = 'Interface Hiding:';
        uiToggleGroup.appendChild(uiLegend);

        const createToggle = (id, labelText, settingKey) => {
            const div = document.createElement('div');
            div.className = 'popup-setting toggle-setting';

            const label = document.createElement('label');
            label.htmlFor = id;
            label.className = 'toggle-label';
            label.textContent = labelText;

            const input = document.createElement('input');
            input.type = 'checkbox';
            input.id = id;
            input.className = 'basic-slide-toggle'; // Style with CSS
            input.addEventListener('change', (e) => updateSetting(settingKey, e.target.checked));

            div.appendChild(label); // Add label first
            div.appendChild(input); // Then add input (for styling purposes sometimes)
            uiToggleGroup.appendChild(div);
            // No need to return the input here as it's not used elsewhere directly
        };

        createToggle('hide-sidebars-toggle', 'Hide Sidebars', 'hideSidebars');
        createToggle('hide-sysinstruct-toggle', 'Hide System Instructions', 'hideSystemInstructions');
        createToggle('use-lagfix-toggle', 'Input Lag Fix', 'useLagFixInput');

        content.appendChild(uiToggleGroup);
        popup.appendChild(content);

        // --- Footer ---
        const footer = document.createElement('div');
        footer.className = 'popup-footer';
        const footerSpan = document.createElement('span');
        footerSpan.className = 'footer-note';
        footerSpan.textContent = 'Mode changes close panel. Toggles save instantly.';
        footer.appendChild(footerSpan);
        popup.appendChild(footer);

        return popup;
    }
    // Updates the state of controls within the popup
       // Updates the state of controls within the popup
    function updatePopupUIState() {
        if (!popupElement || popupElement.style.display === 'none') return;

        const isVibe = settings.mode === 'vibe';
        const isOff = settings.mode === 'off';

        // --- Update Vibe Button Appearance ---
        const vibeButton = popupElement.querySelector('#vibe-mode-button');
        if (vibeButton) {
            vibeButton.classList.toggle('active', isVibe);
            // Maybe change text when active?
            const iconSpan = vibeButton.querySelector('.material-symbols-outlined');
            const textNode = vibeButton.lastChild; // Assuming text is last child
            if (isVibe && textNode.nodeType === Node.TEXT_NODE) {
                textNode.textContent = ' VIBE MODE ACTIVE';
                if (iconSpan) iconSpan.textContent = 'check_circle'; // Show checkmark?
            } else if (textNode.nodeType === Node.TEXT_NODE){
                textNode.textContent = ' Activate VIBE Mode';
                if (iconSpan) iconSpan.textContent = ICON_VIBE; // Restore original icon
            }
        }


        // --- Update History Mode Radio Buttons ---
        popupElement.querySelectorAll('input[name="history-mode-radio"]').forEach(radio => {
             radio.checked = (settings.mode === radio.value);
             // --- FIX: DO NOT disable radios when Vibe is active ---
             // Radios should always be clickable to exit Vibe mode
             radio.disabled = false;
        });

        // --- Update Number Input ---
        const numInput = popupElement.querySelector('#num-turns-input');
        const numDesc = popupElement.querySelector('#num-turns-description');
        if (numInput) {
            numInput.value = settings.numTurnsToShow;
            // Disable number input only if mode is Off OR Vibe
            numInput.disabled = isOff || isVibe;
        }
         if(numDesc) {
             numDesc.textContent = (settings.mode === 'manual' || settings.mode === 'auto') ? 'Exchanges (User+AI)' : ' ';
             // Hide description if number input is disabled
             numDesc.style.display = (isOff || isVibe) ? 'none' : '';
         }

        // --- Update UI Toggles State and Disabled Status ---
        const updateToggleUI = (id, settingKey) => {
            const toggle = popupElement.querySelector(`#${id}`);
            if (toggle) {
                toggle.checked = settings[settingKey];
                // FIX: Disable UI toggles ONLY if Vibe mode is active
                toggle.disabled = isVibe;
                 // Also visually grey out the label if disabled
                 const label = popupElement.querySelector(`label[for="${id}"]`);
                 if (label) label.style.opacity = isVibe ? '0.5' : '1';
            }
        };
        updateToggleUI('hide-sidebars-toggle', 'hideSidebars');
        updateToggleUI('hide-sysinstruct-toggle', 'hideSystemInstructions');
        updateToggleUI('use-lagfix-toggle', 'useLagFixInput');

    }
    function showPopup() {
        // ... (Similar to v3.0, creates popup if needed, updates UI, positions, adds listener) ...
        if (!scriptToggleButton) return;
        if (!popupElement) {
            popupElement = createPopupHtml();
            document.body.appendChild(popupElement);
        }
        updatePopupUIState(); // Ensure UI reflects current settings

        const buttonRect = scriptToggleButton.getBoundingClientRect();
        popupElement.style.top = `${buttonRect.bottom + window.scrollY + 5}px`;
        popupElement.style.left = 'auto';
        popupElement.style.right = `${window.innerWidth - buttonRect.right - window.scrollX}px`;
        popupElement.style.display = 'block';
        console.log("AC Script: Popup shown.");
        setTimeout(() => {
            document.addEventListener('click', handleClickOutsidePopup, { capture: true, once: true });
        }, 0);
    }

    function hidePopup() {
        // ... (Similar to v3.0) ...
        if (popupElement) {
            popupElement.style.display = 'none';
            document.removeEventListener('click', handleClickOutsidePopup, { capture: true });
            console.log("AC Script: Popup hidden.");
        }
    }

    function togglePopup(event) {
        // ... (Similar to v3.0) ...
        if (event) event.stopPropagation();
        if (popupElement?.style.display === 'block') { hidePopup(); }
        else { showPopup(); }
    }

    function handleClickOutsidePopup(event) {
        // ... (Similar to v3.0, but check scriptToggleButton too) ...
        if (popupElement?.style.display === 'block' &&
            !popupElement.contains(event.target) &&
            scriptToggleButton && !scriptToggleButton.contains(event.target)) {
            console.log("AC Script: Clicked outside popup.");
            hidePopup();
        } else if (popupElement?.style.display === 'block') {
            // Re-add listener if click was inside
            document.addEventListener('click', handleClickOutsidePopup, { capture: true, once: true });
        }
    }

    // --- Input Lag Fix Logic ---
          // --- Input Lag Fix Logic ---
    function toggleLagFixInput(activate) {
        // Ensure we have the real elements cached or find them
        if (!realChatInput) realChatInput = document.querySelector(CHAT_INPUT_SELECTOR);
        if (!realRunButton) realRunButton = document.querySelector(RUN_BUTTON_SELECTOR);

        if (activate) {
            // --- Activate Lag Fix ---
            if (!realChatInput || !realRunButton) {
                console.error("AC Script: Cannot activate Lag Fix - Real Input or Run button not found! Verify selectors:", CHAT_INPUT_SELECTOR, RUN_BUTTON_SELECTOR);
                 if(settings.useLagFixInput || settings.mode === 'vibe') {
                     if(settings.useLagFixInput) updateSetting('useLagFixInput', false);
                 }
                return; // Stop activation
            }

            // Check if fake elements already exist to prevent duplicates
            const existingFakeInput = document.getElementById(FAKE_INPUT_ID);
            const existingFakeButton = document.getElementById(FAKE_RUN_BUTTON_ID);

            if (!existingFakeInput) { // Only create if it doesn't exist
                console.log("AC Script: Activating Lag Fix Input.");
                try {
                    // --- Hide Real Input ---
                    realChatInput.classList.add('adv-controls-real-input-hidden');

                    // --- Create Fake Input ---
                    fakeChatInput = document.createElement('textarea'); // Use the state variable
                    fakeChatInput.id = FAKE_INPUT_ID;
                    fakeChatInput.className = 'advanced-control-fake-input'; // Class for styling via addStyles
                    fakeChatInput.setAttribute('placeholder', realChatInput.getAttribute('placeholder') || 'Type something...');
                    fakeChatInput.setAttribute('rows', realChatInput.getAttribute('rows') || '1'); // Copy rows attribute
                    // Apply necessary styles directly for layout matching
                    const computedStyle = window.getComputedStyle(realChatInput);
                    fakeChatInput.style.height = computedStyle.height;
                    fakeChatInput.style.resize = computedStyle.resize;
                    fakeChatInput.style.overflow = computedStyle.overflow;
                    fakeChatInput.style.width = '100%';

                    // Insert fake input before the real one
                    realChatInput.parentNode.insertBefore(fakeChatInput, realChatInput);

                    // Explicitly focus the fake input
                     setTimeout(() => fakeChatInput.focus(), 100);

                } catch (error) {
                    console.error("AC Script: Error creating fake input:", error);
                    // Attempt cleanup if input creation failed
                    realChatInput?.classList.remove('adv-controls-real-input-hidden');
                    if(fakeChatInput) fakeChatInput.remove(); // Remove partially created element
                    fakeChatInput = null; // Reset state variable
                    return; // Stop activation
                }
            } else {
                // If fake input exists ensure it gets focus
                fakeChatInput = existingFakeInput; // Ensure state variable is correct
                setTimeout(() => fakeChatInput.focus(), 100);
            }

            if (!existingFakeButton && fakeChatInput) { // Only create button if it doesn't exist AND fake input exists
                 console.log("AC Script: Creating Fake Run Button.");
                 try {
                     const fakeRunButton = document.createElement('button');
                     fakeRunButton.id = FAKE_RUN_BUTTON_ID;
                     fakeRunButton.className = 'advanced-control-fake-run-button'; // Style with CSS
                     fakeRunButton.textContent = 'Run (Lag Fix)'; // Indicate it's the fake one
                     fakeRunButton.type = 'button'; // Prevent default form submission if any
                     fakeRunButton.addEventListener('click', handleFakeRunButtonClick); // Use a dedicated handler

                     // Insert the fake button - try inserting it *after* the fake input's container div
                     // Adjust this based on where the original Run button visually is relative to the textarea
                     // Assuming the real input and button share a common parent wrapper:
                     const inputWrapper = realChatInput.closest('.prompt-input-wrapper-container') || realChatInput.parentNode; // Find a suitable parent
                     const buttonContainer = inputWrapper.querySelector('.button-wrapper:last-of-type'); // Find the original button's wrapper
                     if (buttonContainer) {
                         buttonContainer.parentNode.insertBefore(fakeRunButton, buttonContainer.nextSibling); // Insert after button wrapper
                         // Hide the original button's wrapper visually
                         buttonContainer.style.display = 'none';
                     } else {
                         // Fallback: Insert after the fake input if wrapper not found
                         fakeChatInput.parentNode.insertBefore(fakeRunButton, fakeChatInput.nextSibling);
                     }
                      console.log("AC Script: Fake Run button added.");
                 } catch (error) {
                     console.error("AC Script: Error creating fake run button:", error);
                     // Don't necessarily stop activation, input might still work manually
                 }
            }

        } else {
            // --- Deactivate Lag Fix ---
            console.log("AC Script: Deactivating Lag Fix Input.");
            const existingFakeInput = document.getElementById(FAKE_INPUT_ID);
            if (existingFakeInput) {
                existingFakeInput.remove();
            }
            fakeChatInput = null; // Reset state

            const existingFakeButton = document.getElementById(FAKE_RUN_BUTTON_ID);
            if (existingFakeButton) {
                existingFakeButton.remove();
            }

            // Restore real input
            if (realChatInput) {
                realChatInput.classList.remove('adv-controls-real-input-hidden');
            }

            // Restore original button wrapper's visibility if we hid it
             if(realRunButton){
                 const inputWrapper = realChatInput.closest('.prompt-input-wrapper-container') || realChatInput.parentNode;
                 const buttonContainer = inputWrapper.querySelector('.button-wrapper:has(run-button)'); // Find original button container
                 if (buttonContainer) buttonContainer.style.display = ''; // Restore display
             }
        }
    }
       // --- Handler for the FAKE Run Button ---
    function handleFakeRunButtonClick(event) {
        // --- Ensure we have references to the necessary elements ---
        // Re-query just in case, although they should be cached if lag fix is active
        const currentFakeInput = document.getElementById(FAKE_INPUT_ID);
        const currentRealInput = realChatInput || document.querySelector(CHAT_INPUT_SELECTOR);
        const currentRealRunButton = realRunButton || document.querySelector(RUN_BUTTON_SELECTOR);

        if (currentFakeInput && currentRealInput && currentRealRunButton) {
            console.log("AC Script: FAKE Run Button Clicked! Attempting submit.");

            // 1. Copy text from fake to real
            const textToSubmit = currentFakeInput.value;
            if (!textToSubmit.trim()) {
                console.log("AC Script: Fake input is empty, doing nothing.");
                return; // Don't submit if empty
            }
            currentRealInput.value = textToSubmit;
            console.log("AC Script: Copied text to real input.");

            // 2. Trigger events on real input to make the site aware
            try {
                currentRealInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
                currentRealInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
                console.log("AC Script: Dispatched input/change events on real input.");
            } catch (e) {
                console.error("AC Script: Error dispatching events on real input:", e);
                // Don't necessarily stop here, try clicking anyway? Maybe comment out return.
                // return; // Optional: Stop if events fail
            }

            // 3. Clear the fake input (do this AFTER potentially needing focus)
            // currentFakeInput.value = ''; // Let's clear it AFTER the click attempt

            // 4. Ensure the REAL input potentially has focus briefly before the click might need it
            // Although clicking the button should be sufficient usually
             currentRealInput.focus(); // Try focusing the real input briefly
             currentRealInput.blur(); // Then blur it, sometimes helps trigger validation

            // 5. Force Enable the REAL Run Button
            let wasDisabled = false;
            if (currentRealRunButton.disabled) {
                currentRealRunButton.disabled = false;
                wasDisabled = true;
                console.log("AC Script: Force removed 'disabled' attribute from Real Run button.");
            }
            // NOTE: Add class removal here if necessary, based on inspecting the disabled state

            // 6. Programmatically click the REAL Run Button
            // Use a timeout to allow potential UI updates after events/focus/enable
            setTimeout(() => {
                 console.log("AC Script: Programmatically clicking REAL Run button.");
                 if (currentRealRunButton.offsetParent === null) { // Check if button is actually visible
                      console.warn("AC Script: Real run button is not visible/in DOM just before click?");
                       if (wasDisabled) currentRealRunButton.disabled = true; // Re-disable if we couldn't click
                      return;
                 }

                 // --- THE ACTUAL CLICK ---
                 currentRealRunButton.click();
                 // --- END CLICK ---

                 console.log("AC Script: Click dispatched on real button.");

                 // Clear the fake input now
                 currentFakeInput.value = '';


                 // Optional: Re-disable immediately after clicking? Less critical now.
                 // if (wasDisabled) {
                 //     setTimeout(() => { currentRealRunButton.disabled = true; }, 10);
                 // }

            }, 150); // Slightly increased delay again to 150ms, just in case

        } else {
            console.warn("AC Script: Fake Run Button click failed - elements missing.",
                { currentFakeInput, currentRealInput, currentRealRunButton }); // Log which element might be missing
        }
    }

    // --- Mutation Observer (for Auto mode chat hiding) ---
    function handleChatMutation(mutationsList, observer) {
        // ... (Similar logic to v3.0, checking for added AI turns) ...
        if (settings.mode !== 'auto') return;
        let newAiTurnAdded = false;
        // ... (rest of mutation checking logic) ...

        if (newAiTurnAdded) {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                console.log("AC Script: Auto mode applying chat rules.");
                applyChatVisibilityRules();
                const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
                if(chatContainer) setTimeout(() => chatContainer.scrollTop = chatContainer.scrollHeight, 50);
            }, 300);
        }
    }

    function initChatObserver() {
        // ... (Similar logic to v3.0, starting/stopping based on settings.mode === 'auto') ...
        if (chatObserver) { chatObserver.disconnect(); chatObserver = null; }
        if (settings.mode === 'auto') {
            const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
            if (chatContainer) {
                chatObserver = new MutationObserver(handleChatMutation);
                chatObserver.observe(chatContainer, { childList: true, subtree: true });
                console.log("AC Script: Chat observer started for auto-hide.");
            } else { console.warn("AC Script: Could not find chat container for observer."); }
        } else { console.log("AC Script: Chat observer inactive."); }
    }

    // --- Initialization & Styles ---
    function addStyles() {
        // --- APPROXIMATE DARK THEME ---
        // These colors are guesses based on common dark themes.
        // Replace with exact values if you find them via Inspector.
        const darkBg = '#202124';// Main dark background
        const lighterDarkBg = '#303134'; // Slightly lighter background (e.g., popup header/footer)
        const lightText = '#e8eaed';// Main light text
        const mediumText = '#bdc1c6';// Secondary text (e.g., descriptions)
        const darkBorder = '#5f6368';// Borders
        const accentColor = '#8ab4f8';// Accent color (e.g., toggle ON state, buttons) - Google blueish
        const handleColor = '#e8eaed';// Toggle handle color
        const trackOffColor = '#5f6368';// Toggle track OFF color
        const inputBg = '#3c4043';// Input field background

        GM_addStyle(`
            /* --- General Popup Styling (Dark Theme Approx) --- */
            :root { /* Define CSS variables for easier reuse */
                --popup-bg: ${darkBg};
                --popup-header-bg: ${lighterDarkBg};
                --popup-footer-bg: ${lighterDarkBg};
                --popup-text-primary: ${lightText};
                --popup-text-secondary: ${mediumText};
                --popup-border: ${darkBorder};
                --input-bg: ${inputBg};
                --input-border: ${darkBorder};
                --input-text: ${lightText};
                --accent-color: ${accentColor};
                --toggle-handle-color: ${handleColor};
                --toggle-track-off-color: ${trackOffColor};
                --textarea-bg-color: ${inputBg}; /* For fake input */
                --textarea-text-color: ${lightText}; /* For fake input */
                --textarea-border-color: ${darkBorder}; /* For fake input */
            }
                        /* --- Hiding Real Input for Lag Fix --- */
            .adv-controls-real-input-hidden {
                visibility: hidden !important;
                position: absolute !important; /* Take out of flow */
                height: 1px !important;
                width: 1px !important;
                overflow: hidden !important;
                border: none !important;
                padding: 0 !important;
                margin: 0 !important;
                opacity: 0 !important;
             }

             /* --- Fake Input Basic Styling (Refined) --- */
             #${FAKE_INPUT_ID}.advanced-control-fake-input {
                 /* Styles copied dynamically, use CSS vars */
                 background-color: var(--textarea-bg-color);
                 color: var(--textarea-text-color);
                 border: 1px solid var(--textarea-border-color);
                 padding: 10px; /* Example padding, adjust if needed based on real input */
                 border-radius: 4px; /* Example radius */
                 font-family: inherit; /* Inherit from container */
                 font-size: inherit; /* Inherit from container */
                 line-height: 1.5; /* Example line-height */
                 display: block; /* Ensure block layout */
                 box-sizing: border-box;
                 margin: 0; /* Reset margin */
                 /* Width and height set dynamically via JS */
                 /* Ensure transitions don't interfere */
                 transition: none !important;
             }


            #${POPUP_ID} {
                display: none; position: absolute; z-index: 10001;
                background-color: var(--popup-bg);
                border: 1px solid var(--popup-border);
                border-radius: 8px;
                box-shadow: 0 4px 8px 3px rgba(0,0,0,0.3); /* Darker shadow */
                width: 340px; /* Slightly wider */
                font-family: "Google Sans", Roboto, Arial, sans-serif; /* Verify Font */
                font-size: 14px;
                color: var(--popup-text-primary);
                overflow: hidden;
            }
            #${POPUP_ID} .popup-header {
                display: flex; justify-content: space-between; align-items: center;
                padding: 12px 16px; border-bottom: 1px solid var(--popup-border);
                font-weight: 500; font-size: 16px;
                background-color: var(--popup-header-bg);
            }
            #${POPUP_ID} .close-popup-button {
                background: none; border: none; font-size: 24px; line-height: 1;
                cursor: pointer; color: var(--popup-text-secondary); padding: 0 4px; margin: -4px;
            }
            #${POPUP_ID} .close-popup-button:hover { color: var(--popup-text-primary); }
            #${POPUP_ID} .popup-content { padding: 16px; display: flex; flex-direction: column; gap: 16px; }
            #${POPUP_ID} .popup-section { border: none; padding: 0; margin: 0; }
            #${POPUP_ID} legend { font-weight: 500; padding-bottom: 8px; color: var(--popup-text-primary); border-bottom: 1px solid var(--popup-border); margin-bottom: 8px; }

            /* --- Vibe Button --- */
             #${POPUP_ID} .vibe-section { margin-bottom: 10px; border-bottom: 1px solid var(--popup-border); padding-bottom: 15px;}
             #${POPUP_ID} .vibe-button {
                 display: flex; align-items: center; justify-content: center; gap: 8px;
                 width: 100%; padding: 10px 16px; font-size: 15px; font-weight: 500;
                 border: 1px solid var(--popup-border); border-radius: 4px; cursor: pointer;
                 background-color: var(--popup-bg); color: var(--popup-text-primary);
                 transition: background-color 0.2s, border-color 0.2s;
             }
            #${POPUP_ID} .vibe-button:hover { background-color: ${lighterDarkBg}; border-color: var(--accent-color); }
            #${POPUP_ID} .vibe-button.active { background-color: var(--accent-color); color: ${darkBg}; border-color: var(--accent-color); }
            #${POPUP_ID} .vibe-button .material-symbols-outlined { font-size: 20px; }

            /* --- Settings Items --- */
            #${POPUP_ID} .popup-setting { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
            #${POPUP_ID} .popup-setting label { cursor: pointer; user-select: none; }
            #${POPUP_ID} input[type="radio"] { accent-color: var(--accent-color); cursor: pointer; width: 16px; height: 16px; margin: 0;}
            #${POPUP_ID} input[type="radio"]:disabled + label { color: var(--popup-text-secondary); cursor: not-allowed; }
             #${POPUP_ID} input:disabled { cursor: not-allowed; opacity: 0.6; }

            #${POPUP_ID} .number-setting label { white-space: nowrap; }
            #${POPUP_ID} input[type="number"] {
                width: 60px; padding: 6px 8px; border-radius: 4px; text-align: right;
                background-color: var(--input-bg); color: var(--input-text); border: 1px solid var(--input-border);
            }
            #${POPUP_ID} input[type="number"]:disabled { background-color: ${darkBg}; border-color: ${darkBorder}; opacity: 0.5; }
            #${POPUP_ID} #num-turns-description { color: var(--popup-text-secondary); font-size: 13px; }

            /* --- Basic Slide Toggle Styling (Approximate) --- */
            #${POPUP_ID} .toggle-setting { justify-content: space-between; } /* Push toggle right */
            #${POPUP_ID} .toggle-label { flex-grow: 1; } /* Allow label to take space */
            #${POPUP_ID} .basic-slide-toggle {
                appearance: none; -webkit-appearance: none; position: relative;
                width: 36px; height: 20px; border-radius: 10px;
                background-color: var(--toggle-track-off-color);
                cursor: pointer; transition: background-color 0.2s ease-in-out;
                display: inline-block; vertical-align: middle;
            }
            #${POPUP_ID} .basic-slide-toggle::before { /* The Handle */
                content: ''; position: absolute;
                width: 16px; height: 16px; border-radius: 50%;
                background-color: var(--toggle-handle-color);
                top: 2px; left: 2px;
                transition: transform 0.2s ease-in-out;
                box-shadow: 0 1px 3px rgba(0,0,0,0.4);
            }
            #${POPUP_ID} .basic-slide-toggle:checked {
                background-color: var(--accent-color);
            }
            #${POPUP_ID} .basic-slide-toggle:checked::before {
                transform: translateX(16px); /* Move handle right */
            }
            #${POPUP_ID} .basic-slide-toggle:disabled { opacity: 0.5; cursor: not-allowed; }
            #${POPUP_ID} .basic-slide-toggle:disabled::before { background-color: ${mediumText}; }


            /* --- Footer --- */
            #${POPUP_ID} .popup-footer {
                padding: 8px 16px; border-top: 1px solid var(--popup-border); font-size: 12px;
                color: var(--popup-text-secondary); text-align: center;
                background-color: var(--popup-footer-bg);
            }

            /* --- UI Hiding Classes --- */
            /* Apply these classes to OVERALL_LAYOUT_SELECTOR */
            .${LAYOUT_HIDE_CLASS}-sidebars ${LEFT_SIDEBAR_SELECTOR},
            .${LAYOUT_HIDE_CLASS}-sidebars ${RIGHT_SIDEBAR_SELECTOR} {
                display: none !important;
            }
            .${LAYOUT_HIDE_CLASS}-sysinstruct ${SYSTEM_INSTRUCTIONS_SELECTOR} {
                 display: none !important;
            }

             /* --- Fake Input Styling --- */
             #${FAKE_INPUT_ID} {
                 /* Styles copied in JS, use CSS vars */
                 background-color: var(--textarea-bg-color);
                 color: var(--textarea-text-color);
                 border: 1px solid var(--textarea-border-color);
                 display: block; /* Ensure it takes block layout */
                 /* Ensure transitions don't interfere if original had them */
                 transition: none !important;
             }
                         /* --- Fake Run Button Styling --- */
            #${FAKE_RUN_BUTTON_ID}.advanced-control-fake-run-button {
                /* Style similarly to the real run button */
                background-color: var(--accent-color); /* Use accent color */
                color: var(--popup-bg); /* Dark text on light button */
                border: none;
                border-radius: 4px; /* Match real button radius */
                padding: 8px 16px; /* Adjust padding */
                margin-left: 8px; /* Space from input */
                font-size: 14px; /* Match real button */
                font-weight: 500; /* Match real button */
                cursor: pointer;
                transition: background-color 0.2s;
            }
            #${FAKE_RUN_BUTTON_ID}.advanced-control-fake-run-button:hover {
                opacity: 0.9; /* Simple hover effect */
            }

        `);
    }

    // Utility to wait for an element
    function waitForElement(selector, callback, checkFrequency = 300, timeout = 15000) {
        // ... (same as before) ...
        const startTime = Date.now();
        const interval = setInterval(() => {
            const element = document.querySelector(selector);
            if (element) {
                clearInterval(interval); callback(element);
            } else if (Date.now() - startTime > timeout) {
                console.error(`AC Script: Timeout waiting for element: ${selector}`); clearInterval(interval); callback(null); // Indicate failure
            }
        }, checkFrequency);
        return interval;
    }

    // --- Main Initialization Sequence ---
    async function initialize() {
        console.log("AC Script: Initializing Advanced Control Suite v4.0...");
        addStyles();
        await loadSettings(); // Load settings first

        // Use Promise.allSettled to wait for multiple elements, some might timeout
        Promise.allSettled([
            new Promise((resolve, reject) => waitForElement(BUTTON_CONTAINER_SELECTOR, resolve, 150, 10000)),
            new Promise((resolve, reject) => waitForElement(CHAT_CONTAINER_SELECTOR, resolve, 300, 15000)),
            new Promise((resolve, reject) => waitForElement(OVERALL_LAYOUT_SELECTOR, resolve, 300, 15000)) // Wait for layout container too
        ]).then(results => {
            const buttonContainerResult = results[0];
            const chatContainerResult = results[1];
            const layoutContainerResult = results[2];

            if (buttonContainerResult.status === 'fulfilled' && buttonContainerResult.value) {
                console.log("AC Script: Button container found.");
                createScriptToggleButton(); // Create the main button
            } else {
                console.error("AC Script: Button container not found. UI button cannot be added.");
            }

            if (chatContainerResult.status === 'fulfilled' && chatContainerResult.value) {
                console.log("AC Script: Chat container found.");
                // Apply initial chat rules
                applyChatVisibilityRules();
                // Initialize the chat observer based on loaded settings
                initChatObserver();
            } else {
                console.warn("AC Script: Chat container not found. History features may fail.");
            }

            if (layoutContainerResult.status === 'fulfilled' && layoutContainerResult.value) {
                console.log("AC Script: Layout container found.");
                // Apply initial layout rules (hiding UI elements, activating lag fix if needed)
                applyLayoutRules();
            } else {
                console.warn(`AC Script: Layout container (${OVERALL_LAYOUT_SELECTOR}) not found. UI hiding features may fail.`);
            }

            console.log("AC Script: Initial setup attempted.");

            // Pre-cache input/run button for lag fix if setting is initially true
            if(settings.useLagFixInput || settings.mode === 'vibe'){
                waitForElement(CHAT_INPUT_SELECTOR, el => { if(el) realChatInput = el; console.log("AC Script: Cached real chat input."); }, 500, 10000);
                waitForElement(RUN_BUTTON_SELECTOR, el => { if(el) realRunButton = el; console.log("AC Script: Cached real run button."); }, 500, 10000);
            }

        });

        // Register menu command regardless
        GM_registerMenuCommand('Adv. Control Settings (AI Studio)', togglePopup);
    }

    // --- Start Execution ---
    // Wait for window load to maximize chance of elements being ready
    window.addEventListener('load', initialize);

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址