Greasy Fork

Greasy Fork is available in English.

AI Studio Enhancer

Eye-Friendly Styles, Element Control & Auto Collapse: Enhances Google AI Studio with eye-friendly styles, toggles visibility for user prompts, thinking process, AI messages, and input box. Includes menu controls and an option to auto-collapse the right panel.

当前为 2025-04-12 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AI Studio Enhancer
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Eye-Friendly Styles, Element Control & Auto Collapse: Enhances Google AI Studio with eye-friendly styles, toggles visibility for user prompts, thinking process, AI messages, and input box. Includes menu controls and an option to auto-collapse the right panel.
// @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/*
// @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);
        }
    });

})();