自动关闭 Google AI Studio 设置面板

当 AI Studio 页面加载时,自动关闭设置面板以节省页面空间。顺带一提,这个脚本是 Gemini 在 AI Studio 中编写的。

  1. // ==UserScript==
  2. // @name Automatically Close Google AI Studio Settings Panel
  3. // @name:zh-CN 自动关闭 Google AI Studio 设置面板
  4. // @namespace Violentmonkey Scripts
  5. // @match https://aistudio.google.com/*
  6. // @grant GM_addStyle
  7. // @run-at document-start
  8. // @version 1.1.20250413
  9. // @author plasma-green & Gemini 2.5 Pro Preview 03-25
  10. // @description When the AI Studio page loads, the settings panel is automatically closed to save page space. By the way, this script was written by Gemini in AI Studio.
  11. // @description:zh-CN 当 AI Studio 页面加载时,自动关闭设置面板以节省页面空间。顺带一提,这个脚本是 Gemini 在 AI Studio 中编写的。
  12. // @license AGPL-3.0
  13. // ==/UserScript==
  14.  
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. // --- 配置 ---
  20. const buttonSelector = 'button[aria-label="Close run settings panel"]'; // 目标按钮的选择器
  21. const logPrefix = '[AutoClick RunSettings] '; // 日志前缀,方便调试
  22. const clickDelay = 1200; // 延迟时间 (毫秒),2000ms = 2秒
  23.  
  24. console.log(logPrefix + '脚本启动,开始监控按钮: ' + buttonSelector + ',延迟 ' + (clickDelay / 1000) + ' 秒');
  25.  
  26. // 使用 WeakMap 来跟踪哪些按钮已经有待处理的点击计时器
  27. // WeakMap 的好处是当按钮元素从 DOM 中移除并被垃圾回收时,对应的条目也会自动移除,避免内存泄漏。
  28. const pendingClickTimeouts = new WeakMap();
  29.  
  30. // --- 实际执行点击操作的函数 ---
  31. function performActualClick(button) {
  32. try {
  33. console.log(logPrefix + '执行点击...');
  34. // 尝试 dispatchEvent (无 view)
  35. const event = new MouseEvent('click', {
  36. bubbles: true,
  37. cancelable: true
  38. });
  39. if (button.dispatchEvent(event)) {
  40. console.log(logPrefix + 'dispatchEvent(click) 成功。');
  41. return true;
  42. } else {
  43. console.log(logPrefix + 'dispatchEvent(click) 执行但可能被取消。');
  44. return true; // 认为已尝试
  45. }
  46. } catch (dispatchError) {
  47. console.warn(logPrefix + 'dispatchEvent 点击失败:', dispatchError);
  48. console.log(logPrefix + '尝试回退到 button.click()...');
  49. try {
  50. button.click(); // 回退
  51. console.log(logPrefix + 'button.click() 成功。');
  52. return true;
  53. } catch (clickError) {
  54. console.error(logPrefix + 'button.click() 也失败:', clickError);
  55. return false;
  56. }
  57. }
  58. }
  59.  
  60.  
  61. // --- 检查按钮状态并计划(或取消)延迟点击 ---
  62. function scheduleOrCancelClick(button) {
  63. // 检查按钮当前是否可见且可交互
  64. const isButtonClickable = button && document.body.contains(button) && button.offsetParent !== null && !button.disabled;
  65.  
  66. if (isButtonClickable) {
  67. // 如果按钮可点击,并且 *没有* 正在等待的计时器
  68. if (!pendingClickTimeouts.has(button)) {
  69. console.log(logPrefix + '检测到可点击按钮,将在 ' + (clickDelay / 1000) + ' 秒后点击...');
  70. const timeoutId = setTimeout(() => {
  71. // --- Timeout 回调 ---
  72. // 再次检查按钮状态,确保在延迟期间它没有消失或变得不可点击
  73. if (button && document.body.contains(button) && button.offsetParent !== null && !button.disabled) {
  74. console.log(logPrefix + '延迟时间到,按钮仍然可点击。');
  75. performActualClick(button);
  76. } else {
  77. console.log(logPrefix + '延迟时间到,但按钮已不可点击,取消本次点击。');
  78. }
  79. // 无论是否点击,都要从 WeakMap 中移除记录,允许下次重新调度
  80. pendingClickTimeouts.delete(button);
  81. // --- Timeout 回调结束 ---
  82. }, clickDelay);
  83.  
  84. // 将计时器 ID 存入 WeakMap,标记这个按钮正在等待点击
  85. pendingClickTimeouts.set(button, timeoutId);
  86. } else {
  87. // console.log(logPrefix + '按钮已在等待点击计时中,忽略本次检测。'); // 可选调试日志
  88. }
  89. } else {
  90. // 如果按钮变得不可点击 (隐藏、禁用、移除) 并且 *有* 正在等待的计时器
  91. if (pendingClickTimeouts.has(button)) {
  92. console.log(logPrefix + '按钮变得不可点击,取消之前计划的延迟点击。');
  93. clearTimeout(pendingClickTimeouts.get(button)); // 清除计时器
  94. pendingClickTimeouts.delete(button); // 从 WeakMap 中移除记录
  95. }
  96. }
  97. }
  98.  
  99. // --- MutationObserver 回调 ---
  100. const observerCallback = (mutationsList, observer) => {
  101. // 简单起见,每次DOM变化都重新查找按钮并评估状态
  102. // 优化:可以只检查与 buttonSelector 相关的变化,但通常 querySelector 性能足够
  103. const targetButton = document.querySelector(buttonSelector);
  104.  
  105. if (targetButton) {
  106. // 如果按钮存在,调用调度函数来决定是否启动或什么都不做
  107. scheduleOrCancelClick(targetButton);
  108. } else {
  109. // 如果按钮在 DOM 中找不到了,我们需要检查是否有对应的计时器需要取消
  110. // (注意:如果按钮实例还在内存中且在WeakMap里,下面的逻辑可能不完美,
  111. // 但通常按钮不存在于DOM时,scheduleOrCancelClick(null)不会执行,
  112. // 而如果按钮实例还在但被隐藏/禁用,scheduleOrCancelClick会处理取消)
  113. // 简单起见,依赖 scheduleOrCancelClick 在按钮不可见时处理取消。
  114. // console.log(logPrefix + '未找到目标按钮。');
  115. }
  116.  
  117. // --- 更精细的检查(可选)---
  118. // 如果性能有问题,可以像之前版本那样检查 mutationsList
  119. // 但需要在找到按钮或按钮消失时都调用 scheduleOrCancelClick
  120. /*
  121. for (const mutation of mutationsList) {
  122. // ... 检查添加、移除、属性变化 ...
  123. if (/* 按钮出现或变得可点击 * /) {
  124. const button = mutation.target or found button;
  125. scheduleOrCancelClick(button);
  126. } else if (/* 按钮消失或变得不可点击 * /) {
  127. const button = mutation.target or previously known button;
  128. // 需要一种方法获取到旧按钮的引用来取消计时器
  129. // 这使得简单地每次重新 querySelector 更方便
  130. }
  131. }
  132. */
  133. };
  134.  
  135. // --- 创建并配置观察器 ---
  136. const observer = new MutationObserver(observerCallback);
  137. const observerConfig = {
  138. childList: true,
  139. subtree: true,
  140. attributes: true, // 监控属性变化也很重要,因为按钮可能通过 disabled 或 style/class 变化
  141. attributeFilter: ['disabled', 'class', 'style'] // 可以限定关心的属性
  142. };
  143.  
  144. // --- 初始化脚本 ---
  145. function initialize() {
  146. if (document.body) {
  147. console.log(logPrefix + 'DOM 已准备好,执行首次检查并启动 MutationObserver...');
  148. const initialButton = document.querySelector(buttonSelector);
  149. if (initialButton) {
  150. scheduleOrCancelClick(initialButton); // 初始检查也走调度逻辑
  151. }
  152. observer.observe(document.body, observerConfig);
  153. console.log(logPrefix + 'MutationObserver 已启动。');
  154. } else {
  155. requestAnimationFrame(initialize);
  156. }
  157. }
  158.  
  159. // --- 启动逻辑 ---
  160. if (document.readyState === 'loading') {
  161. document.addEventListener('DOMContentLoaded', initialize);
  162. } else {
  163. initialize();
  164. }
  165.  
  166. // --- 清理 ---
  167. window.addEventListener('unload', () => {
  168. if (observer) {
  169. observer.disconnect();
  170. console.log(logPrefix + '页面卸载,MutationObserver 已断开。');
  171. }
  172. // 清理所有可能存在的待处理计时器
  173. // 遍历当前页面上所有匹配的按钮来查找并清除计时器
  174. document.querySelectorAll(buttonSelector).forEach(button => {
  175. if (pendingClickTimeouts.has(button)) {
  176. clearTimeout(pendingClickTimeouts.get(button));
  177. pendingClickTimeouts.delete(button);
  178. console.log(logPrefix + '页面卸载,清除了一个待处理的点击计时器。');
  179. }
  180. });
  181. });
  182.  
  183. GM_addStyle(`
  184. /* 隐藏不需要的元素 */
  185. .header-container, .placeholder-overlay.ng-star-inserted {
  186. display: none !important;
  187. }`);
  188. })();