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