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