Google AI Studio - Auto Settings (Ultimate)

Reliable text-based search + Draggable UI + Mobile support + Auto Focus + Visual Settings Menu.

当前为 2025-11-19 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Google AI Studio - Auto Settings (Ultimate)
// @namespace    https://github.com/Stranmor/google-ai-studio-auto-settings
// @version      10.0
// @description  Reliable text-based search + Draggable UI + Mobile support + Auto Focus + Visual Settings Menu.
// @author       Stranmor
// @match        https://aistudio.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // ==================== CONFIGURATION MANAGER ====================
  const ConfigManager = {
    defaults: {
      temperature: 1.0,
      topP: 0.0,
      mediaResolution: "Low"
    },

    get() {
      return {
        temperature: GM_getValue('temperature', this.defaults.temperature),
        topP: GM_getValue('topP', this.defaults.topP),
        mediaResolution: GM_getValue('mediaResolution', this.defaults.mediaResolution)
      };
    },

    set(key, value) {
      GM_setValue(key, value);
    },

    saveAll(settings) {
      this.set('temperature', parseFloat(settings.temperature));
      this.set('topP', parseFloat(settings.topP));
      this.set('mediaResolution', settings.mediaResolution);
    }
  };

  const CONSTANTS = {
    execution: {
      maxAttempts: 20,
      retryDelay: 500,
    },
    storageKey: "as-panel-pos-v10"
  };

  // ==================== UTILITIES ====================
  const Utils = {
    sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
    
    isMobile: () => window.innerWidth < 768,

    findByText: (text, tag = "*") => {
      const xpath = `//${tag}[contains(text(), '${text}')]`;
      const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
      return result.singleNodeValue;
    },

    findInputNearLabel: (labelText) => {
      const label = Utils.findByText(labelText);
      if (!label) return null;
      
      let parent = label.parentElement;
      for (let i = 0; i < 6; i++) {
        if (!parent) break;
        const input = parent.querySelector('input[type="number"]');
        if (input) return input;
        parent = parent.parentElement;
      }
      return null;
    },

    setValue: (input, value) => {
      if (!input) return false;
      if (Math.abs(parseFloat(input.value) - value) < 0.01) return true;

      try {
        input.focus();
        const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
        nativeInputValueSetter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
        input.blur();
        return true;
      } catch (e) {
        return false;
      }
    },

    focusPrompt: () => {
      const selectors = ['textarea.textarea', '.ms-autosize-textarea textarea', 'textarea[placeholder*="Type"]', 'textarea'];
      for (const sel of selectors) {
        const el = document.querySelector(sel);
        if (el) {
          el.focus();
          if (el.value) el.selectionStart = el.selectionEnd = el.value.length;
          return true;
        }
      }
      return false;
    }
  };

  // ==================== SETTINGS MODAL (UI) ====================
  class SettingsModal {
    constructor(onSave) {
      this.onSave = onSave;
      this.injectStyles();
    }

    injectStyles() {
      if (document.getElementById('as-modal-styles')) return;
      const css = `
        .as-modal-overlay {
          position: fixed; top: 0; left: 0; width: 100%; height: 100%;
          background: rgba(0, 0, 0, 0.5); z-index: 1000000;
          display: flex; align-items: center; justify-content: center;
          backdrop-filter: blur(2px);
        }
        .as-modal {
          background: white; padding: 24px; border-radius: 12px;
          width: 320px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
          font-family: 'Google Sans', Roboto, sans-serif;
        }
        .as-modal h2 { margin: 0 0 20px 0; font-size: 20px; color: #1f2937; }
        .as-field { margin-bottom: 16px; }
        .as-field label { display: block; font-size: 14px; color: #4b5563; margin-bottom: 6px; font-weight: 500; }
        .as-input {
          width: 100%; padding: 8px 12px; border: 1px solid #d1d5db;
          border-radius: 6px; font-size: 14px; box-sizing: border-box;
          transition: border-color 0.2s;
        }
        .as-input:focus { outline: none; border-color: #3b82f6; ring: 2px solid #3b82f6; }
        .as-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 24px; }
        .as-btn {
          padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500;
          cursor: pointer; border: none; transition: background 0.2s;
        }
        .as-btn-cancel { background: #f3f4f6; color: #374151; }
        .as-btn-cancel:hover { background: #e5e7eb; }
        .as-btn-save { background: #2563eb; color: white; }
        .as-btn-save:hover { background: #1d4ed8; }
      `;
      const style = document.createElement('style');
      style.id = 'as-modal-styles';
      style.textContent = css;
      document.head.appendChild(style);
    }

    open() {
      if (document.getElementById('as-settings-modal')) return;

      const settings = ConfigManager.get();
      
      const overlay = document.createElement('div');
      overlay.className = 'as-modal-overlay';
      overlay.id = 'as-settings-modal';
      
      overlay.innerHTML = `
        <div class="as-modal">
          <h2>Auto Settings Config</h2>
          
          <div class="as-field">
            <label>Temperature (0.0 - 2.0)</label>
            <input type="number" id="as-cfg-temp" class="as-input" step="0.1" min="0" max="2" value="${settings.temperature}">
          </div>

          <div class="as-field">
            <label>Top P (0.0 - 1.0)</label>
            <input type="number" id="as-cfg-topp" class="as-input" step="0.05" min="0" max="1" value="${settings.topP}">
          </div>

          <div class="as-field">
            <label>Media Resolution</label>
            <select id="as-cfg-res" class="as-input">
              <option value="Low" ${settings.mediaResolution === 'Low' ? 'selected' : ''}>Low</option>
              <option value="Medium" ${settings.mediaResolution === 'Medium' ? 'selected' : ''}>Medium</option>
              <option value="High" ${settings.mediaResolution === 'High' ? 'selected' : ''}>High</option>
            </select>
          </div>

          <div class="as-actions">
            <button class="as-btn as-btn-cancel" id="as-btn-cancel">Cancel</button>
            <button class="as-btn as-btn-save" id="as-btn-save">Save & Apply</button>
          </div>
        </div>
      `;

      document.body.appendChild(overlay);

      // Event Listeners
      document.getElementById('as-btn-cancel').onclick = () => this.close();
      overlay.onclick = (e) => { if(e.target === overlay) this.close(); };
      
      document.getElementById('as-btn-save').onclick = () => {
        const newSettings = {
          temperature: document.getElementById('as-cfg-temp').value,
          topP: document.getElementById('as-cfg-topp').value,
          mediaResolution: document.getElementById('as-cfg-res').value
        };
        ConfigManager.saveAll(newSettings);
        this.close();
        this.onSave(); // Trigger re-run
      };
    }

    close() {
      const el = document.getElementById('as-settings-modal');
      if (el) el.remove();
    }
  }

  // ==================== MOBILE HANDLER ====================
  const MobileHandler = {
    isPanelOpen: () => {
      const input = document.querySelector('input[type="number"]');
      return input && input.offsetParent !== null;
    },
    findToggleButton: () => {
      const icons = Array.from(document.querySelectorAll('.material-symbols-outlined, .material-icons'));
      const tuneIcon = icons.find(icon => icon.textContent.trim() === 'tune');
      if (tuneIcon) return tuneIcon.closest('button');
      return document.querySelector('button.runsettings-toggle-button');
    },
    async toggle(shouldOpen) {
      if (this.isPanelOpen() === shouldOpen) return true;
      const btn = this.findToggleButton();
      if (!btn) return false;
      btn.click();
      for (let i = 0; i < 5; i++) {
        await Utils.sleep(200);
        if (this.isPanelOpen() === shouldOpen) return true;
      }
      return false;
    }
  };

  // ==================== SETTINGS APPLIER ====================
  class SettingsApplier {
    constructor() {
      this.status = { temp: false, topP: false, res: false };
    }

    reset() {
      this.status = { temp: false, topP: false, res: false };
    }

    async applyResolution(targetRes) {
      if (this.status.res) return true;
      const label = Utils.findByText("Media resolution");
      if (!label) return false;

      let parent = label.parentElement;
      let select = null;
      for (let i = 0; i < 6; i++) {
        if (!parent) break;
        select = parent.querySelector('mat-select');
        if (select) break;
        parent = parent.parentElement;
      }

      if (!select) return false;

      const currentVal = select.querySelector('.mat-mdc-select-value-text span')?.textContent?.trim();
      if (currentVal === targetRes) {
        this.status.res = true;
        return true;
      }

      select.click();
      await Utils.sleep(150);
      
      const options = document.querySelectorAll('mat-option');
      let clicked = false;
      for (const opt of options) {
        if (opt.textContent.includes(targetRes)) {
          opt.click();
          clicked = true;
          break;
        }
      }

      if (!clicked) document.body.click();
      else this.status.res = true;
      return this.status.res;
    }

    async run() {
      const settings = ConfigManager.get();

      if (Utils.isMobile()) {
        await MobileHandler.toggle(true);
        await Utils.sleep(300);
      }

      if (!this.status.temp) {
        const input = Utils.findInputNearLabel("Temperature");
        if (input && Utils.setValue(input, settings.temperature)) this.status.temp = true;
      }

      if (!this.status.topP) {
        const input = Utils.findInputNearLabel("Top P") || Utils.findInputNearLabel("Top-P");
        if (input) {
          if (settings.topP === 0) input.removeAttribute('min');
          if (Utils.setValue(input, settings.topP)) this.status.topP = true;
        }
      }

      await this.applyResolution(settings.mediaResolution);

      const allDone = this.status.temp && this.status.topP && this.status.res;

      if (Utils.isMobile() && allDone) {
        await Utils.sleep(200);
        await MobileHandler.toggle(false);
      }

      return allDone;
    }
  }

  // ==================== UI (DRAGGABLE PANEL) ====================
  class UI {
    constructor(onRetry, onSettings) {
      this.panel = null;
      this.onRetry = onRetry;
      this.onSettings = onSettings;
      this.isDragging = false;
      this.render();
    }

    render() {
      const old = document.getElementById('as-panel-v10');
      if (old) old.remove();

      this.panel = document.createElement('div');
      this.panel.id = 'as-panel-v10';
      this.panel.title = "Left Click: Retry | Right Click: Settings";
      
      Object.assign(this.panel.style, {
        position: 'fixed', zIndex: '999999', width: '32px', height: '32px',
        background: '#ffffff', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        cursor: 'pointer', userSelect: 'none', border: '1px solid #e5e7eb',
        fontSize: '18px', transition: 'transform 0.1s'
      });

      const savedPos = localStorage.getItem(CONSTANTS.storageKey);
      if (savedPos) {
        const { left, bottom } = JSON.parse(savedPos);
        this.panel.style.left = left;
        this.panel.style.bottom = bottom;
      } else {
        this.panel.style.left = '20px';
        this.panel.style.bottom = '20px';
      }

      this.updateStatus('loading');
      document.body.appendChild(this.panel);
      this.makeDraggable();
      
      // Click Events
      this.panel.addEventListener('click', () => { 
        if (!this.isDragging) this.onRetry(); 
      });
      
      this.panel.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        if (!this.isDragging) this.onSettings();
      });
    }

    updateStatus(state) {
      if (!this.panel) return;
      if (state === 'loading') {
        this.panel.innerHTML = '⏳';
        this.panel.style.borderColor = '#3b82f6';
      } else if (state === 'success') {
        this.panel.innerHTML = '<span style="color:#10b981">✓</span>';
        this.panel.style.borderColor = '#10b981';
      } else {
        this.panel.innerHTML = '<span style="color:#ef4444">!</span>';
        this.panel.style.borderColor = '#ef4444';
      }
    }

    makeDraggable() {
      let startX, startY, startLeft, startBottom;
      const onDown = (e) => {
        if (e.button === 2) return;
        this.isDragging = false;
        startX = e.clientX || e.touches?.[0]?.clientX;
        startY = e.clientY || e.touches?.[0]?.clientY;
        const rect = this.panel.getBoundingClientRect();
        startLeft = rect.left;
        startBottom = window.innerHeight - rect.bottom;
        document.addEventListener('mousemove', onMove);
        document.addEventListener('touchmove', onMove, { passive: false });
        document.addEventListener('mouseup', onUp);
        document.addEventListener('touchend', onUp);
      };
      const onMove = (e) => {
        const cx = e.clientX || e.touches?.[0]?.clientX;
        const cy = e.clientY || e.touches?.[0]?.clientY;
        if (Math.abs(cx - startX) > 3 || Math.abs(cy - startY) > 3) {
          this.isDragging = true;
          if (e.preventDefault) e.preventDefault();
        }
        this.panel.style.left = `${startLeft + (cx - startX)}px`;
        this.panel.style.bottom = `${startBottom - (cy - startY)}px`;
      };
      const onUp = () => {
        document.removeEventListener('mousemove', onMove);
        document.removeEventListener('touchmove', onMove);
        document.removeEventListener('mouseup', onUp);
        document.removeEventListener('touchend', onUp);
        localStorage.setItem(CONSTANTS.storageKey, JSON.stringify({
          left: this.panel.style.left, bottom: this.panel.style.bottom
        }));
      };
      this.panel.addEventListener('mousedown', onDown);
      this.panel.addEventListener('touchstart', onDown, { passive: false });
    }
  }

  // ==================== MAIN CONTROLLER ====================
  class Main {
    constructor() {
      this.applier = new SettingsApplier();
      this.modal = new SettingsModal(() => this.startProcess());
      
      this.ui = new UI(
        () => this.startProcess(), // Left click: Retry
        () => this.modal.open()    // Right click: Settings
      );
      
      this.attempts = 0;
      this.timer = null;

      // Register Tampermonkey Menu Command
      GM_registerMenuCommand("⚙️ Configure Auto Settings", () => this.modal.open());
    }

    init() {
      let lastUrl = location.href;
      setInterval(() => {
        if (location.href !== lastUrl) {
          lastUrl = location.href;
          this.startProcess();
        }
      }, 1000);
      this.startProcess();
    }

    startProcess() {
      this.applier.reset();
      this.attempts = 0;
      this.ui.updateStatus('loading');
      if (this.timer) clearTimeout(this.timer);
      this.loop();
    }

    async loop() {
      const done = await this.applier.run();
      
      if (done) {
        this.ui.updateStatus('success');
        setTimeout(() => Utils.focusPrompt(), 100);
      } else {
        this.attempts++;
        if (this.attempts < CONSTANTS.execution.maxAttempts) {
          this.timer = setTimeout(() => this.loop(), CONSTANTS.execution.retryDelay);
        } else {
          this.ui.updateStatus('error');
        }
      }
    }
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => new Main().init());
  } else {
    new Main().init();
  }

})();