// ==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);
}
});
})();