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

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

  1. // ==UserScript==
  2. // @name AI Studio - Advanced Control Suite (History, UI, Lag Fix)
  3. // @namespace http://tampermonkey.net/
  4. // @version 4.0
  5. // @description Advanced control for Google AI Studio: Chat history modes (Exchanges, Vibe), UI Hiding (Sidebars, System Instr.), Input Lag Fix, Dark Theme Popup.
  6. // @author so it goes...again & Gemini
  7. // @match https://aistudio.google.com/*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_registerMenuCommand
  12. // @run-at document-idle
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // --- Configuration ---
  20.  
  21. // --- !!! CRITICAL SELECTORS - VERIFY THESE CAREFULLY !!! ---
  22. const LEFT_SIDEBAR_SELECTOR = 'ms-navbar';// Confirmed from previous snippet
  23. const RIGHT_SIDEBAR_SELECTOR = 'ms-run-settings';/* !!! VERIFY THIS SELECTOR !!! */ // Verify when OPEN
  24. const SYSTEM_INSTRUCTIONS_SELECTOR = 'ms-system-instructions';// Assumed Correct - Verify if possible
  25. const CHAT_INPUT_SELECTOR = 'textarea[aria-label="Type something"]'; // <<< CONFIRMED from snippet
  26. const RUN_BUTTON_SELECTOR = 'button.run-button[aria-label="Run"]';// <<< CONFIRMED from snippet
  27. const OVERALL_LAYOUT_SELECTOR = 'body > app-root > ms-app > div';// <<< Best guess, update if needed
  28. const CHAT_CONTAINER_SELECTOR = 'ms-autoscroll-container';// Stable
  29. const USER_TURN_SELECTOR = 'ms-chat-turn:has([data-turn-role="User"])'; // Stable
  30. const AI_TURN_SELECTOR = 'ms-chat-turn:has([data-turn-role="Model"])'; // Stable
  31. const BUTTON_CONTAINER_SELECTOR = 'div.right-side';// Stable
  32. // --- END CRITICAL SELECTORS ---
  33.  
  34. const SCRIPT_BUTTON_ID = 'advanced-control-toggle-button';
  35. const POPUP_ID = 'advanced-control-popup';
  36. const FAKE_INPUT_ID = 'advanced-control-fake-input';
  37. const FAKE_RUN_BUTTON_ID = 'advanced-control-fake-run-button';
  38. const LAYOUT_HIDE_CLASS = 'adv-controls-hide-ui'; // Class added to OVERALL_LAYOUT_SELECTOR
  39.  
  40. // Settings Keys
  41. const SETTINGS_KEY = 'aiStudioAdvancedControlSettings_v4'; // New key for this version
  42.  
  43. // Default Settings
  44. const DEFAULT_SETTINGS = {
  45. mode: 'manual',// 'off' | 'manual' | 'auto' | 'vibe'
  46. numTurnsToShow: 2,// Number of exchanges (Manual/Auto) or AI turns (unused in Vibe v4)
  47. hideSidebars: false,// User preference for hiding sidebars
  48. hideSystemInstructions: false, // User preference for hiding sys instructions
  49. useLagFixInput: false,// User preference for the input lag fix
  50. };
  51.  
  52. // --- State ---
  53. let settings = { ...DEFAULT_SETTINGS };
  54. let isCurrentlyHidden = false; // Chat history hidden state
  55. let scriptToggleButton = null;
  56. let popupElement = null;
  57. let chatObserver = null;
  58. let debounceTimer = null;
  59. let realChatInput = null; // Cache the real input element for lag fix
  60. let realRunButton = null; // Cache the real run button for lag fix
  61. let fakeChatInput = null; // Cache the fake input element
  62.  
  63. // --- Icons ---
  64. const ICON_VISIBLE = 'visibility';
  65. const ICON_HIDDEN = 'visibility_off';
  66. const ICON_VIBE = 'neurology'; // Or choose another icon for Vibe button
  67.  
  68. // --- Core Logic: Chat History Hiding ---
  69. function applyChatVisibilityRules() {
  70. console.log("AC Script: Applying chat visibility. Mode:", settings.mode, "Num:", settings.numTurnsToShow);
  71. const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
  72. if (!chatContainer) {
  73. console.warn("AC Script: Chat container not found for visibility rules.");
  74. return;
  75. }
  76.  
  77. const allUserTurns = Array.from(chatContainer.querySelectorAll(USER_TURN_SELECTOR));
  78. const allAiTurns = Array.from(chatContainer.querySelectorAll(AI_TURN_SELECTOR));
  79. // Query all turns together for simplicity in show/hide all scenarios and iteration
  80. const allTurns = Array.from(chatContainer.querySelectorAll(`${USER_TURN_SELECTOR}, ${AI_TURN_SELECTOR}`));
  81.  
  82. let turnsToShow = [];
  83. let localDidHideSomething = false;
  84.  
  85. // Helper to set display style idempotently
  86. const setDisplay = (element, visible) => {
  87. const targetDisplay = visible ? '' : 'none';
  88. if (element.style.display !== targetDisplay) {
  89. element.style.display = targetDisplay;
  90. }
  91. };
  92.  
  93. switch (settings.mode) {
  94. case 'off':
  95. // --- Show All ---
  96. allTurns.forEach(turn => setDisplay(turn, true));
  97. localDidHideSomething = false;
  98. break; // End of 'off' case
  99.  
  100. case 'vibe':
  101. // --- VIBE Mode: Show only the very last AI turn, hide all user turns ---
  102. allUserTurns.forEach(turn => {
  103. setDisplay(turn, false);
  104. });
  105. // If any user turns exist, we definitely hid something (or tried to)
  106. if (allUserTurns.length > 0) localDidHideSomething = true;
  107.  
  108. // Now handle AI turns
  109. if (allAiTurns.length > 0) {
  110. const lastAiTurn = allAiTurns[allAiTurns.length - 1];
  111. allAiTurns.forEach(turn => {
  112. const shouldBeVisible = (turn === lastAiTurn);
  113. setDisplay(turn, shouldBeVisible);
  114. // If we hide any AI turn (i.e., not the last one), mark as hidden
  115. if (!shouldBeVisible) localDidHideSomething = true;
  116. });
  117. }
  118. // No 'else' needed - if no AI turns, nothing to show.
  119.  
  120. break; // End of 'vibe' case
  121.  
  122. case 'manual':
  123. case 'auto':{
  124. // --- Manual/Auto Mode: Show last N *exchanges* (User+AI pairs) ---
  125. const numExchangesToShow = settings.numTurnsToShow;
  126.  
  127. if (numExchangesToShow <= 0) { // Show all if 0 or less
  128. allTurns.forEach(turn => setDisplay(turn, true));
  129. localDidHideSomething = false;
  130. } else {
  131. let exchangesFound = 0;
  132. turnsToShow = []; // Stores the elements that should be visible
  133. // Iterate backwards through all turns to find pairs/exchanges
  134. for (let i = allTurns.length - 1; i >= 0; i--) {
  135. const currentTurn = allTurns[i];
  136.  
  137. if (currentTurn.matches(AI_TURN_SELECTOR)) {
  138. // Found an AI turn
  139. exchangesFound++; // Count this as (part of) an exchange
  140. turnsToShow.unshift(currentTurn); // Definitely show the AI turn
  141.  
  142. // Look for the User turn immediately before it
  143. if (i > 0 && allTurns[i - 1].matches(USER_TURN_SELECTOR)) {
  144. turnsToShow.unshift(allTurns[i - 1]); // Show the preceding User turn
  145. i--; // Decrement i again to skip this User turn in the next iteration
  146. }
  147. // If no preceding user turn, it's an "orphan" AI start, still counts as 1 exchange
  148.  
  149. } else if (currentTurn.matches(USER_TURN_SELECTOR)) {
  150. // Found a User turn without a following AI (maybe the very last prompt)
  151. exchangesFound++; // Count this incomplete exchange
  152. turnsToShow.unshift(currentTurn); // Show this User turn
  153. }
  154.  
  155. // Stop if we have found enough exchanges
  156. if (exchangesFound >= numExchangesToShow) {
  157. break;
  158. }
  159. } // End backwards loop
  160.  
  161. // Now apply visibility based on the collected turnsToShow list
  162. allTurns.forEach(turn => {
  163. const shouldBeVisible = turnsToShow.includes(turn);
  164. setDisplay(turn, shouldBeVisible);
  165. if (!shouldBeVisible) localDidHideSomething = true; // If any turn is hidden
  166. });
  167. }
  168. break; // End of 'manual'/'auto' case
  169. }
  170. } // End switch
  171.  
  172. // --- Update button icon state ---
  173. if (isCurrentlyHidden !== localDidHideSomething) {
  174. isCurrentlyHidden = localDidHideSomething;
  175. updateScriptToggleButtonAppearance(); // Assumes this function exists elsewhere
  176. console.log(`AC Script: Chat visibility updated. Currently hidden: ${isCurrentlyHidden}`);
  177. }
  178. } // End applyChatVisibilityRules
  179. // --- Core Logic: UI Element Hiding ---
  180. function applyLayoutRules() {
  181. const layoutContainer = document.querySelector(OVERALL_LAYOUT_SELECTOR);
  182. if (!layoutContainer) {
  183. console.warn("AC Script: Overall layout container not found:", OVERALL_LAYOUT_SELECTOR);
  184. return;
  185. }
  186.  
  187. const forceHide = settings.mode === 'vibe'; // Vibe mode forces UI hidden
  188. const shouldHideSidebars = forceHide || settings.hideSidebars;
  189. const shouldHideSysInstructions = forceHide || settings.hideSystemInstructions;
  190. const shouldApplyLagFix = forceHide || settings.useLagFixInput;
  191.  
  192. // Toggle main class on layout container
  193. layoutContainer.classList.toggle(`${LAYOUT_HIDE_CLASS}-sidebars`, shouldHideSidebars);
  194. layoutContainer.classList.toggle(`${LAYOUT_HIDE_CLASS}-sysinstruct`, shouldHideSysInstructions);
  195.  
  196. // Activate/Deactivate Lag Fix Input
  197. toggleLagFixInput(shouldApplyLagFix);
  198.  
  199. console.log(`AC Script: Applied Layout Rules. Mode: ${settings.mode}, Hide Sidebars: ${shouldHideSidebars}, Hide SysInstruct: ${shouldHideSysInstructions}, LagFix: ${shouldApplyLagFix}`);
  200.  
  201. // Update UI state in popup if open
  202. if (popupElement?.style.display === 'block') {
  203. updatePopupUIState();
  204. }
  205. }
  206.  
  207.  
  208. // --- Settings Management ---
  209. async function loadSettings() {
  210. const storedSettings = await GM_getValue(SETTINGS_KEY, DEFAULT_SETTINGS);
  211. settings = { ...DEFAULT_SETTINGS, ...storedSettings };
  212. isCurrentlyHidden = false; // Reset runtime state
  213. console.log("AC Script: Settings loaded:", settings);
  214. }
  215.  
  216. async function saveSettings() {
  217. // Make sure to save all persistent settings
  218. const settingsToSave = {
  219. mode: settings.mode,
  220. numTurnsToShow: settings.numTurnsToShow,
  221. hideSidebars: settings.hideSidebars,
  222. hideSystemInstructions: settings.hideSystemInstructions,
  223. useLagFixInput: settings.useLagFixInput
  224. };
  225. await GM_setValue(SETTINGS_KEY, settingsToSave);
  226. console.log("AC Script: Settings saved:", settingsToSave);
  227. }
  228.  
  229. // Update setting, save, and apply relevant rules
  230. function updateSetting(key, value) {
  231. if (settings[key] === value) return; // No change
  232.  
  233. console.log(`AC Script: Setting ${key} changing to ${value}`);
  234. const previousMode = settings.mode;
  235. settings[key] = value;
  236.  
  237. let needsChatRules = false;
  238. let needsLayoutRules = false;
  239. let needsObserverReinit = false;
  240. let needsPopupClose = false;
  241.  
  242. if (key === 'mode') {
  243. needsChatRules = true;
  244. needsLayoutRules = true; // Mode change (esp. Vibe) affects layout
  245. needsObserverReinit = (value === 'auto' || previousMode === 'auto');
  246. needsPopupClose = true; // Close popup on mode change (radio or vibe button)
  247. } else if (key === 'numTurnsToShow') {
  248. if (settings.mode === 'manual' || settings.mode === 'auto') {
  249. needsChatRules = true; // Apply if relevant mode is active
  250. }
  251. } else if (key === 'hideSidebars' || key === 'hideSystemInstructions' || key === 'useLagFixInput') {
  252. needsLayoutRules = true; // These directly affect layout/input fix
  253. }
  254.  
  255. saveSettings(); // Save any change
  256.  
  257. if (needsLayoutRules) {
  258. applyLayoutRules(); // Apply layout changes (handles lag fix toggle too)
  259. }
  260. if (needsChatRules) {
  261. // Delay slightly after potential layout changes
  262. setTimeout(applyChatVisibilityRules, 50);
  263. }
  264. if (needsObserverReinit) {
  265. initChatObserver();
  266. }
  267.  
  268. // Update popup UI state if it's open, *before* closing it
  269. if (popupElement?.style.display === 'block') {
  270. updatePopupUIState();
  271. }
  272.  
  273. if (needsPopupClose) {
  274. hidePopup();
  275. }
  276. }
  277.  
  278. // --- UI Elements (Button & Popup) ---
  279. function updateScriptToggleButtonAppearance() {
  280. if (!scriptToggleButton) return;
  281. const iconSpan = scriptToggleButton.querySelector('.material-symbols-outlined');
  282. if (iconSpan) {
  283. iconSpan.textContent = isCurrentlyHidden ? ICON_HIDDEN : ICON_VISIBLE;
  284. }
  285. const tooltipText = isCurrentlyHidden ? 'Chat history hidden (Click for options)' : 'Chat history visible (Click for options)';
  286. scriptToggleButton.setAttribute('aria-label', tooltipText);
  287. scriptToggleButton.setAttribute('mattooltip', tooltipText); // Attempt to update tooltip
  288. // Update Greasemonkey menu command text
  289. GM_registerMenuCommand(isCurrentlyHidden ? 'Show All History (via settings)' : 'Hide History (via settings)', togglePopup);
  290. }
  291.  
  292. function createScriptToggleButton() {
  293. if (document.getElementById(SCRIPT_BUTTON_ID)) {
  294. scriptToggleButton = document.getElementById(SCRIPT_BUTTON_ID);
  295. updateScriptToggleButtonAppearance(); // Ensure icon is correct
  296. return;
  297. }
  298. const buttonContainer = document.querySelector(BUTTON_CONTAINER_SELECTOR);
  299. if (!buttonContainer) {
  300. console.error("AC Script: Could not find button container:", BUTTON_CONTAINER_SELECTOR); return;
  301. }
  302. console.log("AC Script: Creating settings button.");
  303. scriptToggleButton = document.createElement('button');
  304. scriptToggleButton.id = SCRIPT_BUTTON_ID;
  305. scriptToggleButton.className = 'mdc-icon-button mat-mdc-icon-button mat-unthemed mat-mdc-button-base gmat-mdc-button advanced-control-button';
  306. scriptToggleButton.style.marginLeft = '4px'; scriptToggleButton.style.marginRight = '4px';
  307. scriptToggleButton.style.order = '-1'; // Place first
  308.  
  309. // --- FIX for TrustedHTML: Build elements manually ---
  310. const spanRipple = document.createElement('span');
  311. spanRipple.className = 'mat-mdc-button-persistent-ripple mdc-icon-button__ripple';
  312. scriptToggleButton.appendChild(spanRipple);
  313.  
  314. const icon = document.createElement('span');
  315. icon.className = 'material-symbols-outlined notranslate';
  316. icon.setAttribute('aria-hidden', 'true');
  317. // Icon textContent (visibility/visibility_off) will be set by updateScriptToggleButtonAppearance
  318. scriptToggleButton.appendChild(icon);
  319.  
  320. const focusIndicator = document.createElement('span');
  321. focusIndicator.className = 'mat-focus-indicator';
  322. scriptToggleButton.appendChild(focusIndicator);
  323.  
  324. const touchTarget = document.createElement('span');
  325. touchTarget.className = 'mat-mdc-button-touch-target';
  326. scriptToggleButton.appendChild(touchTarget);
  327. // --- END FIX ---
  328.  
  329. scriptToggleButton.addEventListener('click', togglePopup);
  330. buttonContainer.insertBefore(scriptToggleButton, buttonContainer.firstChild);
  331. updateScriptToggleButtonAppearance(); // Set initial icon/tooltip
  332. console.log("AC Script: Settings button added into", BUTTON_CONTAINER_SELECTOR);
  333. }
  334. function createPopupHtml() {
  335. const popup = document.createElement('div');
  336. popup.id = POPUP_ID;
  337. popup.className = 'advanced-control-popup';
  338.  
  339. // --- Header ---
  340. const header = document.createElement('div');
  341. header.className = 'popup-header';
  342.  
  343. const headerSpan = document.createElement('span');
  344. headerSpan.textContent = 'Advanced Controls'; // Use textContent
  345. header.appendChild(headerSpan);
  346.  
  347. const closeButton = document.createElement('button');
  348. closeButton.type = 'button';
  349. closeButton.className = 'close-popup-button';
  350. closeButton.setAttribute('aria-label', 'Close settings');
  351. closeButton.textContent = '×'; // Use textContent
  352. closeButton.addEventListener('click', hidePopup);
  353. header.appendChild(closeButton);
  354.  
  355. popup.appendChild(header);
  356.  
  357. // --- Content Area ---
  358. const content = document.createElement('div');
  359. content.className = 'popup-content';
  360.  
  361. // --- Vibe Mode Button ---
  362. const vibeButtonContainer = document.createElement('div');
  363. vibeButtonContainer.className = 'popup-section vibe-section';
  364. const vibeButton = document.createElement('button');
  365. vibeButton.id = 'vibe-mode-button';
  366. vibeButton.className = 'vibe-button';
  367. // Build button content manually
  368. const vibeIconSpan = document.createElement('span');
  369. vibeIconSpan.className = 'material-symbols-outlined';
  370. vibeIconSpan.textContent = ICON_VIBE;
  371. vibeButton.appendChild(vibeIconSpan);
  372. vibeButton.appendChild(document.createTextNode(' Activate VIBE Mode')); // Add text node
  373. vibeButton.addEventListener('click', () => updateSetting('mode', 'vibe'));
  374. vibeButtonContainer.appendChild(vibeButton);
  375. content.appendChild(vibeButtonContainer);
  376.  
  377. // --- History Hiding Mode ---
  378. const historyGroup = document.createElement('fieldset');
  379. historyGroup.className = 'popup-section history-section';
  380. const historyLegend = document.createElement('legend');
  381. historyLegend.textContent = 'Chat History Mode:';
  382. historyGroup.appendChild(historyLegend);
  383.  
  384. const modes = ['off', 'manual', 'auto'];
  385. const modeLabels = { off: 'Off (Show All)', manual: 'Manual Hide', auto: 'Auto Hide' };
  386. modes.forEach(modeValue => {
  387. const div = document.createElement('div');
  388. div.className = 'popup-setting radio-setting';
  389.  
  390. const input = document.createElement('input');
  391. input.type = 'radio';
  392. input.name = 'history-mode-radio';
  393. input.id = `mode-${modeValue}-radio`;
  394. input.value = modeValue;
  395. input.addEventListener('change', (e) => {
  396. if (e.target.checked) updateSetting('mode', e.target.value);
  397. });
  398.  
  399. const label = document.createElement('label');
  400. label.htmlFor = `mode-${modeValue}-radio`;
  401. label.textContent = modeLabels[modeValue];
  402.  
  403. div.appendChild(input);
  404. div.appendChild(label);
  405. historyGroup.appendChild(div);
  406. });
  407. content.appendChild(historyGroup);
  408.  
  409. // --- Number of Exchanges ---
  410. const numTurnsSetting = document.createElement('div');
  411. numTurnsSetting.className = 'popup-setting number-setting';
  412.  
  413. const numLabel = document.createElement('label');
  414. numLabel.htmlFor = 'num-turns-input';
  415. numLabel.textContent = 'Keep Last:';
  416. numTurnsSetting.appendChild(numLabel);
  417.  
  418. const numInput = document.createElement('input');
  419. numInput.type = 'number';
  420. numInput.id = 'num-turns-input';
  421. numInput.min = '0';
  422. numInput.addEventListener('change', (e) => {
  423. const num = parseInt(e.target.value, 10);
  424. const newValue = (!isNaN(num) && num >= 0) ? num : DEFAULT_SETTINGS.numTurnsToShow;
  425. updateSetting('numTurnsToShow', newValue);
  426. if (e.target.value !== newValue.toString()) e.target.value = newValue;
  427. });
  428. numTurnsSetting.appendChild(numInput);
  429.  
  430. const numDescSpan = document.createElement('span');
  431. numDescSpan.id = 'num-turns-description';
  432. numDescSpan.textContent = 'Exchanges'; // Initial value
  433. numTurnsSetting.appendChild(numDescSpan);
  434.  
  435. content.appendChild(numTurnsSetting);
  436.  
  437.  
  438. // --- UI Hiding Toggles ---
  439. const uiToggleGroup = document.createElement('fieldset');
  440. uiToggleGroup.className = 'popup-section ui-toggles-section';
  441. const uiLegend = document.createElement('legend');
  442. uiLegend.textContent = 'Interface Hiding:';
  443. uiToggleGroup.appendChild(uiLegend);
  444.  
  445. const createToggle = (id, labelText, settingKey) => {
  446. const div = document.createElement('div');
  447. div.className = 'popup-setting toggle-setting';
  448.  
  449. const label = document.createElement('label');
  450. label.htmlFor = id;
  451. label.className = 'toggle-label';
  452. label.textContent = labelText;
  453.  
  454. const input = document.createElement('input');
  455. input.type = 'checkbox';
  456. input.id = id;
  457. input.className = 'basic-slide-toggle'; // Style with CSS
  458. input.addEventListener('change', (e) => updateSetting(settingKey, e.target.checked));
  459.  
  460. div.appendChild(label); // Add label first
  461. div.appendChild(input); // Then add input (for styling purposes sometimes)
  462. uiToggleGroup.appendChild(div);
  463. // No need to return the input here as it's not used elsewhere directly
  464. };
  465.  
  466. createToggle('hide-sidebars-toggle', 'Hide Sidebars', 'hideSidebars');
  467. createToggle('hide-sysinstruct-toggle', 'Hide System Instructions', 'hideSystemInstructions');
  468. createToggle('use-lagfix-toggle', 'Input Lag Fix', 'useLagFixInput');
  469.  
  470. content.appendChild(uiToggleGroup);
  471. popup.appendChild(content);
  472.  
  473. // --- Footer ---
  474. const footer = document.createElement('div');
  475. footer.className = 'popup-footer';
  476. const footerSpan = document.createElement('span');
  477. footerSpan.className = 'footer-note';
  478. footerSpan.textContent = 'Mode changes close panel. Toggles save instantly.';
  479. footer.appendChild(footerSpan);
  480. popup.appendChild(footer);
  481.  
  482. return popup;
  483. }
  484. // Updates the state of controls within the popup
  485. // Updates the state of controls within the popup
  486. function updatePopupUIState() {
  487. if (!popupElement || popupElement.style.display === 'none') return;
  488.  
  489. const isVibe = settings.mode === 'vibe';
  490. const isOff = settings.mode === 'off';
  491.  
  492. // --- Update Vibe Button Appearance ---
  493. const vibeButton = popupElement.querySelector('#vibe-mode-button');
  494. if (vibeButton) {
  495. vibeButton.classList.toggle('active', isVibe);
  496. // Maybe change text when active?
  497. const iconSpan = vibeButton.querySelector('.material-symbols-outlined');
  498. const textNode = vibeButton.lastChild; // Assuming text is last child
  499. if (isVibe && textNode.nodeType === Node.TEXT_NODE) {
  500. textNode.textContent = ' VIBE MODE ACTIVE';
  501. if (iconSpan) iconSpan.textContent = 'check_circle'; // Show checkmark?
  502. } else if (textNode.nodeType === Node.TEXT_NODE){
  503. textNode.textContent = ' Activate VIBE Mode';
  504. if (iconSpan) iconSpan.textContent = ICON_VIBE; // Restore original icon
  505. }
  506. }
  507.  
  508.  
  509. // --- Update History Mode Radio Buttons ---
  510. popupElement.querySelectorAll('input[name="history-mode-radio"]').forEach(radio => {
  511. radio.checked = (settings.mode === radio.value);
  512. // --- FIX: DO NOT disable radios when Vibe is active ---
  513. // Radios should always be clickable to exit Vibe mode
  514. radio.disabled = false;
  515. });
  516.  
  517. // --- Update Number Input ---
  518. const numInput = popupElement.querySelector('#num-turns-input');
  519. const numDesc = popupElement.querySelector('#num-turns-description');
  520. if (numInput) {
  521. numInput.value = settings.numTurnsToShow;
  522. // Disable number input only if mode is Off OR Vibe
  523. numInput.disabled = isOff || isVibe;
  524. }
  525. if(numDesc) {
  526. numDesc.textContent = (settings.mode === 'manual' || settings.mode === 'auto') ? 'Exchanges (User+AI)' : ' ';
  527. // Hide description if number input is disabled
  528. numDesc.style.display = (isOff || isVibe) ? 'none' : '';
  529. }
  530.  
  531. // --- Update UI Toggles State and Disabled Status ---
  532. const updateToggleUI = (id, settingKey) => {
  533. const toggle = popupElement.querySelector(`#${id}`);
  534. if (toggle) {
  535. toggle.checked = settings[settingKey];
  536. // FIX: Disable UI toggles ONLY if Vibe mode is active
  537. toggle.disabled = isVibe;
  538. // Also visually grey out the label if disabled
  539. const label = popupElement.querySelector(`label[for="${id}"]`);
  540. if (label) label.style.opacity = isVibe ? '0.5' : '1';
  541. }
  542. };
  543. updateToggleUI('hide-sidebars-toggle', 'hideSidebars');
  544. updateToggleUI('hide-sysinstruct-toggle', 'hideSystemInstructions');
  545. updateToggleUI('use-lagfix-toggle', 'useLagFixInput');
  546.  
  547. }
  548. function showPopup() {
  549. // ... (Similar to v3.0, creates popup if needed, updates UI, positions, adds listener) ...
  550. if (!scriptToggleButton) return;
  551. if (!popupElement) {
  552. popupElement = createPopupHtml();
  553. document.body.appendChild(popupElement);
  554. }
  555. updatePopupUIState(); // Ensure UI reflects current settings
  556.  
  557. const buttonRect = scriptToggleButton.getBoundingClientRect();
  558. popupElement.style.top = `${buttonRect.bottom + window.scrollY + 5}px`;
  559. popupElement.style.left = 'auto';
  560. popupElement.style.right = `${window.innerWidth - buttonRect.right - window.scrollX}px`;
  561. popupElement.style.display = 'block';
  562. console.log("AC Script: Popup shown.");
  563. setTimeout(() => {
  564. document.addEventListener('click', handleClickOutsidePopup, { capture: true, once: true });
  565. }, 0);
  566. }
  567.  
  568. function hidePopup() {
  569. // ... (Similar to v3.0) ...
  570. if (popupElement) {
  571. popupElement.style.display = 'none';
  572. document.removeEventListener('click', handleClickOutsidePopup, { capture: true });
  573. console.log("AC Script: Popup hidden.");
  574. }
  575. }
  576.  
  577. function togglePopup(event) {
  578. // ... (Similar to v3.0) ...
  579. if (event) event.stopPropagation();
  580. if (popupElement?.style.display === 'block') { hidePopup(); }
  581. else { showPopup(); }
  582. }
  583.  
  584. function handleClickOutsidePopup(event) {
  585. // ... (Similar to v3.0, but check scriptToggleButton too) ...
  586. if (popupElement?.style.display === 'block' &&
  587. !popupElement.contains(event.target) &&
  588. scriptToggleButton && !scriptToggleButton.contains(event.target)) {
  589. console.log("AC Script: Clicked outside popup.");
  590. hidePopup();
  591. } else if (popupElement?.style.display === 'block') {
  592. // Re-add listener if click was inside
  593. document.addEventListener('click', handleClickOutsidePopup, { capture: true, once: true });
  594. }
  595. }
  596.  
  597. // --- Input Lag Fix Logic ---
  598. // --- Input Lag Fix Logic ---
  599. function toggleLagFixInput(activate) {
  600. // Ensure we have the real elements cached or find them
  601. if (!realChatInput) realChatInput = document.querySelector(CHAT_INPUT_SELECTOR);
  602. if (!realRunButton) realRunButton = document.querySelector(RUN_BUTTON_SELECTOR);
  603.  
  604. if (activate) {
  605. // --- Activate Lag Fix ---
  606. if (!realChatInput || !realRunButton) {
  607. console.error("AC Script: Cannot activate Lag Fix - Real Input or Run button not found! Verify selectors:", CHAT_INPUT_SELECTOR, RUN_BUTTON_SELECTOR);
  608. if(settings.useLagFixInput || settings.mode === 'vibe') {
  609. if(settings.useLagFixInput) updateSetting('useLagFixInput', false);
  610. }
  611. return; // Stop activation
  612. }
  613.  
  614. // Check if fake elements already exist to prevent duplicates
  615. const existingFakeInput = document.getElementById(FAKE_INPUT_ID);
  616. const existingFakeButton = document.getElementById(FAKE_RUN_BUTTON_ID);
  617.  
  618. if (!existingFakeInput) { // Only create if it doesn't exist
  619. console.log("AC Script: Activating Lag Fix Input.");
  620. try {
  621. // --- Hide Real Input ---
  622. realChatInput.classList.add('adv-controls-real-input-hidden');
  623.  
  624. // --- Create Fake Input ---
  625. fakeChatInput = document.createElement('textarea'); // Use the state variable
  626. fakeChatInput.id = FAKE_INPUT_ID;
  627. fakeChatInput.className = 'advanced-control-fake-input'; // Class for styling via addStyles
  628. fakeChatInput.setAttribute('placeholder', realChatInput.getAttribute('placeholder') || 'Type something...');
  629. fakeChatInput.setAttribute('rows', realChatInput.getAttribute('rows') || '1'); // Copy rows attribute
  630. // Apply necessary styles directly for layout matching
  631. const computedStyle = window.getComputedStyle(realChatInput);
  632. fakeChatInput.style.height = computedStyle.height;
  633. fakeChatInput.style.resize = computedStyle.resize;
  634. fakeChatInput.style.overflow = computedStyle.overflow;
  635. fakeChatInput.style.width = '100%';
  636.  
  637. // Insert fake input before the real one
  638. realChatInput.parentNode.insertBefore(fakeChatInput, realChatInput);
  639.  
  640. // Explicitly focus the fake input
  641. setTimeout(() => fakeChatInput.focus(), 100);
  642.  
  643. } catch (error) {
  644. console.error("AC Script: Error creating fake input:", error);
  645. // Attempt cleanup if input creation failed
  646. realChatInput?.classList.remove('adv-controls-real-input-hidden');
  647. if(fakeChatInput) fakeChatInput.remove(); // Remove partially created element
  648. fakeChatInput = null; // Reset state variable
  649. return; // Stop activation
  650. }
  651. } else {
  652. // If fake input exists ensure it gets focus
  653. fakeChatInput = existingFakeInput; // Ensure state variable is correct
  654. setTimeout(() => fakeChatInput.focus(), 100);
  655. }
  656.  
  657. if (!existingFakeButton && fakeChatInput) { // Only create button if it doesn't exist AND fake input exists
  658. console.log("AC Script: Creating Fake Run Button.");
  659. try {
  660. const fakeRunButton = document.createElement('button');
  661. fakeRunButton.id = FAKE_RUN_BUTTON_ID;
  662. fakeRunButton.className = 'advanced-control-fake-run-button'; // Style with CSS
  663. fakeRunButton.textContent = 'Run (Lag Fix)'; // Indicate it's the fake one
  664. fakeRunButton.type = 'button'; // Prevent default form submission if any
  665. fakeRunButton.addEventListener('click', handleFakeRunButtonClick); // Use a dedicated handler
  666.  
  667. // Insert the fake button - try inserting it *after* the fake input's container div
  668. // Adjust this based on where the original Run button visually is relative to the textarea
  669. // Assuming the real input and button share a common parent wrapper:
  670. const inputWrapper = realChatInput.closest('.prompt-input-wrapper-container') || realChatInput.parentNode; // Find a suitable parent
  671. const buttonContainer = inputWrapper.querySelector('.button-wrapper:last-of-type'); // Find the original button's wrapper
  672. if (buttonContainer) {
  673. buttonContainer.parentNode.insertBefore(fakeRunButton, buttonContainer.nextSibling); // Insert after button wrapper
  674. // Hide the original button's wrapper visually
  675. buttonContainer.style.display = 'none';
  676. } else {
  677. // Fallback: Insert after the fake input if wrapper not found
  678. fakeChatInput.parentNode.insertBefore(fakeRunButton, fakeChatInput.nextSibling);
  679. }
  680. console.log("AC Script: Fake Run button added.");
  681. } catch (error) {
  682. console.error("AC Script: Error creating fake run button:", error);
  683. // Don't necessarily stop activation, input might still work manually
  684. }
  685. }
  686.  
  687. } else {
  688. // --- Deactivate Lag Fix ---
  689. console.log("AC Script: Deactivating Lag Fix Input.");
  690. const existingFakeInput = document.getElementById(FAKE_INPUT_ID);
  691. if (existingFakeInput) {
  692. existingFakeInput.remove();
  693. }
  694. fakeChatInput = null; // Reset state
  695.  
  696. const existingFakeButton = document.getElementById(FAKE_RUN_BUTTON_ID);
  697. if (existingFakeButton) {
  698. existingFakeButton.remove();
  699. }
  700.  
  701. // Restore real input
  702. if (realChatInput) {
  703. realChatInput.classList.remove('adv-controls-real-input-hidden');
  704. }
  705.  
  706. // Restore original button wrapper's visibility if we hid it
  707. if(realRunButton){
  708. const inputWrapper = realChatInput.closest('.prompt-input-wrapper-container') || realChatInput.parentNode;
  709. const buttonContainer = inputWrapper.querySelector('.button-wrapper:has(run-button)'); // Find original button container
  710. if (buttonContainer) buttonContainer.style.display = ''; // Restore display
  711. }
  712. }
  713. }
  714. // --- Handler for the FAKE Run Button ---
  715. function handleFakeRunButtonClick(event) {
  716. // --- Ensure we have references to the necessary elements ---
  717. // Re-query just in case, although they should be cached if lag fix is active
  718. const currentFakeInput = document.getElementById(FAKE_INPUT_ID);
  719. const currentRealInput = realChatInput || document.querySelector(CHAT_INPUT_SELECTOR);
  720. const currentRealRunButton = realRunButton || document.querySelector(RUN_BUTTON_SELECTOR);
  721.  
  722. if (currentFakeInput && currentRealInput && currentRealRunButton) {
  723. console.log("AC Script: FAKE Run Button Clicked! Attempting submit.");
  724.  
  725. // 1. Copy text from fake to real
  726. const textToSubmit = currentFakeInput.value;
  727. if (!textToSubmit.trim()) {
  728. console.log("AC Script: Fake input is empty, doing nothing.");
  729. return; // Don't submit if empty
  730. }
  731. currentRealInput.value = textToSubmit;
  732. console.log("AC Script: Copied text to real input.");
  733.  
  734. // 2. Trigger events on real input to make the site aware
  735. try {
  736. currentRealInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  737. currentRealInput.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
  738. console.log("AC Script: Dispatched input/change events on real input.");
  739. } catch (e) {
  740. console.error("AC Script: Error dispatching events on real input:", e);
  741. // Don't necessarily stop here, try clicking anyway? Maybe comment out return.
  742. // return; // Optional: Stop if events fail
  743. }
  744.  
  745. // 3. Clear the fake input (do this AFTER potentially needing focus)
  746. // currentFakeInput.value = ''; // Let's clear it AFTER the click attempt
  747.  
  748. // 4. Ensure the REAL input potentially has focus briefly before the click might need it
  749. // Although clicking the button should be sufficient usually
  750. currentRealInput.focus(); // Try focusing the real input briefly
  751. currentRealInput.blur(); // Then blur it, sometimes helps trigger validation
  752.  
  753. // 5. Force Enable the REAL Run Button
  754. let wasDisabled = false;
  755. if (currentRealRunButton.disabled) {
  756. currentRealRunButton.disabled = false;
  757. wasDisabled = true;
  758. console.log("AC Script: Force removed 'disabled' attribute from Real Run button.");
  759. }
  760. // NOTE: Add class removal here if necessary, based on inspecting the disabled state
  761.  
  762. // 6. Programmatically click the REAL Run Button
  763. // Use a timeout to allow potential UI updates after events/focus/enable
  764. setTimeout(() => {
  765. console.log("AC Script: Programmatically clicking REAL Run button.");
  766. if (currentRealRunButton.offsetParent === null) { // Check if button is actually visible
  767. console.warn("AC Script: Real run button is not visible/in DOM just before click?");
  768. if (wasDisabled) currentRealRunButton.disabled = true; // Re-disable if we couldn't click
  769. return;
  770. }
  771.  
  772. // --- THE ACTUAL CLICK ---
  773. currentRealRunButton.click();
  774. // --- END CLICK ---
  775.  
  776. console.log("AC Script: Click dispatched on real button.");
  777.  
  778. // Clear the fake input now
  779. currentFakeInput.value = '';
  780.  
  781.  
  782. // Optional: Re-disable immediately after clicking? Less critical now.
  783. // if (wasDisabled) {
  784. // setTimeout(() => { currentRealRunButton.disabled = true; }, 10);
  785. // }
  786.  
  787. }, 150); // Slightly increased delay again to 150ms, just in case
  788.  
  789. } else {
  790. console.warn("AC Script: Fake Run Button click failed - elements missing.",
  791. { currentFakeInput, currentRealInput, currentRealRunButton }); // Log which element might be missing
  792. }
  793. }
  794.  
  795. // --- Mutation Observer (for Auto mode chat hiding) ---
  796. function handleChatMutation(mutationsList, observer) {
  797. // ... (Similar logic to v3.0, checking for added AI turns) ...
  798. if (settings.mode !== 'auto') return;
  799. let newAiTurnAdded = false;
  800. // ... (rest of mutation checking logic) ...
  801.  
  802. if (newAiTurnAdded) {
  803. clearTimeout(debounceTimer);
  804. debounceTimer = setTimeout(() => {
  805. console.log("AC Script: Auto mode applying chat rules.");
  806. applyChatVisibilityRules();
  807. const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
  808. if(chatContainer) setTimeout(() => chatContainer.scrollTop = chatContainer.scrollHeight, 50);
  809. }, 300);
  810. }
  811. }
  812.  
  813. function initChatObserver() {
  814. // ... (Similar logic to v3.0, starting/stopping based on settings.mode === 'auto') ...
  815. if (chatObserver) { chatObserver.disconnect(); chatObserver = null; }
  816. if (settings.mode === 'auto') {
  817. const chatContainer = document.querySelector(CHAT_CONTAINER_SELECTOR);
  818. if (chatContainer) {
  819. chatObserver = new MutationObserver(handleChatMutation);
  820. chatObserver.observe(chatContainer, { childList: true, subtree: true });
  821. console.log("AC Script: Chat observer started for auto-hide.");
  822. } else { console.warn("AC Script: Could not find chat container for observer."); }
  823. } else { console.log("AC Script: Chat observer inactive."); }
  824. }
  825.  
  826. // --- Initialization & Styles ---
  827. function addStyles() {
  828. // --- APPROXIMATE DARK THEME ---
  829. // These colors are guesses based on common dark themes.
  830. // Replace with exact values if you find them via Inspector.
  831. const darkBg = '#202124';// Main dark background
  832. const lighterDarkBg = '#303134'; // Slightly lighter background (e.g., popup header/footer)
  833. const lightText = '#e8eaed';// Main light text
  834. const mediumText = '#bdc1c6';// Secondary text (e.g., descriptions)
  835. const darkBorder = '#5f6368';// Borders
  836. const accentColor = '#8ab4f8';// Accent color (e.g., toggle ON state, buttons) - Google blueish
  837. const handleColor = '#e8eaed';// Toggle handle color
  838. const trackOffColor = '#5f6368';// Toggle track OFF color
  839. const inputBg = '#3c4043';// Input field background
  840.  
  841. GM_addStyle(`
  842. /* --- General Popup Styling (Dark Theme Approx) --- */
  843. :root { /* Define CSS variables for easier reuse */
  844. --popup-bg: ${darkBg};
  845. --popup-header-bg: ${lighterDarkBg};
  846. --popup-footer-bg: ${lighterDarkBg};
  847. --popup-text-primary: ${lightText};
  848. --popup-text-secondary: ${mediumText};
  849. --popup-border: ${darkBorder};
  850. --input-bg: ${inputBg};
  851. --input-border: ${darkBorder};
  852. --input-text: ${lightText};
  853. --accent-color: ${accentColor};
  854. --toggle-handle-color: ${handleColor};
  855. --toggle-track-off-color: ${trackOffColor};
  856. --textarea-bg-color: ${inputBg}; /* For fake input */
  857. --textarea-text-color: ${lightText}; /* For fake input */
  858. --textarea-border-color: ${darkBorder}; /* For fake input */
  859. }
  860. /* --- Hiding Real Input for Lag Fix --- */
  861. .adv-controls-real-input-hidden {
  862. visibility: hidden !important;
  863. position: absolute !important; /* Take out of flow */
  864. height: 1px !important;
  865. width: 1px !important;
  866. overflow: hidden !important;
  867. border: none !important;
  868. padding: 0 !important;
  869. margin: 0 !important;
  870. opacity: 0 !important;
  871. }
  872.  
  873. /* --- Fake Input Basic Styling (Refined) --- */
  874. #${FAKE_INPUT_ID}.advanced-control-fake-input {
  875. /* Styles copied dynamically, use CSS vars */
  876. background-color: var(--textarea-bg-color);
  877. color: var(--textarea-text-color);
  878. border: 1px solid var(--textarea-border-color);
  879. padding: 10px; /* Example padding, adjust if needed based on real input */
  880. border-radius: 4px; /* Example radius */
  881. font-family: inherit; /* Inherit from container */
  882. font-size: inherit; /* Inherit from container */
  883. line-height: 1.5; /* Example line-height */
  884. display: block; /* Ensure block layout */
  885. box-sizing: border-box;
  886. margin: 0; /* Reset margin */
  887. /* Width and height set dynamically via JS */
  888. /* Ensure transitions don't interfere */
  889. transition: none !important;
  890. }
  891.  
  892.  
  893. #${POPUP_ID} {
  894. display: none; position: absolute; z-index: 10001;
  895. background-color: var(--popup-bg);
  896. border: 1px solid var(--popup-border);
  897. border-radius: 8px;
  898. box-shadow: 0 4px 8px 3px rgba(0,0,0,0.3); /* Darker shadow */
  899. width: 340px; /* Slightly wider */
  900. font-family: "Google Sans", Roboto, Arial, sans-serif; /* Verify Font */
  901. font-size: 14px;
  902. color: var(--popup-text-primary);
  903. overflow: hidden;
  904. }
  905. #${POPUP_ID} .popup-header {
  906. display: flex; justify-content: space-between; align-items: center;
  907. padding: 12px 16px; border-bottom: 1px solid var(--popup-border);
  908. font-weight: 500; font-size: 16px;
  909. background-color: var(--popup-header-bg);
  910. }
  911. #${POPUP_ID} .close-popup-button {
  912. background: none; border: none; font-size: 24px; line-height: 1;
  913. cursor: pointer; color: var(--popup-text-secondary); padding: 0 4px; margin: -4px;
  914. }
  915. #${POPUP_ID} .close-popup-button:hover { color: var(--popup-text-primary); }
  916. #${POPUP_ID} .popup-content { padding: 16px; display: flex; flex-direction: column; gap: 16px; }
  917. #${POPUP_ID} .popup-section { border: none; padding: 0; margin: 0; }
  918. #${POPUP_ID} legend { font-weight: 500; padding-bottom: 8px; color: var(--popup-text-primary); border-bottom: 1px solid var(--popup-border); margin-bottom: 8px; }
  919.  
  920. /* --- Vibe Button --- */
  921. #${POPUP_ID} .vibe-section { margin-bottom: 10px; border-bottom: 1px solid var(--popup-border); padding-bottom: 15px;}
  922. #${POPUP_ID} .vibe-button {
  923. display: flex; align-items: center; justify-content: center; gap: 8px;
  924. width: 100%; padding: 10px 16px; font-size: 15px; font-weight: 500;
  925. border: 1px solid var(--popup-border); border-radius: 4px; cursor: pointer;
  926. background-color: var(--popup-bg); color: var(--popup-text-primary);
  927. transition: background-color 0.2s, border-color 0.2s;
  928. }
  929. #${POPUP_ID} .vibe-button:hover { background-color: ${lighterDarkBg}; border-color: var(--accent-color); }
  930. #${POPUP_ID} .vibe-button.active { background-color: var(--accent-color); color: ${darkBg}; border-color: var(--accent-color); }
  931. #${POPUP_ID} .vibe-button .material-symbols-outlined { font-size: 20px; }
  932.  
  933. /* --- Settings Items --- */
  934. #${POPUP_ID} .popup-setting { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
  935. #${POPUP_ID} .popup-setting label { cursor: pointer; user-select: none; }
  936. #${POPUP_ID} input[type="radio"] { accent-color: var(--accent-color); cursor: pointer; width: 16px; height: 16px; margin: 0;}
  937. #${POPUP_ID} input[type="radio"]:disabled + label { color: var(--popup-text-secondary); cursor: not-allowed; }
  938. #${POPUP_ID} input:disabled { cursor: not-allowed; opacity: 0.6; }
  939.  
  940. #${POPUP_ID} .number-setting label { white-space: nowrap; }
  941. #${POPUP_ID} input[type="number"] {
  942. width: 60px; padding: 6px 8px; border-radius: 4px; text-align: right;
  943. background-color: var(--input-bg); color: var(--input-text); border: 1px solid var(--input-border);
  944. }
  945. #${POPUP_ID} input[type="number"]:disabled { background-color: ${darkBg}; border-color: ${darkBorder}; opacity: 0.5; }
  946. #${POPUP_ID} #num-turns-description { color: var(--popup-text-secondary); font-size: 13px; }
  947.  
  948. /* --- Basic Slide Toggle Styling (Approximate) --- */
  949. #${POPUP_ID} .toggle-setting { justify-content: space-between; } /* Push toggle right */
  950. #${POPUP_ID} .toggle-label { flex-grow: 1; } /* Allow label to take space */
  951. #${POPUP_ID} .basic-slide-toggle {
  952. appearance: none; -webkit-appearance: none; position: relative;
  953. width: 36px; height: 20px; border-radius: 10px;
  954. background-color: var(--toggle-track-off-color);
  955. cursor: pointer; transition: background-color 0.2s ease-in-out;
  956. display: inline-block; vertical-align: middle;
  957. }
  958. #${POPUP_ID} .basic-slide-toggle::before { /* The Handle */
  959. content: ''; position: absolute;
  960. width: 16px; height: 16px; border-radius: 50%;
  961. background-color: var(--toggle-handle-color);
  962. top: 2px; left: 2px;
  963. transition: transform 0.2s ease-in-out;
  964. box-shadow: 0 1px 3px rgba(0,0,0,0.4);
  965. }
  966. #${POPUP_ID} .basic-slide-toggle:checked {
  967. background-color: var(--accent-color);
  968. }
  969. #${POPUP_ID} .basic-slide-toggle:checked::before {
  970. transform: translateX(16px); /* Move handle right */
  971. }
  972. #${POPUP_ID} .basic-slide-toggle:disabled { opacity: 0.5; cursor: not-allowed; }
  973. #${POPUP_ID} .basic-slide-toggle:disabled::before { background-color: ${mediumText}; }
  974.  
  975.  
  976. /* --- Footer --- */
  977. #${POPUP_ID} .popup-footer {
  978. padding: 8px 16px; border-top: 1px solid var(--popup-border); font-size: 12px;
  979. color: var(--popup-text-secondary); text-align: center;
  980. background-color: var(--popup-footer-bg);
  981. }
  982.  
  983. /* --- UI Hiding Classes --- */
  984. /* Apply these classes to OVERALL_LAYOUT_SELECTOR */
  985. .${LAYOUT_HIDE_CLASS}-sidebars ${LEFT_SIDEBAR_SELECTOR},
  986. .${LAYOUT_HIDE_CLASS}-sidebars ${RIGHT_SIDEBAR_SELECTOR} {
  987. display: none !important;
  988. }
  989. .${LAYOUT_HIDE_CLASS}-sysinstruct ${SYSTEM_INSTRUCTIONS_SELECTOR} {
  990. display: none !important;
  991. }
  992.  
  993. /* --- Fake Input Styling --- */
  994. #${FAKE_INPUT_ID} {
  995. /* Styles copied in JS, use CSS vars */
  996. background-color: var(--textarea-bg-color);
  997. color: var(--textarea-text-color);
  998. border: 1px solid var(--textarea-border-color);
  999. display: block; /* Ensure it takes block layout */
  1000. /* Ensure transitions don't interfere if original had them */
  1001. transition: none !important;
  1002. }
  1003. /* --- Fake Run Button Styling --- */
  1004. #${FAKE_RUN_BUTTON_ID}.advanced-control-fake-run-button {
  1005. /* Style similarly to the real run button */
  1006. background-color: var(--accent-color); /* Use accent color */
  1007. color: var(--popup-bg); /* Dark text on light button */
  1008. border: none;
  1009. border-radius: 4px; /* Match real button radius */
  1010. padding: 8px 16px; /* Adjust padding */
  1011. margin-left: 8px; /* Space from input */
  1012. font-size: 14px; /* Match real button */
  1013. font-weight: 500; /* Match real button */
  1014. cursor: pointer;
  1015. transition: background-color 0.2s;
  1016. }
  1017. #${FAKE_RUN_BUTTON_ID}.advanced-control-fake-run-button:hover {
  1018. opacity: 0.9; /* Simple hover effect */
  1019. }
  1020.  
  1021. `);
  1022. }
  1023.  
  1024. // Utility to wait for an element
  1025. function waitForElement(selector, callback, checkFrequency = 300, timeout = 15000) {
  1026. // ... (same as before) ...
  1027. const startTime = Date.now();
  1028. const interval = setInterval(() => {
  1029. const element = document.querySelector(selector);
  1030. if (element) {
  1031. clearInterval(interval); callback(element);
  1032. } else if (Date.now() - startTime > timeout) {
  1033. console.error(`AC Script: Timeout waiting for element: ${selector}`); clearInterval(interval); callback(null); // Indicate failure
  1034. }
  1035. }, checkFrequency);
  1036. return interval;
  1037. }
  1038.  
  1039. // --- Main Initialization Sequence ---
  1040. async function initialize() {
  1041. console.log("AC Script: Initializing Advanced Control Suite v4.0...");
  1042. addStyles();
  1043. await loadSettings(); // Load settings first
  1044.  
  1045. // Use Promise.allSettled to wait for multiple elements, some might timeout
  1046. Promise.allSettled([
  1047. new Promise((resolve, reject) => waitForElement(BUTTON_CONTAINER_SELECTOR, resolve, 150, 10000)),
  1048. new Promise((resolve, reject) => waitForElement(CHAT_CONTAINER_SELECTOR, resolve, 300, 15000)),
  1049. new Promise((resolve, reject) => waitForElement(OVERALL_LAYOUT_SELECTOR, resolve, 300, 15000)) // Wait for layout container too
  1050. ]).then(results => {
  1051. const buttonContainerResult = results[0];
  1052. const chatContainerResult = results[1];
  1053. const layoutContainerResult = results[2];
  1054.  
  1055. if (buttonContainerResult.status === 'fulfilled' && buttonContainerResult.value) {
  1056. console.log("AC Script: Button container found.");
  1057. createScriptToggleButton(); // Create the main button
  1058. } else {
  1059. console.error("AC Script: Button container not found. UI button cannot be added.");
  1060. }
  1061.  
  1062. if (chatContainerResult.status === 'fulfilled' && chatContainerResult.value) {
  1063. console.log("AC Script: Chat container found.");
  1064. // Apply initial chat rules
  1065. applyChatVisibilityRules();
  1066. // Initialize the chat observer based on loaded settings
  1067. initChatObserver();
  1068. } else {
  1069. console.warn("AC Script: Chat container not found. History features may fail.");
  1070. }
  1071.  
  1072. if (layoutContainerResult.status === 'fulfilled' && layoutContainerResult.value) {
  1073. console.log("AC Script: Layout container found.");
  1074. // Apply initial layout rules (hiding UI elements, activating lag fix if needed)
  1075. applyLayoutRules();
  1076. } else {
  1077. console.warn(`AC Script: Layout container (${OVERALL_LAYOUT_SELECTOR}) not found. UI hiding features may fail.`);
  1078. }
  1079.  
  1080. console.log("AC Script: Initial setup attempted.");
  1081.  
  1082. // Pre-cache input/run button for lag fix if setting is initially true
  1083. if(settings.useLagFixInput || settings.mode === 'vibe'){
  1084. waitForElement(CHAT_INPUT_SELECTOR, el => { if(el) realChatInput = el; console.log("AC Script: Cached real chat input."); }, 500, 10000);
  1085. waitForElement(RUN_BUTTON_SELECTOR, el => { if(el) realRunButton = el; console.log("AC Script: Cached real run button."); }, 500, 10000);
  1086. }
  1087.  
  1088. });
  1089.  
  1090. // Register menu command regardless
  1091. GM_registerMenuCommand('Adv. Control Settings (AI Studio)', togglePopup);
  1092. }
  1093.  
  1094. // --- Start Execution ---
  1095. // Wait for window load to maximize chance of elements being ready
  1096. window.addEventListener('load', initialize);
  1097.  
  1098. })();