Google AI Studio auto-continue helper

auto-continue helper for Google AI Studio

Tính đến 11-06-2025. Xem phiên bản mới nhất.

// ==UserScript==
// @name         Google AI Studio auto-continue helper
// @name:zh-CN   Google AI Studio 自动续写助手
// @namespace    http://tampermonkey.net/
// @version      4.0 (Advanced UI - Collapsible Panel)
// @description  auto-continue helper for Google AI Studio
// @description:zh-CN  谷歌AI Studio 自动续写助手
// @author       metrovoc
// @match        https://aistudio.google.com/prompts/*
// @grant        GM_addStyle
// @icon         https://www.google.com/s2/favicons?domain=aistudio.google.com
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // --- 配置区域 ---
  const SCROLL_CONTAINER_SELECTOR = "ms-autoscroll-container";
  const MESSAGE_TURN_SELECTOR = "ms-chat-turn";
  const AUTOSIZE_CONTAINER_SELECTOR = "ms-autosize-textarea";
  const TEXTAREA_SELECTOR = "ms-autosize-textarea textarea";
  const RUN_BUTTON_SELECTOR = 'run-button button[aria-label="Run"]';
  const STOP_BUTTON_SELECTOR = "run-button button.stoppable";
  const DEFAULT_CONTINUE_PROMPT = "continue";

  // --- 脚本状态变量 ---
  let isAutoContinueEnabled = false;
  let isPanelExpanded = false;
  let customContinuePrompt = DEFAULT_CONTINUE_PROMPT;
  let debugPanel = null;
  let continueButton = null;
  let toggleButton = null;
  let customPromptInput = null;
  let uiContainer = null;
  let scrollTimeout = null;

  // --- 启动逻辑 ---
  const readyCheck = setInterval(() => {
    const scrollContainer = document.querySelector(SCROLL_CONTAINER_SELECTOR);
    if (scrollContainer) {
      clearInterval(readyCheck);
      console.log("Gemini 自动续写脚本 v4.0 (Advanced UI) 已启动!");
      init(scrollContainer);
    }
  }, 1000);

  function init(scrollContainer) {
    createAdvancedUI();

    scrollContainer.addEventListener("scroll", () => {
      clearTimeout(scrollTimeout);
      scrollTimeout = setTimeout(() => {
        updateDebugInfoAndTrigger(scrollContainer);
      }, 100);
    });

    setInterval(syncUIState, 250);
    setTimeout(() => updateDebugInfoAndTrigger(scrollContainer), 500);
  }

  // --- 核心功能函数 ---
  function isCurrentlyFetching() {
    return !!document.querySelector(STOP_BUTTON_SELECTOR);
  }

  function syncUIState() {
    if (continueButton) {
      continueButton.disabled = isCurrentlyFetching();
    }
  }

  function performAutoContinue() {
    if (isCurrentlyFetching()) {
      console.log("正在等待AI响应,续写操作已跳过。");
      return;
    }

    const autosizeContainer = document.querySelector(
      AUTOSIZE_CONTAINER_SELECTOR
    );
    const textarea = document.querySelector(TEXTAREA_SELECTOR);
    const runButton = document.querySelector(RUN_BUTTON_SELECTOR);

    if (autosizeContainer && textarea && runButton) {
      console.log(`尝试执行续写,发送: "${customContinuePrompt}"`);

      autosizeContainer.setAttribute("data-value", customContinuePrompt);
      textarea.value = customContinuePrompt;
      textarea.dispatchEvent(
        new Event("input", { bubbles: true, composed: true })
      );

      setTimeout(() => {
        const finalRunButton = document.querySelector(RUN_BUTTON_SELECTOR);
        if (finalRunButton && !finalRunButton.disabled) {
          finalRunButton.click();
          console.log("消息已发送!");
        } else {
          console.error("发送失败:按钮在填充输入后仍然被禁用。");
        }
      }, 100);
    } else {
      console.warn("无法执行续写:缺少必要的UI组件。");
    }
  }

  function updateDebugInfoAndTrigger(container) {
    if (!debugPanel) return;
    const allTurns = container.querySelectorAll(MESSAGE_TURN_SELECTOR);
    const total = allTurns.length;
    if (total === 0) {
      debugPanel.textContent = "0/0";
      return;
    }

    let currentIndex = -1;
    const viewportTopThreshold = container.getBoundingClientRect().top + 60;
    for (let i = 0; i < allTurns.length; i++) {
      const rect = allTurns[i].getBoundingClientRect();
      if (rect.top >= viewportTopThreshold) {
        currentIndex = i;
        break;
      }
    }
    if (currentIndex === -1) currentIndex = total - 1;
    debugPanel.textContent = `${currentIndex + 1}/${total}`;

    const shouldTrigger =
      isAutoContinueEnabled && total > 1 && currentIndex + 1 >= total - 1;
    if (shouldTrigger && !isCurrentlyFetching()) {
      console.log(`自动续写条件满足:位置 ${currentIndex + 1}/${total}`);
      performAutoContinue();
    }
  }

  // --- SVG图标创建函数 ---
  function createSVGIcon(pathData, viewBox = "0 0 24 24") {
    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("width", "20");
    svg.setAttribute("height", "20");
    svg.setAttribute("viewBox", viewBox);
    svg.setAttribute("fill", "currentColor");

    const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
    path.setAttribute("d", pathData);

    svg.appendChild(path);
    return svg;
  }

  function createExpandIcon() {
    return createSVGIcon("M7 14l5-5 5 5z");
  }

  function createCollapseIcon() {
    return createSVGIcon("M7 10l5 5 5-5z");
  }

  function createSettingsIcon() {
    return createSVGIcon(
      "M12 15.5A3.5 3.5 0 0 1 8.5 12A3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66Z"
    );
  }

  function togglePanel() {
    isPanelExpanded = !isPanelExpanded;
    updatePanelState();
  }

  function updatePanelState() {
    const panel = document.getElementById("tampermonkey-panel");
    const toggleBtnIcon = document.getElementById("tampermonkey-toggle-icon");

    if (isPanelExpanded) {
      panel.style.display = "flex";
      // 清空并重新添加icon
      toggleBtnIcon.textContent = "";
      toggleBtnIcon.appendChild(createCollapseIcon());
      uiContainer.classList.add("expanded");
    } else {
      panel.style.display = "none";
      // 清空并重新添加icon
      toggleBtnIcon.textContent = "";
      toggleBtnIcon.appendChild(createSettingsIcon());
      uiContainer.classList.remove("expanded");
    }
  }

  // --- 高级UI创建 ---
  function createAdvancedUI() {
    // 主容器
    uiContainer = document.createElement("div");
    uiContainer.id = "tampermonkey-ui-container";
    uiContainer.className = "collapsed";
    document.body.appendChild(uiContainer);

    // 切换按钮(始终可见)
    toggleButton = document.createElement("button");
    toggleButton.id = "tampermonkey-toggle-btn";
    toggleButton.className = "toggle-button";
    toggleButton.setAttribute("title", "Toggle Gemini Assistant Panel");

    const toggleIcon = document.createElement("span");
    toggleIcon.id = "tampermonkey-toggle-icon";
    toggleIcon.appendChild(createSettingsIcon());
    toggleButton.appendChild(toggleIcon);

    toggleButton.addEventListener("click", togglePanel);
    uiContainer.appendChild(toggleButton);

    // 主面板容器(可折叠)
    const panel = document.createElement("div");
    panel.id = "tampermonkey-panel";
    panel.className = "main-panel";
    panel.style.display = "none";
    uiContainer.appendChild(panel);

    // 状态显示区域
    const statusSection = document.createElement("div");
    statusSection.className = "panel-section";

    const statusLabel = document.createElement("span");
    statusLabel.className = "section-label";
    statusLabel.textContent = "Position:";

    debugPanel = document.createElement("div");
    debugPanel.id = "tampermonkey-debug-panel";
    debugPanel.className = "status-display";
    debugPanel.textContent = "...";

    statusSection.appendChild(statusLabel);
    statusSection.appendChild(debugPanel);
    panel.appendChild(statusSection);

    // 控制按钮区域
    const controlSection = document.createElement("div");
    controlSection.className = "panel-section";

    // 手动继续按钮
    continueButton = document.createElement("button");
    continueButton.id = "tampermonkey-continue-btn";
    continueButton.className = "control-button primary";
    continueButton.textContent = "Continue";
    continueButton.addEventListener("click", () => {
      console.log("手动触发续写...");
      performAutoContinue();
    });

    // 自动续写开关
    const autoToggleLabel = document.createElement("label");
    autoToggleLabel.className = "switch-label";
    autoToggleLabel.setAttribute("title", "Auto-Continue Toggle");

    const autoContinueToggle = document.createElement("input");
    autoContinueToggle.type = "checkbox";
    autoContinueToggle.checked = isAutoContinueEnabled;
    autoContinueToggle.addEventListener("change", (e) => {
      isAutoContinueEnabled = e.target.checked;
      console.log(`自动续写已 ${isAutoContinueEnabled ? "开启" : "关闭"}`);
    });

    const switchSlider = document.createElement("span");
    switchSlider.className = "switch-slider";

    autoToggleLabel.appendChild(autoContinueToggle);
    autoToggleLabel.appendChild(switchSlider);

    controlSection.appendChild(continueButton);
    controlSection.appendChild(autoToggleLabel);
    panel.appendChild(controlSection);

    // 自定义提示词区域
    const promptSection = document.createElement("div");
    promptSection.className = "panel-section";

    const promptLabel = document.createElement("label");
    promptLabel.className = "section-label";
    promptLabel.setAttribute("for", "tampermonkey-prompt-input");
    promptLabel.textContent = "Custom Prompt:";

    customPromptInput = document.createElement("input");
    customPromptInput.id = "tampermonkey-prompt-input";
    customPromptInput.type = "text";
    customPromptInput.className = "prompt-input";
    customPromptInput.value = customContinuePrompt;
    customPromptInput.placeholder = "Enter custom continue prompt...";
    customPromptInput.addEventListener("input", (e) => {
      customContinuePrompt = e.target.value.trim() || DEFAULT_CONTINUE_PROMPT;
      console.log(`自定义提示词已更新: "${customContinuePrompt}"`);
    });

    promptSection.appendChild(promptLabel);
    promptSection.appendChild(customPromptInput);
    panel.appendChild(promptSection);

    // 添加样式
    GM_addStyle(`
      #tampermonkey-ui-container {
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 9999;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 12px;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      }

      .toggle-button {
        width: 48px;
        height: 48px;
        border: none;
        border-radius: 50%;
        background: linear-gradient(135deg, #1a73e8, #185abc);
        color: white;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 4px 12px rgba(26, 115, 232, 0.3);
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        backdrop-filter: blur(10px);
      }

      .toggle-button:hover {
        transform: translateY(-2px);
        box-shadow: 0 6px 20px rgba(26, 115, 232, 0.4);
        background: linear-gradient(135deg, #185abc, #1557a0);
      }

      .toggle-button:active {
        transform: translateY(0);
      }

      #tampermonkey-toggle-icon {
        display: flex;
        align-items: center;
        justify-content: center;
        transition: transform 0.3s ease;
      }

      .expanded #tampermonkey-toggle-icon {
        transform: rotate(180deg);
      }

      .main-panel {
        background: rgba(255, 255, 255, 0.95);
        backdrop-filter: blur(20px);
        border-radius: 16px;
        padding: 20px;
        min-width: 280px;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
        border: 1px solid rgba(255, 255, 255, 0.2);
        display: none;
        flex-direction: column;
        gap: 16px;
        animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
      }

      @keyframes slideIn {
        from {
          opacity: 0;
          transform: translateY(20px) scale(0.95);
        }
        to {
          opacity: 1;
          transform: translateY(0) scale(1);
        }
      }

      .panel-section {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }

      .section-label {
        font-size: 12px;
        font-weight: 600;
        color: #5f6368;
        text-transform: uppercase;
        letter-spacing: 0.5px;
      }

      .status-display {
        background: rgba(26, 115, 232, 0.1);
        color: #1a73e8;
        padding: 8px 12px;
        border-radius: 8px;
        font-family: 'SF Mono', 'Monaco', 'Cascadia Code', monospace;
        font-size: 14px;
        font-weight: 600;
        text-align: center;
        border: 1px solid rgba(26, 115, 232, 0.2);
      }

      .control-button {
        background: linear-gradient(135deg, #1a73e8, #185abc);
        color: white;
        border: none;
        padding: 12px 20px;
        border-radius: 10px;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        box-shadow: 0 2px 8px rgba(26, 115, 232, 0.3);
      }

      .control-button:hover {
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba(26, 115, 232, 0.4);
      }

      .control-button:disabled {
        background: #e0e0e0;
        color: #9e9e9e;
        cursor: not-allowed;
        transform: none;
        box-shadow: none;
      }

      .switch-label {
        position: relative;
        display: inline-block;
        width: 52px;
        height: 28px;
        cursor: pointer;
        align-self: flex-start;
      }

      .switch-label input {
        opacity: 0;
        width: 0;
        height: 0;
      }

      .switch-slider {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        border-radius: 28px;
        transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
      }

      .switch-slider:before {
        position: absolute;
        content: "";
        height: 20px;
        width: 20px;
        left: 4px;
        bottom: 4px;
        background-color: white;
        border-radius: 50%;
        transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
      }

      .switch-label input:checked + .switch-slider {
        background: linear-gradient(135deg, #1a73e8, #185abc);
      }

      .switch-label input:checked + .switch-slider:before {
        transform: translateX(24px);
      }

      .prompt-input {
        width: 100%;
        padding: 12px 16px;
        border: 2px solid #e0e0e0;
        border-radius: 10px;
        font-size: 14px;
        font-family: inherit;
        transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
        background: rgba(255, 255, 255, 0.8);
        box-sizing: border-box;
      }

      .prompt-input:focus {
        outline: none;
        border-color: #1a73e8;
        box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.1);
        background: white;
      }

      .prompt-input::placeholder {
        color: #9e9e9e;
        font-style: italic;
      }

      /* Dark mode support */
      @media (prefers-color-scheme: dark) {
        .main-panel {
          background: rgba(32, 33, 36, 0.95);
          border: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .section-label {
          color: #bdc1c6;
        }
        
        .prompt-input {
          background: rgba(32, 33, 36, 0.8);
          border-color: #5f6368;
          color: #e8eaed;
        }
        
        .prompt-input:focus {
          background: #32333;
          border-color: #1a73e8;
        }
      }
    `);

    // 初始化面板状态
    updatePanelState();
  }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。