Google AI Studio 增强

提供护眼样式、元素显隐控制和自动折叠右侧面板功能,优化 AI Studio 使用体验。

目前为 2025-04-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         Google AI Studio Enhancer
// @name:zh-CN   Google AI Studio 增强
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Eye-Friendly Styles, Element Control & Auto Collapse Right Panel.
// @description:zh-CN 提供护眼样式、元素显隐控制和自动折叠右侧面板功能,优化 AI Studio 使用体验。
// @author       Claude 3.5 Sonnet & Gemini 2.0 Flash Thinking Experimental 01-21 & Gemini 2.5 Pro Preview 03-25
// @match        https://aistudio.google.com/prompts/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[AI Studio Enhancer+] Initializing...');

    // --- Default Settings ---
    const defaultSettings = {
        useCustomStyles: true,
        showUserPrompts: true,
        showThinkingProcess: true,
        showAIMessages: true, // Renamed from showSystemInstructions
        showInputBox: true,
        autoCollapseRightPanel: false // New setting
    };

    // --- Initialize Settings ---
    // Ensure default settings are saved on first run or if new settings are added
    let settings = {};
    for (const key in defaultSettings) {
        settings[key] = GM_getValue(key, defaultSettings[key]);
        // If a setting was newly added and doesn't exist in storage, save the default
        if (GM_getValue(key) === undefined) {
             GM_setValue(key, defaultSettings[key]);
             console.log(`[AI Studio Enhancer+] Initialized new setting: ${key} = ${defaultSettings[key]}`);
        }
    }
    console.log('[AI Studio Enhancer+] Current Settings:', settings);


    // --- Menu Definition ---
    var menu_ALL = [
        [
            "useCustomStyles",
            "Custom Styles",
        ],
        [
            "autoCollapseRightPanel", // New menu item
            "Auto Collapse Right Panel",
        ],
        [
            "showUserPrompts",
            "User Messages Display",
        ],
        [
            "showThinkingProcess",
            "Thinking Process Display",
        ],
        [
            "showAIMessages", // Renamed from showSystemInstructions
            "AI Messages Display", // Renamed label
        ],
        [
            "showInputBox",
            "Input Box Display",
        ],
        [
            "toggleAllDisplays", // Special key for toggle all visibility settings
            "Toggle All Displays",
        ],
    ];
    var menu_ID = []; // Array to store menu command IDs for unregistering

    // --- CSS Styles ---
    const customStyles = `
        /* Base eye-friendly styles */
        .chunk-editor-main { background: #e6e5e0 !important; font-size: 2em !important; }
        .chunk-editor-main p { font-family: "Times New Roman", "思源宋体", "思源宋体 CN" !important; }
        .user-prompt-container .text-chunk { background: #d6d5b7 !important; }
        .model-prompt-container { background: #f3f2ee !important; padding: 15px !important; border-radius: 16px !important; }
        .model-prompt-container:has(.mat-accordion) { background: none !important; } /* Don't style thinking process background */
        .turn-footer { font-size: 10px !important; background: none !important; }
        .user-prompt-container p { font-size: 15px !important; line-height: 1.3 !important; }
        .model-prompt-container p { font-size: 20px !important; line-height: 2 !important; }
        .mat-accordion p { font-size: 15px !important; } /* Style for thinking process content */

        /* UI Element Tweaks */
        .page-header { height: 50px !important; }
        .top-nav { font-size: .1em !important; }
        .token-count-container { position: fixed !important; top: 16px !important; }
        .toolbar-container { height: 40px !important; padding: 0 !important; background: none !important; }
        .token-count-content { padding: 0 !important; font-size: .4em !important; }
        .prompt-input-wrapper { padding: 5px 10px !important; }
        .prompt-input-wrapper-container { padding: 0 !important; font-size: .5em !important; }
        .prompt-chip-button { background: #eee !important; }
        .prompt-chip-button:hover { background: #dadada !important; }
    `;

    const hideUserPromptsStyle = `
        .chat-turn-container.user { display: none !important; }
    `;

    const hideThinkingProcessStyle = `
        .chat-turn-container .thought-container {display: none !important;}
    `;

    // Renamed variable for clarity, CSS selector remains the same
    const hideAIMessagesStyle = `
        .chat-turn-container.model { display: none !important; }
    `;

    const hideInputBoxStyle = `
        footer:has(.prompt-input-wrapper) { display: none !important; }
    `;

    // --- Style Application Function ---
    function updateStyles() {
        // Remove existing style elements managed by this script
        const existingStyles = document.querySelectorAll('style[data-enhancer-style]');
        existingStyles.forEach(style => style.remove());

        // Apply custom styles if enabled
        if (settings.useCustomStyles) {
            const styleElement = document.createElement('style');
            styleElement.setAttribute('data-enhancer-style', 'base');
            styleElement.textContent = customStyles;
            document.head.appendChild(styleElement);
        }

        // Apply user prompts visibility style if hidden
        if (!settings.showUserPrompts) {
            const hideUserStyle = document.createElement('style');
            hideUserStyle.setAttribute('data-enhancer-style', 'user-visibility');
            hideUserStyle.textContent = hideUserPromptsStyle;
            document.head.appendChild(hideUserStyle);
        }

        // Apply thinking process visibility style if hidden
        if (!settings.showThinkingProcess) {
            const hideThinkingStyle = document.createElement('style');
            hideThinkingStyle.setAttribute('data-enhancer-style', 'thinking-visibility');
            hideThinkingStyle.textContent = hideThinkingProcessStyle;
            document.head.appendChild(hideThinkingStyle);
        }

        // Apply AI messages visibility style if hidden
        if (!settings.showAIMessages) { // Uses the renamed setting
            const hideAIStyle = document.createElement('style');
            hideAIStyle.setAttribute('data-enhancer-style', 'ai-message-visibility');
            hideAIStyle.textContent = hideAIMessagesStyle; // Uses the appropriately named variable
            document.head.appendChild(hideAIStyle);
        }

        // Apply input box visibility style if hidden
        if (!settings.showInputBox) {
            const hideInputBox = document.createElement('style');
            hideInputBox.setAttribute('data-enhancer-style', 'input-box-visibility');
            hideInputBox.textContent = hideInputBoxStyle;
            document.head.appendChild(hideInputBox);
        }
        console.log('[AI Studio Enhancer+] Styles updated based on settings.');
    }

    // --- Floating Notification Function ---
    function showNotification(message) {
        const notificationId = 'enhancer-notification';
        let notification = document.getElementById(notificationId);
        if (!notification) {
            notification = document.createElement('div');
            notification.id = notificationId;
            notification.style.cssText = `
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background-color: rgba(0, 0, 0, 0.75);
                color: white;
                padding: 10px 20px;
                border-radius: 5px;
                z-index: 10000; /* Ensure high z-index */
                opacity: 0;
                transition: opacity 0.5s ease-in-out;
                font-size: 14px; /* Readable size */
            `;
            document.body.appendChild(notification);
        }

        // Clear existing timeout if notification is shown again quickly
        if (notification.timeoutId) {
            clearTimeout(notification.timeoutId);
        }

        notification.textContent = message;
        notification.style.opacity = '1'; // Fade in

        // Set timeout to fade out and remove
        notification.timeoutId = setTimeout(() => {
            notification.style.opacity = '0';
            setTimeout(() => {
                if (notification.parentNode) {
                    notification.parentNode.removeChild(notification);
                }
                notification.timeoutId = null; // Clear the stored timeout ID
            }, 500); // Wait for fade out transition (0.5s)
        }, 1500); // Show for 1.5 seconds before starting fade out
    }


    // --- Menu Command Functions ---
    function registerMenuCommands() {
        // Unregister previous commands to prevent duplicates and update text
        menu_ID.forEach(id => GM_unregisterMenuCommand(id));
        menu_ID = []; // Clear the array

        console.log("[AI Studio Enhancer+] Registering menu commands...");
        menu_ALL.forEach(item => {
            const settingKey = item[0];
            const baseMenuText = item[1];

            if (settingKey === "toggleAllDisplays") {
                // Handle "Toggle All Displays" menu text specifically
                const displaySettingsKeys = ["showUserPrompts", "showThinkingProcess", "showAIMessages", "showInputBox"];
                // Check if *all* relevant displays are currently enabled
                const allEnabled = displaySettingsKeys.every(key => settings[key]);
                const menuText = `${allEnabled ? "🔴 Hide All Displays" : "🟢 Show All Displays"}`;
                menu_ID.push(GM_registerMenuCommand(
                    menuText,
                    toggleAllDisplays // Call the toggle function
                ));
            } else {
                // Handle regular settings toggles
                const currentSettingValue = settings[settingKey];
                const menuText = `${currentSettingValue ? "🔴 Disable" : "🟢 Enable"} ${baseMenuText}`; // Dynamic menu text
                menu_ID.push(GM_registerMenuCommand(
                    menuText,
                    () => menuSwitch(settingKey) // Call menuSwitch with the key
                ));
            }
        });
         console.log("[AI Studio Enhancer+] Menu commands registered.");
    }

    // Toggle a single setting via menu
    function menuSwitch(settingKey) {
        let newValue = !settings[settingKey]; // Toggle the boolean value
        settings[settingKey] = newValue;
        GM_setValue(settingKey, newValue);
        console.log(`[AI Studio Enhancer+] Toggled ${settingKey} to ${newValue}`);

        // Find the base text for the notification message
        const baseMenuText = menu_ALL.find(item => item[0] === settingKey)[1];

        // Apply style changes *immediately* for visual feedback
        // Except for autoCollapseRightPanel, which doesn't directly change styles
        if (settingKey !== 'autoCollapseRightPanel') {
             updateStyles();
        } else {
             // If auto-collapse was just enabled, maybe trigger an initial check/click?
             if (newValue) {
                  console.log('[AI Studio Enhancer+] Auto-collapse enabled, attempting initial check/click.');
                  // Use a small delay to ensure UI is ready
                  setTimeout(triggerAutoCollapseIfNeeded, 500);
             }
        }


        registerMenuCommands(); // Re-register menus to update text and emoji
        showNotification(`${baseMenuText} ${newValue ? 'Enabled' : 'Disabled'}`); // Show confirmation
    }

    // Toggle all display-related settings
    function toggleAllDisplays() {
        const displaySettingsKeys = ["showUserPrompts", "showThinkingProcess", "showAIMessages", "showInputBox"];
        // Determine the new state - if *all* are currently enabled, disable all, otherwise enable all
        const enableAll = !displaySettingsKeys.every(key => settings[key]);

        console.log(`[AI Studio Enhancer+] Toggling all displays to: ${enableAll}`);
        displaySettingsKeys.forEach(key => {
            settings[key] = enableAll;
            GM_setValue(key, enableAll);
        });

        updateStyles(); // Apply combined style changes
        registerMenuCommands(); // Update menus to reflect new states
        showNotification(`All Displays ${enableAll ? 'Enabled' : 'Disabled'}`);
    }


    // --- Auto Collapse Right Panel Logic ---

    const RUN_SETTINGS_BUTTON_SELECTOR = '.toggles-container button[aria-label="Run settings"]';
    const RIGHT_PANEL_TAG_NAME = 'MS-RIGHT-SIDE-PANEL'; // HTML tag names are usually uppercase in querySelector/tagName checks
    const NGTNS_REGEX = /ng-tns-c\d+-\d+/; // Regex to match ng-tns-c...-... class pattern

    let lastNgTnsClass = null; // Store the last seen ng-tns class of the panel
    let clickDebounceTimeout = null; // Timeout ID for debouncing clicks
    let panelObserver = null; // Reference to the MutationObserver

    // Function to safely click the "Run settings" button if needed
    function clickRunSettingsButton() {
        // Clear any pending debounce timeout
        if (clickDebounceTimeout) {
            clearTimeout(clickDebounceTimeout);
            clickDebounceTimeout = null;
        }

        // Only proceed if the setting is enabled
        if (!settings.autoCollapseRightPanel) {
            // console.log('[AI Studio Enhancer+] Auto-collapse is disabled, skipping click.');
            return;
        }

        const button = document.querySelector(RUN_SETTINGS_BUTTON_SELECTOR);
        if (button) {
            // Optional: Check if the button is actually visible and clickable
            const style = window.getComputedStyle(button);
            const panel = button.closest(RIGHT_PANEL_TAG_NAME); // Check if the button is inside an *expanded* panel

            // Heuristic: If the panel exists and the button is visible, assume it needs clicking (i.e., panel is open)
            // A more precise check might involve looking at panel's specific classes or styles indicating expansion state,
            // but clicking an already "collapsed" state button usually has no effect.
            if (panel && style.display !== 'none' && style.visibility !== 'hidden' && !button.disabled) {
                console.log('[AI Studio Enhancer+] Auto-collapsing: Clicking "Run settings" button.');
                button.click();
            } else {
                 // console.log('[AI Studio Enhancer+] Auto-collapse: "Run settings" button found but not deemed clickable (possibly already collapsed or hidden).');
            }
        } else {
            // console.log('[AI Studio Enhancer+] Auto-collapse: "Run settings" button not found.');
        }
    }

    // Helper to get the ng-tns class from an element
    function getNgTnsClass(element) {
        if (!element || !element.classList) return null;
        for (const className of element.classList) {
            if (NGTNS_REGEX.test(className)) {
                return className;
            }
        }
        return null;
    }

     // Function to trigger the collapse check/action, typically called after a delay or event
    function triggerAutoCollapseIfNeeded() {
        if (!settings.autoCollapseRightPanel) return; // Exit if feature is disabled

        console.log('[AI Studio Enhancer+] Checking if auto-collapse is needed...');
        const panel = document.querySelector(RIGHT_PANEL_TAG_NAME);
        if (panel) {
            const currentNgTnsClass = getNgTnsClass(panel);
            // console.log(`[AI Studio Enhancer+] Panel found. Current ngTns: ${currentNgTnsClass}, Last known: ${lastNgTnsClass}`);

            // If this is the first time seeing the panel or the class is different,
            // assume it might have just opened or changed state, requiring a collapse.
            if (!lastNgTnsClass || currentNgTnsClass !== lastNgTnsClass) {
                console.log(`[AI Studio Enhancer+] Panel state potentially changed (or first load). Triggering click.`);
                lastNgTnsClass = currentNgTnsClass; // Update the last known class
                // Use debounce here as well
                if (clickDebounceTimeout) clearTimeout(clickDebounceTimeout);
                clickDebounceTimeout = setTimeout(clickRunSettingsButton, 300); // Debounce click
            } else {
                 // console.log(`[AI Studio Enhancer+] Panel ngTns class unchanged (${currentNgTnsClass}). No automatic click triggered.`);
            }
        } else {
             console.log('[AI Studio Enhancer+] Right side panel not found during check.');
             lastNgTnsClass = null; // Reset if panel disappears
        }
    }


    // --- Mutation Observer for Panel Changes ---
    const panelObserverCallback = function(mutationsList, observer) {
        if (!settings.autoCollapseRightPanel) return; // Don't observe if feature is off

        let panelPotentiallyChanged = false;

        for (const mutation of mutationsList) {
            // A) Panel's class attribute changed
            if (mutation.type === 'attributes' &&
                mutation.attributeName === 'class' &&
                mutation.target.tagName === RIGHT_PANEL_TAG_NAME)
            {
                const targetPanel = mutation.target;
                const currentNgTnsClass = getNgTnsClass(targetPanel);
                // console.log(`[AI Studio Enhancer+] Panel Observer: Detected class change on ${RIGHT_PANEL_TAG_NAME}. Old: ${lastNgTnsClass}, New: ${currentNgTnsClass}`);

                if (currentNgTnsClass !== lastNgTnsClass) {
                    console.log(`[AI Studio Enhancer+] Panel Observer: NgTns class changed! (${lastNgTnsClass} -> ${currentNgTnsClass})`);
                    lastNgTnsClass = currentNgTnsClass; // Update record
                    panelPotentiallyChanged = true;
                    break; // Found relevant change
                }
            }
            // B) Nodes added, check if the panel itself or its container was added
            else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                 for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                         let potentialPanel = null;
                         if (node.tagName === RIGHT_PANEL_TAG_NAME) {
                             potentialPanel = node;
                         } else if (node.querySelector) { // Check descendants
                             potentialPanel = node.querySelector(RIGHT_PANEL_TAG_NAME);
                         }

                         if (potentialPanel) {
                            const currentNgTnsClass = getNgTnsClass(potentialPanel);
                            console.log(`[AI Studio Enhancer+] Panel Observer: Detected addition of ${RIGHT_PANEL_TAG_NAME} or container. NgTns: ${currentNgTnsClass}`);
                             if (currentNgTnsClass !== lastNgTnsClass) {
                                console.log(`[AI Studio Enhancer+] Panel Observer: Added panel has different NgTns class! (${lastNgTnsClass} -> ${currentNgTnsClass})`);
                                lastNgTnsClass = currentNgTnsClass; // Update record
                                panelPotentiallyChanged = true;
                             } else if (!lastNgTnsClass && currentNgTnsClass) {
                                 // First time seeing the panel via addition
                                 console.log(`[AI Studio Enhancer+] Panel Observer: Initial NgTns class detected via addition: ${currentNgTnsClass}`);
                                 lastNgTnsClass = currentNgTnsClass;
                                 panelPotentiallyChanged = true; // Trigger click on first appearance too
                             }
                             // Found the panel, no need to check other added nodes in this mutation record
                             if(panelPotentiallyChanged) break;
                         }
                    }
                 }
                 // If found relevant change in added nodes, break outer loop
                 if (panelPotentiallyChanged) break;
            }
        }

        // If a relevant change was detected, schedule a debounced click
        if (panelPotentiallyChanged) {
            console.log('[AI Studio Enhancer+] Panel change detected, scheduling debounced auto-collapse click.');
            if (clickDebounceTimeout) clearTimeout(clickDebounceTimeout);
            // Delay slightly to allow UI to settle after mutation
            clickDebounceTimeout = setTimeout(clickRunSettingsButton, 300); // 300ms debounce/delay
        }
    };

    // --- Initialize Panel Observer ---
    function initializePanelObserver() {
        if (panelObserver) panelObserver.disconnect(); // Disconnect previous if any

        const observerConfig = {
            attributes: true,        // Listen for attribute changes
            attributeFilter: ['class'], // Specifically watch the 'class' attribute
            childList: true,         // Listen for nodes being added or removed
            subtree: true            // Observe the entire subtree from the target
        };

        panelObserver = new MutationObserver(panelObserverCallback);

        // Observe the body, as the panel might be added anywhere dynamically
        panelObserver.observe(document.body, observerConfig);
        console.log('[AI Studio Enhancer+] Panel MutationObserver started on document body.');
    }

    // --- Script Initialization ---
    function initializeScript() {
        updateStyles(); // Apply initial styles
        registerMenuCommands(); // Setup Tampermonkey menu
        initializePanelObserver(); // Start watching for panel changes

        // Perform initial check/click for auto-collapse after a delay (allow page load)
        setTimeout(triggerAutoCollapseIfNeeded, 1500); // Wait 1.5 seconds after script runs
    }

    // --- Start the script ---
    // Use window.onload or a similar mechanism if document.body isn't ready immediately,
    // but Tampermonkey's default run-at usually handles this. Let's ensure body exists.
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeScript);
    } else {
        // DOMContentLoaded has already fired
        initializeScript();
    }

    // Optional: Cleanup observer on page unload
    window.addEventListener('unload', () => {
        if (panelObserver) {
            panelObserver.disconnect();
            console.log('[AI Studio Enhancer+] Panel MutationObserver disconnected.');
        }
        if (clickDebounceTimeout) {
            clearTimeout(clickDebounceTimeout);
        }
    });

})();

QingJ © 2025

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