Greasy Fork is available in English.
Gemini 助手:支持对话大纲(搜索/跳转/详情)、提示词管理(分类/分组/拖拽)、自动加宽页面、中文输入修复(企业版)、多语言支持,智能适配 Gemini 标准版/企业版/Genspark
当前为
// ==UserScript==
// @name gemini-helper
// @namespace http://tampermonkey.net/
// @version 1.7.0
// @description Gemini 助手:支持对话大纲(搜索/跳转/详情)、提示词管理(分类/分组/拖拽)、自动加宽页面、中文输入修复(企业版)、多语言支持,智能适配 Gemini 标准版/企业版/Genspark
// @author urzeye
// @note 参考 https://linux.do/t/topic/925110 的代码与UI布局拓展实现
// @match https://gemini.google.com/*
// @match https://business.gemini.google/*
// @match https://www.genspark.ai/agents*
// @match https://genspark.ai/agents*
// @icon https://raw.githubusercontent.com/gist/urzeye/8d1d3afbbcd0193dbc8a2019b1ba54d3/raw/f7113d329a259963ed1b1ab8cb981e8f635d4cea/gemini.svg
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-idle
// @supportURL https://github.com/urzeye/tampermonkey-scripts/issues
// @homepageURL https://github.com/urzeye/tampermonkey-scripts
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 防止重复初始化
if (window.geminiHelperInitialized) {
return;
}
window.geminiHelperInitialized = true;
// ==================== 设置项与多语言 ====================
const SETTING_KEYS = {
CLEAR_TEXTAREA_ON_SEND: 'gemini_business_clear_on_send',
LANGUAGE: 'gemini_language',
PAGE_WIDTH: 'gemini_page_width',
OUTLINE: 'gemini_outline_settings',
TAB_ORDER: 'gemini_tab_order',
};
// 默认 Tab 顺序
const DEFAULT_TAB_ORDER = ['prompts', 'outline', 'settings'];
// Tab 定义(用于渲染和显示)
const TAB_DEFINITIONS = {
'prompts': { id: 'prompts', labelKey: 'tabPrompts', icon: '📝' },
'outline': { id: 'outline', labelKey: 'tabOutline', icon: '📑' },
'settings': { id: 'settings', labelKey: 'tabSettings', icon: '⚙️' }
};
const I18N = {
'zh-CN': {
panelTitle: 'Gemini 助手',
tabPrompts: '提示词',
tabSettings: '设置',
searchPlaceholder: '搜索提示词...',
addPrompt: '添加新提示词',
allCategory: '全部',
manageCategory: '⚙ 管理',
currentPrompt: '当前提示词:',
scrollTop: '顶部',
scrollBottom: '底部',
refresh: '刷新',
collapse: '收起',
edit: '编辑',
delete: '删除',
copy: '复制',
drag: '拖动',
save: '保存',
cancel: '取消',
add: '添加',
title: '标题',
category: '分类',
categoryPlaceholder: '例如:编程、翻译',
content: '提示词内容',
editPrompt: '编辑提示词',
addNewPrompt: '添加新提示词',
fillTitleContent: '请填写标题和内容',
promptUpdated: '提示词已更新',
promptAdded: '提示词已添加',
deleted: '已删除',
copied: '已复制到剪贴板',
cleared: '已清除内容',
refreshed: '已刷新',
orderUpdated: '已更新排序',
inserted: '已插入提示词',
scrolling: '页面正在滚动,请稍后...',
noTextarea: '未找到输入框,请点击输入框后重试',
confirmDelete: '确定删除?',
// 设置面板
settingsTitle: '通用设置',
clearOnSendLabel: '发送后自动修复中文输入',
clearOnSendDesc: '发送消息后插入零宽字符,修复下次输入首字母问题(仅 Gemini Business)',
settingOn: '开',
settingOff: '关',
// 分类管理
categoryManage: '分类管理',
categoryEmpty: '暂无分类,添加提示词时会自动创建分类',
rename: '重命名',
newCategoryName: '请输入新的分类名称:',
categoryRenamed: '分类已重命名',
confirmDeleteCategory: '确定删除该分类吗?关联的提示词将移至"未分类"',
categoryDeleted: '分类已删除',
// 语言设置
languageLabel: '界面语言',
languageDesc: '设置面板显示语言,重新打开页面生效',
languageAuto: '跟随系统',
languageZhCN: '简体中文',
languageZhTW: '繁體中文',
languageEn: 'English',
// 页面宽度设置
pageWidthLabel: '页面宽度',
pageWidthDesc: '调整聊天页面的宽度,即时生效',
enablePageWidth: '启用页面加宽',
widthValue: '宽度值',
widthUnit: '单位',
unitPx: '像素 (px)',
unitPercent: '百分比 (%)',
// 大纲功能
tabOutline: '大纲',
outlineEmpty: '暂无大纲内容',
outlineRefresh: '刷新',
outlineSettings: '大纲设置',
enableOutline: '启用大纲',
outlineMaxLevel: '显示标题级别',
outlineLevelAll: '全部 (1-6级)',
outlineLevel1: '仅 1 级',
outlineLevel2: '至 2 级',
outlineLevel3: '至 3 级',
// 刷新按钮提示
refreshPrompts: '刷新提示词',
refreshOutline: '刷新大纲',
refreshSettings: '刷新设置',
// 大纲高级工具栏
outlineScrollBottom: '滚动到底部',
outlineScrollTop: '滚动到顶部',
outlineExpandAll: '展开全部',
outlineCollapseAll: '折叠全部',
outlineSearch: '搜索大纲...',
outlineSearchResult: '个结果',
outlineLevelHint: '级标题',
// Tab 顺序设置
tabOrderSettings: '界面排版',
tabOrderDesc: '调整面板 Tab 的显示顺序',
moveUp: '上移',
moveDown: '下移'
},
'zh-TW': {
panelTitle: 'Gemini 助手',
tabPrompts: '提示詞',
tabSettings: '設置',
searchPlaceholder: '搜尋提示詞...',
addPrompt: '新增提示詞',
allCategory: '全部',
manageCategory: '⚙ 管理',
currentPrompt: '當前提示詞:',
scrollTop: '頂部',
scrollBottom: '底部',
refresh: '刷新',
collapse: '收起',
edit: '編輯',
delete: '刪除',
copy: '複製',
drag: '拖動',
save: '保存',
cancel: '取消',
add: '新增',
title: '標題',
category: '分類',
categoryPlaceholder: '例如:程式設計、翻譯',
content: '提示詞內容',
editPrompt: '編輯提示詞',
addNewPrompt: '新增提示詞',
fillTitleContent: '請填寫標題和內容',
promptUpdated: '提示詞已更新',
promptAdded: '提示詞已新增',
deleted: '已刪除',
copied: '已複製到剪貼簿',
cleared: '已清除內容',
refreshed: '已刷新',
orderUpdated: '已更新排序',
inserted: '已插入提示詞',
scrolling: '頁面正在捲動,請稍後...',
noTextarea: '未找到輸入框,請點擊輸入框後重試',
confirmDelete: '確定刪除?',
// 設置面板
settingsTitle: '通用設置',
clearOnSendLabel: '發送後自動修復中文輸入',
clearOnSendDesc: '發送訊息後插入零寬字元,修復下次輸入首字母問題(僅 Gemini Business)',
settingOn: '開',
settingOff: '關',
// 分類管理
categoryManage: '分類管理',
categoryEmpty: '暫無分類,新增提示詞時會自動建立分類',
rename: '重新命名',
newCategoryName: '請輸入新的分類名稱:',
categoryRenamed: '分類已重新命名',
confirmDeleteCategory: '確定刪除該分類嗎?關聯的提示詞將移至「未分類」',
categoryDeleted: '分類已刪除',
// 語言設置
languageLabel: '介面語言',
languageDesc: '設定面板顯示語言,重新開啟頁面生效',
languageAuto: '跟隨系統',
languageZhCN: '简体中文',
languageZhTW: '繁體中文',
languageEn: 'English',
// 頁面寬度設置
pageWidthLabel: '頁面寬度',
pageWidthDesc: '調整聊天頁面的寬度,即時生效',
enablePageWidth: '啟用頁面加寬',
widthValue: '寬度值',
widthUnit: '單位',
unitPx: '像素 (px)',
unitPercent: '百分比 (%)',
// 大綱功能
tabOutline: '大綱',
outlineEmpty: '暫無大綱內容',
outlineRefresh: '刷新',
outlineSettings: '大綱設置',
enableOutline: '啟用大綱',
outlineMaxLevel: '顯示標題級別',
outlineLevelAll: '全部 (1-6級)',
outlineLevel1: '僅 1 級',
outlineLevel2: '至 2 級',
outlineLevel3: '至 3 級',
// 刷新按鈕提示
refreshPrompts: '刷新提示詞',
refreshOutline: '刷新大綱',
refreshSettings: '刷新設置',
// 大綱高級工具欄
outlineScrollBottom: '滾動到底部',
outlineScrollTop: '滾動到頂部',
outlineExpandAll: '展開全部',
outlineCollapseAll: '折疊全部',
outlineSearch: '搜尋大綱...',
outlineSearchResult: '個結果',
outlineLevelHint: '級標題',
// Tab 顺序设置
tabOrderSettings: '介面排版',
tabOrderDesc: '調整面板 Tab 的顯示順序',
moveUp: '上移',
moveDown: '下移'
},
'en': {
panelTitle: 'Gemini Helper',
tabPrompts: 'Prompts',
tabSettings: 'Settings',
searchPlaceholder: 'Search prompts...',
addPrompt: 'Add New Prompt',
allCategory: 'All',
manageCategory: '⚙ Manage',
currentPrompt: 'Current: ',
scrollTop: 'Top',
scrollBottom: 'Bottom',
refresh: 'Refresh',
collapse: 'Collapse',
edit: 'Edit',
delete: 'Delete',
copy: 'Copy',
drag: 'Drag',
save: 'Save',
cancel: 'Cancel',
add: 'Add',
title: 'Title',
category: 'Category',
categoryPlaceholder: 'e.g., Coding, Translation',
content: 'Prompt Content',
editPrompt: 'Edit Prompt',
addNewPrompt: 'Add New Prompt',
fillTitleContent: 'Please fill in title and content',
promptUpdated: 'Prompt updated',
promptAdded: 'Prompt added',
deleted: 'Deleted',
copied: 'Copied to clipboard',
cleared: 'Content cleared',
refreshed: 'Refreshed',
orderUpdated: 'Order updated',
inserted: 'Prompt inserted',
scrolling: 'Page is scrolling, please wait...',
noTextarea: 'Input not found, please click the input area first',
confirmDelete: 'Delete this prompt?',
// Settings panel
settingsTitle: 'General Settings',
clearOnSendLabel: 'Auto-fix Chinese input after send',
clearOnSendDesc: 'Insert zero-width char after send to fix first letter issue (Gemini Business only)',
settingOn: 'ON',
settingOff: 'OFF',
// Category management
categoryManage: 'Category Management',
categoryEmpty: 'No categories yet. Categories are created when you add prompts.',
rename: 'Rename',
newCategoryName: 'Enter new category name:',
categoryRenamed: 'Category renamed',
confirmDeleteCategory: 'Delete this category? Associated prompts will be moved to "Uncategorized"',
categoryDeleted: 'Category deleted',
// Language settings
languageLabel: 'Language',
languageDesc: 'Set panel display language, reload page to apply',
languageAuto: 'Auto',
languageZhCN: '简体中文',
languageZhTW: '繁體中文',
languageEn: 'English',
// Page width settings
pageWidthLabel: 'Page Width',
pageWidthDesc: 'Adjust chat page width, takes effect immediately',
enablePageWidth: 'Enable Page Widening',
widthValue: 'Width Value',
widthUnit: 'Unit',
unitPx: 'Pixels (px)',
unitPercent: 'Percentage (%)',
// Outline feature
tabOutline: 'Outline',
outlineEmpty: 'No outline content',
outlineRefresh: 'Refresh',
outlineSettings: 'Outline Settings',
enableOutline: 'Enable Outline',
outlineMaxLevel: 'Heading Levels',
outlineLevelAll: 'All (1-6)',
outlineLevel1: 'Level 1 only',
outlineLevel2: 'Up to Level 2',
outlineLevel3: 'Up to Level 3',
// Refresh button hints
refreshPrompts: 'Refresh Prompts',
refreshOutline: 'Refresh Outline',
refreshSettings: 'Refresh Settings',
// Outline advanced toolbar
outlineScrollBottom: 'Scroll to bottom',
outlineScrollTop: 'Scroll to top',
outlineExpandAll: 'Expand all',
outlineCollapseAll: 'Collapse all',
outlineSearch: 'Search outline...',
outlineSearchResult: 'result(s)',
outlineLevelHint: 'headings',
// Tab Order Settings
tabOrderSettings: 'Interface Layout',
tabOrderDesc: 'Adjust the display order of panel tabs',
moveUp: 'Move Up',
moveDown: 'Move Down'
}
};
// ============= 默认提示词库 =============
const DEFAULT_PROMPTS = [
{
id: 'default_1',
title: '代码优化',
content: '请帮我优化以下代码,提高性能和可读性:\n\n',
category: '编程'
},
{
id: 'default_2',
title: '翻译助手',
content: '请将以下内容翻译成中文,保持专业术语的准确性:\n\n',
category: '翻译'
},
];
// ============= 页面宽度默认配置 =============
const DEFAULT_WIDTH_SETTINGS = {
'gemini': { enabled: false, value: '70', unit: '%' },
'gemini-business': { enabled: false, value: '1600', unit: 'px' },
'genspark': { enabled: false, value: '70', unit: '%' }
};
// ============= 大纲功能默认配置 =============
const DEFAULT_OUTLINE_SETTINGS = {
enabled: true,
maxLevel: 6 // 显示到几级标题 (1-6)
};
// 语言检测函数(支持手动设置)
function detectLanguage() {
// 优先使用用户手动设置的语言
const savedLang = GM_getValue(SETTING_KEYS.LANGUAGE, 'auto');
if (savedLang !== 'auto' && I18N[savedLang]) {
return savedLang;
}
// 自动检测
const lang = navigator.language || navigator.userLanguage || 'en';
if (lang.startsWith('zh-TW') || lang.startsWith('zh-HK') || lang.startsWith('zh-Hant')) {
return 'zh-TW';
}
if (lang.startsWith('zh')) {
return 'zh-CN';
}
return 'en';
}
// ==================== 站点适配器模式 (Site Adapter Pattern) ====================
/**
* 站点适配器基类
* 添加新站点时,继承此类并实现所有抽象方法
*/
class SiteAdapter {
constructor() {
this.textarea = null;
}
/**
* 检测当前页面是否匹配该站点
* @returns {boolean}
*/
match() { throw new Error('必须实现 match()'); }
/**
* 返回站点标识符(用于配置存储)
* @returns {string}
*/
getSiteId() { throw new Error('必须实现 getSiteId()'); }
/**
* 返回站点显示名称
* @returns {string}
*/
getName() { throw new Error('必须实现 getName()'); }
/**
* 返回站点主题色
* @returns {{primary: string, secondary: string}}
*/
getThemeColors() { throw new Error('必须实现 getThemeColors()'); }
/**
* 返回需要加宽的CSS选择器列表
* @returns {Array<{selector: string, property: string}>}
*/
getWidthSelectors() { return []; }
/**
* 返回输入框选择器列表
* @returns {string[]}
*/
getTextareaSelectors() { return []; }
/**
* 获取提交按钮选择器,可以匹配ID、类名、属性等选择器
*
* @returns 提交按钮选择器
*/
getSubmitButtonSelectors() {
return [];
}
/**
* 查找输入框元素
* 默认实现:遍历选择器查找
* @returns {HTMLElement|null}
*/
findTextarea() {
for (const selector of this.getTextareaSelectors()) {
const elements = document.querySelectorAll(selector);
for (const element of elements) {
if (this.isValidTextarea(element)) {
this.textarea = element;
return element;
}
}
}
return null;
}
/**
* 验证输入框是否有效
* @param {HTMLElement} element
* @returns {boolean}
*/
isValidTextarea(element) {
return element.offsetParent !== null;
}
/**
* 向输入框插入内容
* @param {string} content
* @returns {Promise<boolean>|boolean}
*/
insertPrompt(content) { throw new Error('必须实现 insertPrompt()'); }
/**
* 清空输入框内容
*/
clearTextarea() {
if (this.textarea) {
this.textarea.value = '';
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
}
}
/**
* 获取滚动容器
* @returns {HTMLElement}
*/
getScrollContainer() {
// 1. 优先查找 Shadow DOM 中的滚动容器
const scrollContainerFromShadow = this.findScrollContainerInShadowDOM(document);
if (scrollContainerFromShadow) {
return scrollContainerFromShadow;
}
// 2. 尝试查找常见的滚动容器
const selectors = [
'.chat-mode-scroller',
'main',
'[role="main"]',
'.conversation-container',
'.chat-container'
];
for (const selector of selectors) {
const el = document.querySelector(selector);
if (el && el.scrollHeight > el.clientHeight) {
return el;
}
}
// 3. 回退到 document.documentElement 或 body
if (document.documentElement.scrollHeight > document.documentElement.clientHeight) {
return document.documentElement;
}
return document.body;
}
/**
* 在 Shadow DOM 中递归查找滚动容器
* @param {Node} root
* @param {number} depth
* @returns {HTMLElement|null}
*/
findScrollContainerInShadowDOM(root, depth = 0) {
if (depth > 10) return null;
const allElements = root.querySelectorAll('*');
for (const el of allElements) {
// 检查是否是可滚动元素
if (el.scrollHeight > el.clientHeight + 100) {
const style = window.getComputedStyle(el);
if (style.overflowY === 'auto' || style.overflowY === 'scroll' ||
style.overflow === 'auto' || style.overflow === 'scroll') {
return el;
}
}
// 递归检查 Shadow DOM
if (el.shadowRoot) {
const found = this.findScrollContainerInShadowDOM(el.shadowRoot, depth + 1);
if (found) return found;
}
}
return null;
}
/**
* 页面加载完成后执行
*/
afterPropertiesSet() {
}
/**
* 判断是否应该将样式注入到指定的 Shadow Host 中
* 用于解决 Shadow DOM 样式污染问题
*/
shouldInjectIntoShadow(host) {
return true;
}
/**
* 获取对话历史容器的选择器
* @returns {string} CSS 选择器
*/
getResponseContainerSelector() {
return '';
}
/**
* 从页面提取大纲(标题列表)
* @param {number} maxLevel 最大标题级别 (1-6)
* @returns {Array<{level: number, text: string, element: Element|null}>}
*/
extractOutline(maxLevel = 6) {
return [];
}
}
/**
* Gemini 适配器(gemini.google.com)
*/
class GeminiAdapter extends SiteAdapter {
match() {
return window.location.hostname.includes('gemini.google') &&
!window.location.hostname.includes('business.gemini.google');
}
getSiteId() { return 'gemini'; }
getName() { return 'Gemini'; }
getThemeColors() {
return { primary: '#4285f4', secondary: '#34a853' };
}
getWidthSelectors() {
return [
{ selector: '.conversation-container', property: 'max-width' },
{ selector: '.input-area-container', property: 'max-width' },
// 用户消息右对齐
{ selector: 'user-query', property: 'max-width', value: '100%', noCenter: true, extraCss: 'display: flex !important; justify-content: flex-end !important;' },
{ selector: '.user-query-container', property: 'max-width', value: '100%', noCenter: true, extraCss: 'justify-content: flex-end !important;' }
];
}
getTextareaSelectors() {
return [
'div[contenteditable="true"].ql-editor',
'div[contenteditable="true"]',
'[role="textbox"]',
'[aria-label*="Enter a prompt"]'
];
}
getSubmitButtonSelectors() {
return [
'button[aria-label*="Send"]',
'button[aria-label*="发送"]',
'.send-button',
'[data-testid*="send"]'
];
}
isValidTextarea(element) {
// 必须是可见的 contenteditable 元素
if (element.offsetParent === null) return false;
const isContentEditable = element.getAttribute('contenteditable') === 'true';
const isTextbox = element.getAttribute('role') === 'textbox';
// 排除脚本自身的 UI
if (element.closest('#gemini-helper-panel')) return false;
return (isContentEditable || isTextbox) || element.classList.contains('ql-editor');
}
insertPrompt(content) {
const editor = this.textarea;
if (!editor) return false;
editor.focus();
try {
// 先全选
document.execCommand('selectAll', false, null);
// 然后插入新内容
const success = document.execCommand('insertText', false, content);
if (!success) {
throw new Error('execCommand returned false');
}
} catch (e) {
// 降级方案:直接替换内容,不叠加
editor.textContent = content;
editor.dispatchEvent(new Event('input', { bubbles: true }));
editor.dispatchEvent(new Event('change', { bubbles: true }));
}
return true;
}
clearTextarea() {
if (this.textarea) {
this.textarea.focus();
document.execCommand('selectAll', false, null);
document.execCommand('delete', false, null);
}
}
getResponseContainerSelector() {
return 'infinite-scroller.chat-history';
}
extractOutline(maxLevel = 6) {
const outline = [];
const container = document.querySelector(this.getResponseContainerSelector());
if (!container) return outline;
// Gemini 使用标准的 h1-h6 标签,带有 data-path-to-node 属性
const headingSelectors = [];
for (let i = 1; i <= maxLevel; i++) {
headingSelectors.push(`h${i}`);
}
const headings = container.querySelectorAll(headingSelectors.join(', '));
headings.forEach(heading => {
const level = parseInt(heading.tagName.charAt(1), 10);
if (level <= maxLevel) {
outline.push({
level,
text: heading.textContent.trim(),
element: heading
});
}
});
return outline;
}
}
/**
* Gemini Business 适配器(business.gemini.google)
*/
class GeminiBusinessAdapter extends SiteAdapter {
match() {
return window.location.hostname.includes('business.gemini.google');
}
getSiteId() { return 'gemini-business'; }
getName() { return 'Enterprise'; }
getThemeColors() {
return { primary: '#4285f4', secondary: '#34a853' };
}
// 排除侧边栏 (mat-sidenav, mat-drawer) 中的 Shadow DOM
shouldInjectIntoShadow(host) {
if (host.closest('mat-sidenav') || host.closest('mat-drawer') || host.closest('[class*="bg-sidebar"]')) return false;
return true;
}
getWidthSelectors() {
// 辅助函数:生成带 scoped globalSelector 的配置
// noCenter: 不添加 margin-left/right: auto(用于容器类元素)
const config = (selector, value, extraCss, noCenter = false) => ({
selector,
globalSelector: `mat-sidenav-content ${selector}`, // 全局样式只针对主内容区
property: 'max-width',
value,
extraCss,
noCenter
});
return [
// 容器强制 100%,不需要居中(它们应该填充可用空间)
config('mat-sidenav-content', '100%', undefined, true),
config('.main.chat-mode', '100%', undefined, true),
// 内容区域跟随配置(需要居中)
config('ucs-summary'),
config('ucs-conversation'),
config('ucs-search-bar'),
config('.summary-container.expanded'),
config('.conversation-container'),
// 输入框容器:不居中,使用 left/right 定位
config('.input-area-container', undefined, 'left: 0 !important; right: 0 !important;', true)
];
}
getTextareaSelectors() {
return [
'div.ProseMirror',
'.ProseMirror',
'[contenteditable="true"]:not([type="search"])',
'[role="textbox"]',
'textarea:not([type="search"])'
];
}
getSubmitButtonSelectors() {
return [
'button[aria-label*="Submit"]',
'button[aria-label*="提交"]',
'.send-button',
'[data-testid*="send"]'
];
}
isValidTextarea(element) {
// 排除搜索框
if (element.type === 'search') return false;
if (element.classList.contains('main-input')) return false;
if (element.getAttribute('aria-label')?.includes('搜索')) return false;
if (element.placeholder?.includes('搜索')) return false;
// 排除脚本自己的 UI
if (element.classList.contains('prompt-search-input')) return false;
if (element.id === 'prompt-search') return false;
if (element.closest('#gemini-helper-panel')) return false;
// 必须是 contenteditable 或者 ProseMirror
const isVisible = element.offsetParent !== null;
const isContentEditable = element.getAttribute('contenteditable') === 'true';
const isProseMirror = element.classList.contains('ProseMirror');
return isVisible && (isContentEditable || isProseMirror || element.tagName === 'TEXTAREA');
}
findTextarea() {
// 优先在 Shadow DOM 中查找
const element = this.findInShadowDOM(document);
if (element) {
this.textarea = element;
return element;
}
return super.findTextarea();
}
findInShadowDOM(root, depth = 0) {
if (depth > 15) return null;
// 只在 Shadow Root 中搜索选择器(跳过主文档以避免匹配脚本 UI)
if (root !== document) {
for (const selector of this.getTextareaSelectors()) {
try {
const elements = root.querySelectorAll(selector);
for (const element of elements) {
if (this.isValidTextarea(element)) {
return element;
}
}
} catch (e) {
// 某些选择器可能在 Shadow DOM 中不支持
}
}
}
// 在所有 Shadow Root 中递归搜索
const allElements = root.querySelectorAll('*');
for (const el of allElements) {
if (el.shadowRoot) {
const found = this.findInShadowDOM(el.shadowRoot, depth + 1);
if (found) return found;
}
}
return null;
}
insertPrompt(content) {
return new Promise((resolve) => {
const tryInsert = () => {
// 重新获取一下,以防切页面后元素失效
const editor = this.textarea || this.findTextarea();
if (!editor) {
console.warn('GeminiBusinessAdapter: Editor not found during insert.');
resolve(false);
return;
}
this.textarea = editor; // 更新引用
editor.click();
editor.focus();
// 等待一小段时间后尝试插入
setTimeout(() => {
try {
// 先全选
document.execCommand('selectAll', false, null);
// 插入新内容
const success = document.execCommand('insertText', false, content);
if (!success) throw new Error('execCommand returned false');
resolve(true);
} catch (e) {
// 方法2: 直接操作 DOM (降级方案)
let p = editor.querySelector('p');
if (!p) {
p = document.createElement('p');
editor.appendChild(p);
}
p.textContent = content;
// 触发各种事件以通知 ProseMirror 更新
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: content
});
editor.dispatchEvent(inputEvent);
editor.dispatchEvent(new Event('change', { bubbles: true }));
// 尝试触发 keyup 事件
editor.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
resolve(true);
}
}, 100);
};
if (this.textarea && document.body.contains(this.textarea)) {
tryInsert();
} else {
// 轮询等待元素出现
let attempts = 0;
const maxAttempts = 15;
const checkInterval = setInterval(() => {
attempts++;
if (this.findTextarea()) {
clearInterval(checkInterval);
tryInsert();
} else if (attempts >= maxAttempts) {
clearInterval(checkInterval);
resolve(false);
}
}, 500);
}
});
}
clearTextarea() {
if (this.textarea) {
this.textarea.focus();
document.execCommand('selectAll', false, null);
// 插入零宽空格替换旧内容(修复中文输入首字母问题)
document.execCommand('insertText', false, '\u200B');
}
}
// 普通清空(不插入零宽字符)
clearTextareaNormal() {
if (this.textarea) {
this.textarea.focus();
document.execCommand('selectAll', false, null);
document.execCommand('delete', false, null);
}
}
afterPropertiesSet(clearOnInit = true) {
// fixed: gemini business 在使用中文输入时,首字母会自动转换为英文,多一个字母
// 根据 clearOnInit 参数决定是否在初始化时插入零宽字符
if (clearOnInit) {
this.clearTextarea();
}
}
getResponseContainerSelector() {
// Gemini Business 使用 Shadow DOM,返回空字符串表示需要特殊处理
return '';
}
extractOutline(maxLevel = 6) {
const outline = [];
// 在 Shadow DOM 中递归查找所有标题
this.findHeadingsInShadowDOM(document, outline, maxLevel, 0);
return outline;
}
// 在 Shadow DOM 中递归查找标题
findHeadingsInShadowDOM(root, outline, maxLevel, depth) {
if (depth > 15) return;
// 在当前层级查找标题(h1-h6)
if (root !== document) {
const headingSelector = Array.from({ length: maxLevel }, (_, i) => `h${i + 1}`).join(', ');
try {
const headings = root.querySelectorAll(headingSelector);
headings.forEach(heading => {
// 只匹配包含 data-markdown-start-index 的标题(排除 logo 等非 AI 回复内容)
const span = heading.querySelector('span[data-markdown-start-index]');
if (span) {
const level = parseInt(heading.tagName[1], 10);
const text = span.textContent.trim();
if (text) {
outline.push({ level, text, element: heading });
}
}
});
} catch (e) {
// 忽略选择器错误
}
}
// 递归查找 Shadow DOM
const allElements = root.querySelectorAll('*');
for (const el of allElements) {
if (el.shadowRoot) {
this.findHeadingsInShadowDOM(el.shadowRoot, outline, maxLevel, depth + 1);
}
}
}
}
/**
* Genspark 适配器(genspark.ai)
*/
class GensparkAdapter extends SiteAdapter {
match() {
return window.location.hostname.includes('genspark.ai');
}
getSiteId() { return 'genspark'; }
getName() { return 'Genspark'; }
getThemeColors() {
return { primary: '#667eea', secondary: '#764ba2' };
}
getWidthSelectors() {
// Genspark 暂时不实现加宽,预留接口
return [];
}
getTextareaSelectors() {
return [
'textarea[name="query"]',
'textarea.search-input',
'.textarea-wrapper textarea',
'textarea[placeholder*="Message"]'
];
}
getSubmitButtonSelectors() {
return [
'button[aria-label*="Send"]',
'button[aria-label*="发送"]',
'.send-button',
'[data-testid*="send"]'
];
}
insertPrompt(content) {
if (!this.textarea) return false;
const currentContent = this.textarea.value.trim();
this.textarea.value = currentContent ? (content + '\n\n' + currentContent) : (content + '\n\n');
this.adjustTextareaHeight();
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
this.textarea.focus();
return true;
}
adjustTextareaHeight() {
if (this.textarea) {
this.textarea.style.height = 'auto';
this.textarea.style.height = Math.min(this.textarea.scrollHeight, 200) + 'px';
}
}
clearTextarea() {
if (this.textarea) {
this.textarea.value = '';
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
this.adjustTextareaHeight();
}
}
}
/**
* 站点注册表
* 管理所有站点适配器,提供统一的访问接口
*/
class SiteRegistry {
constructor() {
this.adapters = [];
this.currentAdapter = null;
}
// 注册适配器
register(adapter) {
this.adapters.push(adapter);
}
// 检测并返回匹配的适配器
detect() {
for (const adapter of this.adapters) {
if (adapter.match()) {
this.currentAdapter = adapter;
return adapter;
}
}
return null;
}
// 获取当前适配器
getCurrent() {
return this.currentAdapter;
}
}
// ==================== 核心逻辑 ====================
// 安全的 HTML 创建函数
function createElementSafely(tag, properties = {}, textContent = '') {
const element = document.createElement(tag);
Object.keys(properties).forEach(key => {
if (key === 'className') {
element.className = properties[key];
} else if (key === 'style') {
element.setAttribute('style', properties[key]);
} else {
element.setAttribute(key, properties[key]);
}
});
if (textContent) element.textContent = textContent;
return element;
}
// 安全清空元素内容
function clearElementSafely(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
/**
* 页面宽度样式管理器
* 负责动态注入和移除页面宽度样式
*/
/**
* 页面宽度样式管理器
* 负责动态注入和移除页面宽度样式,支持 Shadow DOM
*/
class WidthStyleManager {
constructor(siteAdapter, widthConfig) {
this.siteAdapter = siteAdapter;
this.widthConfig = widthConfig;
this.styleElement = null;
this.processedShadowRoots = new WeakSet();
this.observer = null;
this.shadowCheckInterval = null;
}
apply() {
// 1. 处理主文档样式
if (this.styleElement) {
this.styleElement.remove();
this.styleElement = null;
}
const css = this.generateCSS();
if (this.widthConfig && this.widthConfig.enabled) {
this.styleElement = document.createElement('style');
this.styleElement.id = 'gemini-helper-width-styles';
this.styleElement.textContent = css;
document.head.appendChild(this.styleElement);
// 启动 Shadow DOM 注入逻辑
this.startShadowInjection(css);
} else {
// 如果禁用了,也要清理 Shadow DOM 中的样式
this.stopShadowInjection();
this.clearShadowStyles();
}
}
generateCSS() {
const globalWidth = `${this.widthConfig.value}${this.widthConfig.unit}`;
const selectors = this.siteAdapter.getWidthSelectors();
return selectors.map((config) => {
const { selector, globalSelector, property, value, extraCss, noCenter } = config;
const params = {
finalWidth: value || globalWidth,
targetSelector: globalSelector || selector, // 优先使用全局特定选择器
property,
extra: extraCss || '',
centerCss: noCenter ? '' : 'margin-left: auto !important; margin-right: auto !important;'
};
return `${params.targetSelector} { ${params.property}: ${params.finalWidth} !important; ${params.centerCss} ${params.extra} }`;
}).join('\n');
}
updateConfig(widthConfig) {
this.widthConfig = widthConfig;
this.apply();
}
// ============= Shadow DOM 支持 =============
startShadowInjection(css) {
// Shadow CSS 需要重新生成,因为不能使用带 ancestor 的 globalSelector
// Shadow DOM 内部必须使用原始 selector,但包含同样的样式规则
const shadowCss = this.generateShadowCSS();
// 立即执行一次全量检查
this.injectToAllShadows(shadowCss);
// 使用定时器定期检查
if (this.shadowCheckInterval) clearInterval(this.shadowCheckInterval);
this.shadowCheckInterval = setInterval(() => {
this.injectToAllShadows(shadowCss);
}, 1000);
}
generateShadowCSS() {
const globalWidth = `${this.widthConfig.value}${this.widthConfig.unit}`;
const selectors = this.siteAdapter.getWidthSelectors();
return selectors.map((config) => {
const { selector, property, value, extraCss, noCenter } = config;
// Shadow DOM 中只使用原始 selector (不带父级限定),靠 JS 过滤来保证安全
const finalWidth = value || globalWidth;
const extra = extraCss || '';
const centerCss = noCenter ? '' : 'margin-left: auto !important; margin-right: auto !important;';
return `${selector} { ${property}: ${finalWidth} !important; ${centerCss} ${extra} }`;
}).join('\n');
}
stopShadowInjection() {
if (this.shadowCheckInterval) {
clearInterval(this.shadowCheckInterval);
this.shadowCheckInterval = null;
}
}
injectToAllShadows(css) {
if (!document.body) return;
const walk = (root) => {
// 如果是 Element 且有 shadowRoot,注入样式
if (root.shadowRoot) {
this.injectToShadowRoot(root.shadowRoot, css);
walk(root.shadowRoot); // 递归遍历 Shadow DOM 内部
}
// 遍历子节点
const children = root.children || root.childNodes; // 兼容 ShadowRoot 和 Element
for (let i = 0; i < children.length; i++) {
walk(children[i]);
}
};
walk(document.body);
}
injectToShadowRoot(shadowRoot, css) {
// 检查是否应该注入到该 Shadow DOM(通过 Adapter 过滤,例如排除侧边栏)
if (shadowRoot.host && !this.siteAdapter.shouldInjectIntoShadow(shadowRoot.host)) {
return;
}
if (this.processedShadowRoots.has(shadowRoot)) {
// 即使已处理过,也要检查样式内容是否需要更新(如果是配置变更)
const existingStyle = shadowRoot.getElementById('gemini-helper-width-shadow-style');
if (existingStyle && existingStyle.textContent !== css) {
existingStyle.textContent = css;
}
return;
}
try {
const style = document.createElement('style');
style.id = 'gemini-helper-width-shadow-style';
style.textContent = css;
shadowRoot.appendChild(style);
this.processedShadowRoots.add(shadowRoot);
} catch (e) {
// 忽略 closed shadow root 错误(虽然我们通常拿不到 closed 的引用)
}
}
clearShadowStyles() {
if (!document.body) return;
const walk = (root) => {
if (root.shadowRoot) {
const style = root.shadowRoot.getElementById('gemini-helper-width-shadow-style');
if (style) style.remove();
this.processedShadowRoots.delete(root.shadowRoot);
walk(root.shadowRoot);
}
const children = root.children || root.childNodes;
for (let i = 0; i < children.length; i++) {
walk(children[i]);
}
};
walk(document.body);
}
}
// ==================== 核心管理类 ====================
/**
* 通用大纲管理器
* 负责大纲的 UI 渲染、交互和状态管理
* 数据源由外部适配器提供
*/
class OutlineManager {
constructor(config) {
this.container = config.container;
this.settings = config.settings;
this.onSettingsChange = config.onSettingsChange;
this.t = config.i18n || ((k) => k);
this.state = {
tree: null,
treeKey: '',
minLevel: 1,
expandLevel: this.settings.outline?.maxLevel || 6,
levelCounts: {},
isAllExpanded: false,
rawOutline: [],
// 搜索相关状态
searchQuery: '',
searchLevelManual: false, // 标记用户是否在搜索时手动调整了层级
searchResults: null, // 存储搜索匹配信息 { matchedIds: Set, relevantIds: Set }
preSearchState: null, // 搜索前的状态快照
};
this.init();
}
init() {
this.createUI();
}
createUI() {
const container = this.container;
clearElementSafely(container);
const content = createElementSafely('div', { className: 'outline-content' });
// 固定工具栏
const toolbar = createElementSafely('div', { className: 'outline-fixed-toolbar' });
// 第一行:按钮和搜索占位
const row1 = createElementSafely('div', { className: 'outline-toolbar-row' });
// 滚动按钮
const scrollBtn = createElementSafely('button', {
className: 'outline-toolbar-btn',
id: 'outline-scroll-btn',
title: this.t('outlineScrollBottom')
}, '⬇');
scrollBtn.addEventListener('click', () => this.scrollList());
row1.appendChild(scrollBtn);
// 展开/折叠按钮
const expandBtn = createElementSafely('button', {
className: 'outline-toolbar-btn',
id: 'outline-expand-btn',
title: this.t('outlineExpandAll')
}, '⊕');
expandBtn.addEventListener('click', () => this.toggleExpandAll());
row1.appendChild(expandBtn);
// 搜索框区域
const searchWrapper = createElementSafely('div', { className: 'outline-search-wrapper' });
const searchInput = createElementSafely('input', {
type: 'text',
className: 'outline-search-input',
placeholder: this.t('outlineSearch'),
value: this.state.searchQuery
});
const clearBtn = createElementSafely('button', {
className: 'outline-search-clear hidden',
title: this.t('clear')
}, '×');
// 搜索事件处理
let debounceTimer;
searchInput.addEventListener('input', (e) => {
const val = e.target.value;
clearBtn.classList.toggle('hidden', !val);
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
this.handleSearch(val.trim());
}, 300);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
clearBtn.classList.add('hidden');
this.handleSearch('');
searchInput.blur();
}
});
clearBtn.addEventListener('click', () => {
searchInput.value = '';
clearBtn.classList.add('hidden');
this.handleSearch('');
searchInput.focus();
});
searchWrapper.appendChild(searchInput);
searchWrapper.appendChild(clearBtn);
row1.appendChild(searchWrapper);
toolbar.appendChild(row1);
// 第二行:层级滑块
const row2 = createElementSafely('div', { className: 'outline-toolbar-row' });
const sliderContainer = createElementSafely('div', { className: 'outline-level-slider-container' });
// 层级节点
const dotsContainer = createElementSafely('div', { className: 'outline-level-dots', id: 'outline-level-dots' });
const levelLine = createElementSafely('div', { className: 'outline-level-line' });
const levelProgress = createElementSafely('div', { className: 'outline-level-progress', id: 'outline-level-progress' });
levelLine.appendChild(levelProgress);
dotsContainer.appendChild(levelLine);
// 创建 6 个层级节点(0 表示不展开,1-6 表示层级)
for (let i = 0; i <= 6; i++) {
const dot = createElementSafely('div', {
className: `outline-level-dot ${i <= (this.state.expandLevel) ? 'active' : ''}`,
'data-level': i
});
const tooltip = createElementSafely('div', { className: 'outline-level-dot-tooltip' });
if (i === 0) {
tooltip.textContent = '⊖'; // 不展开
} else {
tooltip.textContent = `H${i}: 0`;
}
dot.appendChild(tooltip);
dot.addEventListener('click', () => this.setLevel(i));
dotsContainer.appendChild(dot);
}
sliderContainer.appendChild(dotsContainer);
row2.appendChild(sliderContainer);
toolbar.appendChild(row2);
content.appendChild(toolbar);
// 搜索结果统计条 (插入在工具栏和列表之间)
const resultBar = createElementSafely('div', {
className: 'outline-result-bar hidden',
id: 'outline-result-bar'
});
content.appendChild(resultBar);
// 大纲列表包装器(可滚动)
const listWrapper = createElementSafely('div', { className: 'outline-list-wrapper', id: 'outline-list-wrapper' });
const list = createElementSafely('div', { className: 'outline-list', id: 'outline-list' });
listWrapper.appendChild(list);
content.appendChild(listWrapper);
container.appendChild(content);
}
// 刷新数据
update(outlineData) {
const listContainer = document.getElementById('outline-list');
if (!listContainer) return;
clearElementSafely(listContainer);
if (!outlineData || outlineData.length === 0) {
listContainer.appendChild(createElementSafely('div', { className: 'outline-empty' }, this.t('outlineEmpty')));
return;
}
// 保存原始大纲
this.state.rawOutline = outlineData;
// 统计各层级数量
this.state.levelCounts = {};
outlineData.forEach(item => {
this.state.levelCounts[item.level] = (this.state.levelCounts[item.level] || 0) + 1;
});
this.updateTooltips();
// 智能缩进:检测最高层级
const minLevel = Math.min(...outlineData.map(item => item.level));
this.state.minLevel = minLevel;
// 构建树形结构
const outlineKey = outlineData.map(i => i.text).join('|');
let isNewTree = false;
if (this.state.treeKey !== outlineKey || !this.state.tree) {
this.state.tree = this.buildTree(outlineData, minLevel);
this.state.treeKey = outlineKey;
isNewTree = true;
}
const tree = this.state.tree;
// 如果是新树,且不在搜索模式下,初始化折叠状态
if (isNewTree && !this.state.searchQuery) {
const displayLevel = this.state.expandLevel ?? 6;
this.initializeCollapsedState(tree, displayLevel < minLevel ? minLevel : displayLevel);
}
// 如果在搜索模式,需要重新应用搜索标记
if (this.state.searchQuery) {
this.performSearch(this.state.searchQuery, false); // false = 不触发额外刷新
}
// 渲染
this.refreshCurrent();
}
// 处理搜索输入
handleSearch(query) {
if (!query) {
// === 结束搜索 ===
// 1. 清理搜索状态
this.state.searchQuery = '';
this.state.searchResults = null;
this.state.searchLevelManual = false;
// 2. 隐藏结果条
const resultBar = document.getElementById('outline-result-bar');
if (resultBar) resultBar.classList.add('hidden');
// 3. 恢复折叠状态
if (this.state.tree) {
// 3.1 先重置为全局设定的层级状态(兜底)
const displayLevel = this.state.expandLevel ?? 6;
this.clearForceExpandedState(this.state.tree, displayLevel);
// 3.2 如果有搜索前的状态快照,则恢复它(覆盖默认状态)
if (this.state.preSearchState) {
this.restoreTreeState(this.state.tree, this.state.preSearchState);
this.state.preSearchState = null; // 恢复后清除快照
}
}
this.refreshCurrent();
return;
}
// === 开始或更新搜索 ===
// 如果是从无搜索状态进入搜索状态,保存当前快照
if (!this.state.searchQuery && this.state.tree) {
this.state.preSearchState = {};
this.captureTreeState(this.state.tree, this.state.preSearchState);
// Fix Issue 2: 搜索前重置所有状态(折叠所有 + 清除手动展开标记)
// 这样搜索结果就只展示匹配的路径,不会受之前手动展开的干扰
this.clearForceExpandedState(this.state.tree, 0);
}
this.state.searchQuery = query;
this.state.searchLevelManual = false; // 重置手动层级标记
this.performSearch(query);
this.refreshCurrent();
}
// 执行搜索计算
performSearch(query, updateUI = true) {
if (!this.state.tree) return;
const normalize = (str) => str.toLowerCase();
const normalizedQuery = normalize(query);
let matchCount = 0;
// 递归标记树
// 返回值: { isMatch: boolean, hasMatchedDescendant: boolean }
const traverse = (nodes) => {
let hasAnyMatch = false;
nodes.forEach(node => {
const isMatch = normalize(node.text).includes(normalizedQuery);
if (isMatch) matchCount++;
node.isMatch = isMatch;
if (node.children && node.children.length > 0) {
const childResult = traverse(node.children);
node.hasMatchedDescendant = childResult;
} else {
node.hasMatchedDescendant = false;
}
// 如果有匹配子项,自动展开
if (node.hasMatchedDescendant) {
node.collapsed = false;
// node.forceExpanded = true; // 可选:是否强制标记为展开? 暂时不需要,只要 collapsed=false 即可
}
if (isMatch || node.hasMatchedDescendant) {
hasAnyMatch = true;
}
});
return hasAnyMatch;
};
traverse(this.state.tree);
// 更新结果条
if (updateUI) {
const resultBar = document.getElementById('outline-result-bar');
if (resultBar) {
resultBar.textContent = `${matchCount} ${this.t('outlineSearchResult')}`;
resultBar.classList.remove('hidden');
}
}
}
// 内部刷新(用于交互更新)
refreshCurrent() {
const listContainer = document.getElementById('outline-list');
if (this.state.tree && listContainer) {
clearElementSafely(listContainer);
// 确定当前的显示层级上限
// 如果在搜索模式且未手动调整,显示所有层级 (Infinity)
// 否则使用设定的 expandLevel
let displayLevel;
if (this.state.searchQuery && !this.state.searchLevelManual) {
displayLevel = 100; // 足够大以显示所有
} else {
displayLevel = this.state.expandLevel ?? 6;
}
if (displayLevel < this.state.minLevel) {
displayLevel = this.state.minLevel;
}
this.renderItems(listContainer, this.state.tree, this.state.minLevel, displayLevel);
}
}
// 构建树形结构
buildTree(outline, minLevel) {
const tree = [];
const stack = [];
outline.forEach((item, index) => {
const relativeLevel = item.level - minLevel + 1;
const node = {
...item,
relativeLevel,
index,
children: [],
collapsed: false
};
// 找到父节点
while (stack.length > 0 && stack[stack.length - 1].relativeLevel >= relativeLevel) {
stack.pop();
}
if (stack.length === 0) {
tree.push(node);
} else {
stack[stack.length - 1].children.push(node);
}
stack.push(node);
});
return tree;
}
// 渲染大纲项
renderItems(container, items, minLevel, displayLevel, parentCollapsed = false, parentForceExpanded = false) {
items.forEach(item => {
const hasChildren = item.children && item.children.length > 0;
const isTopLevel = item.level === minLevel;
let shouldShow;
// 计算可见性
const isLevelAllowed = item.level <= displayLevel || parentForceExpanded;
if (isTopLevel) {
// 顶层节点逻辑
if (this.state.searchQuery) {
// Fix: 搜索模式下严控顶层显示,无论是否有手动层级操作
// 确保 Expand All 不会将不相关的顶层节点展示出来
shouldShow = item.isMatch || item.hasMatchedDescendant;
} else {
// 普通模式:只需存在即可
shouldShow = true;
}
} else {
// 非顶层节点
const isRelevant = !this.state.searchQuery || (item.isMatch || item.hasMatchedDescendant || parentForceExpanded);
// 注意:parentForceExpanded 意味着父级被手动点开了,此时应该显示子级(即使不匹配)
// 综合判断
if (this.state.searchQuery && !this.state.searchLevelManual) {
// 纯搜索模式:相关即显示,忽略层级
// 但如果 parentForceExpanded,也显示
shouldShow = isRelevant && !parentCollapsed;
} else if (this.state.searchQuery && this.state.searchLevelManual) {
// 搜索且有层级限制
// 必须相关 AND 层级允许
shouldShow = isRelevant && isLevelAllowed && !parentCollapsed;
} else {
// 普通模式
shouldShow = isLevelAllowed && !parentCollapsed;
}
}
// 最终修正:如果父级折叠了,那肯定看不到
if (parentCollapsed) shouldShow = false;
const itemEl = createElementSafely('div', {
className: `outline-item outline-level-${item.relativeLevel}`,
'data-index': item.index,
'data-level': item.relativeLevel
});
const isExpanded = hasChildren && !item.collapsed;
const toggle = createElementSafely('span', {
className: `outline-item-toggle ${hasChildren ? (isExpanded ? 'expanded' : '') : 'invisible'}`
}, '▸');
if (hasChildren) {
toggle.addEventListener('click', (e) => {
e.stopPropagation();
item.collapsed = !item.collapsed;
if (!item.collapsed) {
item.forceExpanded = true;
}
toggle.classList.toggle('expanded', !item.collapsed);
this.refreshCurrent();
});
}
itemEl.appendChild(toggle);
const textEl = createElementSafely('span', { className: 'outline-item-text' });
// 高亮处理
if (this.state.searchQuery && item.isMatch) {
try {
const query = this.state.searchQuery;
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapedQuery})`, 'gi');
const parts = item.text.split(regex);
textEl.innerHTML = '';
parts.forEach(part => {
if (part.toLowerCase() === query.toLowerCase()) {
const mark = document.createElement('mark');
mark.textContent = part;
mark.style.backgroundColor = 'rgba(255, 235, 59, 0.5)';
mark.style.color = 'inherit';
mark.style.padding = '0';
mark.style.borderRadius = '2px';
textEl.appendChild(mark);
} else {
textEl.appendChild(document.createTextNode(part));
}
});
} catch (e) {
textEl.textContent = item.text;
}
} else {
textEl.textContent = item.text;
}
itemEl.appendChild(textEl);
itemEl.addEventListener('click', () => {
if (item.element) {
item.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
item.element.classList.add('outline-highlight');
setTimeout(() => item.element.classList.remove('outline-highlight'), 2000);
}
});
if (!shouldShow) {
itemEl.classList.add('outline-hidden');
}
container.appendChild(itemEl);
if (hasChildren) {
const childParentCollapsed = item.collapsed || parentCollapsed;
this.renderItems(
container,
item.children,
minLevel,
displayLevel,
childParentCollapsed,
item.forceExpanded || parentForceExpanded
);
}
});
}
// 初始化树的折叠状态
initializeCollapsedState(items, displayLevel) {
items.forEach(item => {
if (item.children && item.children.length > 0) {
const allChildrenHidden = item.children.every(child => child.level > displayLevel);
item.collapsed = allChildrenHidden;
this.initializeCollapsedState(item.children, displayLevel);
} else {
item.collapsed = false;
}
});
}
// 滚动列表
scrollList() {
const wrapper = document.getElementById('outline-list-wrapper');
const btn = document.getElementById('outline-scroll-btn');
if (!wrapper || !btn) return;
const isAtBottom = wrapper.scrollTop + wrapper.clientHeight >= wrapper.scrollHeight - 10;
if (isAtBottom) {
wrapper.scrollTo({ top: 0, behavior: 'smooth' });
btn.textContent = '⬇';
btn.title = this.t('outlineScrollBottom');
} else {
wrapper.scrollTo({ top: wrapper.scrollHeight, behavior: 'smooth' });
btn.textContent = '⬆';
btn.title = this.t('outlineScrollTop');
}
}
// 展开/折叠全部
toggleExpandAll() {
const btn = document.getElementById('outline-expand-btn');
if (!btn) return;
if (this.state.isAllExpanded) {
const minLevel = this.state.minLevel || 1;
this.setLevel(minLevel);
} else {
const maxActualLevel = Math.max(...Object.keys(this.state.levelCounts).map(Number), 1);
this.setLevel(maxActualLevel);
}
}
// 设置层级
setLevel(level) {
this.state.expandLevel = level;
// 更新外部设置
if (this.settings.outline) {
this.settings.outline.maxLevel = level;
if (this.onSettingsChange) this.onSettingsChange();
}
// 清除强制展开状态
if (this.state.tree) {
this.clearForceExpandedState(this.state.tree, level);
}
// 更新 UI
const dots = document.querySelectorAll('.outline-level-dot');
dots.forEach(dot => {
const dotLevel = parseInt(dot.dataset.level, 10);
dot.classList.toggle('active', dotLevel <= level);
});
const progress = document.getElementById('outline-level-progress');
if (progress) {
progress.style.width = `${(level / 6) * 100}%`;
}
// 如果在搜索状态下调整了 Slider,标记为手动
if (this.state.searchQuery) {
this.state.searchLevelManual = true;
this.refreshCurrent();
} else {
// 非搜索状态,这里可能不需要 refreshCurrent,因为 updateTooltips 或其他地方可能触发?
// 原有逻辑似乎没有显式调用 refreshCurrent,可能是 toggleExpnadAll 调用的?
// 不,setLevel 是被点击调用的。所以必须刷新。
this.refreshCurrent();
}
const btn = document.getElementById('outline-expand-btn');
const maxActualLevel = Math.max(...Object.keys(this.state.levelCounts).map(Number), 1);
if (btn) {
if (level >= maxActualLevel) {
btn.textContent = '⊖';
btn.title = this.t('outlineCollapseAll');
this.state.isAllExpanded = true;
} else {
btn.textContent = '⊕';
btn.title = this.t('outlineExpandAll');
this.state.isAllExpanded = false;
}
}
this.refreshCurrent();
}
// 清除强制展开状态
clearForceExpandedState(items, displayLevel) {
items.forEach(item => {
item.forceExpanded = false;
if (item.children && item.children.length > 0) {
const allChildrenHidden = item.children.every(child => child.level > displayLevel);
item.collapsed = allChildrenHidden;
this.clearForceExpandedState(item.children, displayLevel);
} else {
item.collapsed = false;
}
});
}
// 更新提示
updateTooltips() {
const dots = document.querySelectorAll('.outline-level-dot');
dots.forEach(dot => {
const level = parseInt(dot.dataset.level, 10);
const tooltip = dot.querySelector('.outline-level-dot-tooltip');
if (tooltip && level > 0) {
const count = this.state.levelCounts[level] || 0;
tooltip.textContent = `H${level}: ${count}`;
}
});
}
// 捕获树的状态(expanded/collapsed)
captureTreeState(nodes, stateMap) {
nodes.forEach(node => {
// 使用 level + text 作为 key
// 注意:如果有完全相同的标题在同一级,可能会冲突,但在当前场景下可以接受
const key = `${node.level}_${node.text}`;
stateMap[key] = {
collapsed: node.collapsed,
forceExpanded: node.forceExpanded
};
if (node.children && node.children.length > 0) {
this.captureTreeState(node.children, stateMap);
}
});
}
// 恢复树的状态
restoreTreeState(nodes, stateMap) {
nodes.forEach(node => {
const key = `${node.level}_${node.text}`;
const state = stateMap[key];
if (state) {
node.collapsed = state.collapsed;
// 只有当明确标记为 forceExpanded 时才恢复它
if (state.forceExpanded !== undefined) {
node.forceExpanded = state.forceExpanded;
}
}
if (node.children && node.children.length > 0) {
this.restoreTreeState(node.children, stateMap);
}
});
}
}
/**
* Gemini 助手核心类
* 管理提示词、设置和 UI 界面
*/
class GeminiHelper {
constructor(siteAdapter) {
this.prompts = this.loadPrompts();
this.selectedPrompt = null;
this.isCollapsed = false;
this.siteAdapter = siteAdapter;
this.isScrolling = false; // 滚动状态锁
this.lang = detectLanguage(); // 当前语言
this.i18n = I18N[this.lang]; // 当前语言文本
this.settings = this.loadSettings(); // 加载设置
// 初始化当前 Tab:优先使用设置的第一个 Tab
this.currentTab = this.settings.tabOrder && this.settings.tabOrder.length > 0
? this.settings.tabOrder[0]
: 'prompts';
// 兜底:如果首个 Tab 是 outline 且被禁用,则回退到 prompts
if (this.currentTab === 'outline' && !this.settings.outline?.enabled) {
this.currentTab = 'prompts';
}
this.outlineManager = null;
this.init();
}
// 获取翻译文本
t(key) {
return this.i18n[key] || key;
}
loadPrompts() {
const saved = GM_getValue('universal_prompts', null);
if (!saved) {
GM_setValue('universal_prompts', DEFAULT_PROMPTS);
return DEFAULT_PROMPTS;
}
return saved;
}
savePrompts() {
GM_setValue('universal_prompts', this.prompts);
}
// 加载设置
loadSettings() {
const widthSettings = GM_getValue(SETTING_KEYS.PAGE_WIDTH, DEFAULT_WIDTH_SETTINGS);
const outlineSettings = GM_getValue(SETTING_KEYS.OUTLINE, DEFAULT_OUTLINE_SETTINGS);
const tabOrder = GM_getValue(SETTING_KEYS.TAB_ORDER, DEFAULT_TAB_ORDER);
return {
clearTextareaOnSend: GM_getValue(SETTING_KEYS.CLEAR_TEXTAREA_ON_SEND, false), // 默认关闭
pageWidth: widthSettings[this.siteAdapter.getSiteId()] || DEFAULT_WIDTH_SETTINGS[this.siteAdapter.getSiteId()],
outline: outlineSettings,
tabOrder: tabOrder
};
}
// 保存设置
saveSettings() {
GM_setValue(SETTING_KEYS.CLEAR_TEXTAREA_ON_SEND, this.settings.clearTextareaOnSend);
// 保存页面宽度设置
const allWidthSettings = GM_getValue(SETTING_KEYS.PAGE_WIDTH, DEFAULT_WIDTH_SETTINGS);
allWidthSettings[this.siteAdapter.getSiteId()] = this.settings.pageWidth;
GM_setValue(SETTING_KEYS.PAGE_WIDTH, allWidthSettings);
// 保存大纲设置
GM_setValue(SETTING_KEYS.OUTLINE, this.settings.outline);
// 保存 Tab 顺序
GM_setValue(SETTING_KEYS.TAB_ORDER, this.settings.tabOrder);
}
addPrompt(prompt) {
prompt.id = 'custom_' + Date.now();
this.prompts.push(prompt);
this.savePrompts();
this.refreshPromptList();
this.refreshCategories();
}
updatePrompt(id, updatedPrompt) {
const index = this.prompts.findIndex(p => p.id === id);
if (index !== -1) {
this.prompts[index] = { ...this.prompts[index], ...updatedPrompt };
this.savePrompts();
this.refreshPromptList();
this.refreshCategories();
}
}
deletePrompt(id) {
this.prompts = this.prompts.filter(p => p.id !== id);
this.savePrompts();
this.refreshPromptList();
}
getCategories() {
const categories = new Set();
this.prompts.forEach(p => {
if (p.category) categories.add(p.category);
});
return Array.from(categories);
}
init() {
this.createStyles();
this.createUI();
this.bindEvents();
this.siteAdapter.findTextarea();
// 对于 Gemini Business,根据设置决定是否在初始化时插入零宽字符
const shouldClearOnInit = this.siteAdapter instanceof GeminiBusinessAdapter
? this.settings.clearTextareaOnSend
: false;
this.siteAdapter.afterPropertiesSet(shouldClearOnInit);
// 创建并应用页面宽度样式
this.widthStyleManager = new WidthStyleManager(this.siteAdapter, this.settings.pageWidth);
this.widthStyleManager.apply();
// 如果初始 Tab 是大纲,立即刷新内容
if (this.currentTab === 'outline') {
// 稍微延迟一下,确保 DOM 已经就绪
setTimeout(() => this.refreshOutline(), 500);
}
}
createStyles() {
const existingStyle = document.getElementById('gemini-helper-styles');
if (existingStyle) existingStyle.remove();
const colors = this.siteAdapter.getThemeColors();
const gradient = `linear-gradient(135deg, ${colors.primary} 0%, ${colors.secondary} 100%)`;
const style = document.createElement('style');
style.id = 'gemini-helper-styles';
style.textContent = `
/* 主面板样式 */
#gemini-helper-panel {
position: fixed;
top: 50%;
right: 20px;
transform: translateY(-50%);
width: 320px;
height: 80vh;
min-height: 600px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.15);
z-index: 999999;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
transition: all 0.3s ease;
border: 1px solid #e0e0e0;
}
#gemini-helper-panel.collapsed { display: none; }
.prompt-panel-header {
padding: 16px;
background: ${gradient};
color: white;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
}
.prompt-panel-title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 6px; white-space: nowrap; flex-shrink: 0; }
.site-indicator { font-size: 10px; padding: 2px 5px; background: rgba(255,255,255,0.2); border-radius: 4px; margin-left: 4px; white-space: nowrap; }
.prompt-panel-controls { display: flex; gap: 8px; }
.prompt-panel-btn {
background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px;
border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center;
transition: all 0.2s; font-size: 14px;
}
.prompt-panel-btn:hover { background: rgba(255,255,255,0.3); transform: scale(1.1); }
.prompt-search-bar { padding: 12px; border-bottom: 1px solid #e5e7eb; background: #f9fafb; }
.prompt-search-input {
width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 14px;
transition: all 0.2s; box-sizing: border-box;
}
.prompt-search-input:focus { outline: none; border-color: ${colors.primary}; }
.prompt-categories { padding: 8px 12px; display: flex; gap: 6px; flex-wrap: wrap; background: white; border-bottom: 1px solid #e5e7eb; }
.category-tag {
padding: 4px 10px; background: #f3f4f6; border-radius: 12px; font-size: 12px; color: #4b5563;
cursor: pointer; transition: all 0.2s; border: 1px solid transparent;
}
.category-tag:hover { background: #e5e7eb; }
.category-tag.active {
background: ${colors.primary}; color: white; border-color: ${colors.primary};
}
.prompt-list { flex: 1; overflow-y: auto; padding: 8px; }
.prompt-item {
background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 8px;
cursor: pointer; transition: all 0.2s; position: relative;
}
.prompt-item:hover {
border-color: ${colors.primary};
box-shadow: 0 4px 12px rgba(66,133,244,0.15);
transform: translateY(-2px);
}
.prompt-item.selected {
background: linear-gradient(135deg, #e8f0fe 0%, #f1f8e9 100%);
border-color: ${colors.primary};
}
.prompt-item-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px; }
.prompt-item-title { font-weight: 600; font-size: 14px; color: #1f2937; flex: 1; }
.prompt-item-category { font-size: 11px; padding: 2px 6px; background: #f3f4f6; border-radius: 4px; color: #6b7280; }
.prompt-item-content { font-size: 13px; color: #6b7280; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.prompt-item-actions { position: absolute; top: 8px; right: 8px; display: none; gap: 4px; }
.prompt-item:hover .prompt-item-actions { display: flex; }
.prompt-action-btn {
width: 24px; height: 24px; border: none; background: white; border-radius: 4px; cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: all 0.2s;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); font-size: 12px;
}
.prompt-action-btn:hover { background: #f3f4f6; transform: scale(1.1); }
.prompt-item.dragging { opacity: 0.5; }
.add-prompt-btn {
margin: 12px; padding: 10px; background: ${gradient};
color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer;
transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 6px;
}
.add-prompt-btn:hover { transform: translateY(-2px); }
/* 模态框 */
.prompt-modal {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5);
display: flex; align-items: center; justify-content: center; z-index: 1000000; animation: fadeIn 0.2s;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.prompt-modal-content {
background: white; border-radius: 12px; width: 90%; max-width: 500px; padding: 24px; animation: slideUp 0.3s;
}
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.prompt-modal-header { font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #1f2937; }
.prompt-form-group { margin-bottom: 16px; }
.prompt-form-label { display: block; font-size: 14px; font-weight: 500; color: #374151; margin-bottom: 6px; }
.prompt-form-input, .prompt-form-textarea {
width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px;
transition: all 0.2s; box-sizing: border-box;
}
.prompt-form-textarea { min-height: 100px; resize: vertical; font-family: inherit; }
.prompt-form-input:focus, .prompt-form-textarea:focus { outline: none; border-color: ${colors.primary}; }
.prompt-modal-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px; }
.prompt-modal-btn { padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; border: none; }
.prompt-modal-btn.primary { background: ${gradient}; color: white; }
.prompt-modal-btn.secondary { background: #f3f4f6; color: #4b5563; }
/* 选中的提示词显示栏 */
.selected-prompt-bar {
position: fixed; bottom: 120px; left: 50%; transform: translateX(-50%);
background: ${gradient};
color: white; padding: 8px 16px; border-radius: 20px; font-size: 13px; display: none;
align-items: center; gap: 8px; box-shadow: 0 4px 12px rgba(66,133,244,0.3);
z-index: 999998; animation: slideInUp 0.3s;
}
@keyframes slideInUp { from { transform: translate(-50%, 20px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }
.selected-prompt-bar.show { display: flex; }
.selected-prompt-text { max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clear-prompt-btn {
background: rgba(255,255,255,0.2); border: none; color: white; width: 20px; height: 20px;
border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.quick-prompt-btn {
width: 44px; height: 44px;
background: ${gradient};
border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white;
font-size: 18px; cursor: pointer; box-shadow: 0 4px 12px rgba(66,133,244,0.3);
border: none; transition: transform 0.3s;
}
.quick-prompt-btn:hover { transform: scale(1.1); }
/* 快捷按钮组(收起时显示) */
.quick-btn-group {
position: fixed; bottom: 120px; right: 30px;
display: flex; flex-direction: column; gap: 10px;
z-index: 999997; transition: opacity 0.3s;
}
.quick-btn-group.hidden { display: none; }
.hidden { display: none !important; }
.outline-hidden { display: none !important; }
.prompt-toast {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #10b981;
color: white; padding: 12px 20px; border-radius: 8px; font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000001; animation: toastSlideIn 0.3s;
}
@keyframes toastSlideIn { from { transform: translate(-50%, -20px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } }
/* 快捷跳转按钮组(面板内) */
.scroll-nav-container {
display: flex; gap: 8px; padding: 10px 16px; border-top: 1px solid #e5e7eb;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 0 0 12px 12px; justify-content: center;
}
.scroll-nav-btn {
flex: 1; max-width: 120px; height: 32px; border-radius: 8px; border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 14px; color: white; gap: 4px;
background: ${gradient};
box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: transform 0.2s, box-shadow 0.2s;
}
.scroll-nav-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
/* 分类管理按钮 */
.category-manage-btn {
padding: 4px 8px; background: transparent; border: 1px dashed #9ca3af; border-radius: 12px;
font-size: 12px; color: #6b7280; cursor: pointer; transition: all 0.2s; margin-left: 4px;
}
.category-manage-btn:hover { background: #f3f4f6; border-color: #6b7280; color: #374151; }
/* 分类管理弹窗 */
.category-modal-content { max-height: 400px; }
.category-list { max-height: 280px; overflow-y: auto; margin: 16px 0; }
.category-item {
display: flex; align-items: center; justify-content: space-between; padding: 12px 16px;
background: #f9fafb; border-radius: 8px; margin-bottom: 8px; transition: all 0.2s;
}
.category-item:hover { background: #f3f4f6; }
.category-item-info { display: flex; align-items: center; gap: 12px; flex: 1; }
.category-item-name { font-weight: 500; color: #1f2937; font-size: 14px; }
.category-item-count { font-size: 12px; color: #6b7280; background: #e5e7eb; padding: 2px 8px; border-radius: 10px; }
.category-item-actions { display: flex; gap: 8px; }
.category-action-btn {
padding: 4px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; border: none; transition: all 0.2s;
}
.category-action-btn.rename { background: #dbeafe; color: #1d4ed8; }
.category-action-btn.rename:hover { background: #bfdbfe; }
.category-action-btn.delete { background: #fee2e2; color: #dc2626; }
.category-action-btn.delete:hover { background: #fecaca; }
.category-empty { text-align: center; color: #9ca3af; padding: 40px 0; font-size: 14px; }
/* Tab 切换栏 */
.prompt-panel-tabs {
display: flex; background: #f9fafb; border-bottom: 1px solid #e5e7eb;
}
.prompt-panel-tab {
flex: 1; padding: 10px 16px; background: transparent; border: none;
font-size: 13px; font-weight: 500; color: #6b7280; cursor: pointer;
transition: all 0.2s; border-bottom: 2px solid transparent;
}
.prompt-panel-tab:hover { color: #374151; background: #f3f4f6; }
.prompt-panel-tab.active {
color: ${colors.primary}; border-bottom-color: ${colors.primary}; background: white;
}
/* 面板内容区 */
.prompt-panel-content { display: flex; flex-direction: column; flex: 1; overflow: hidden; min-height: 280px; }
.prompt-panel-content.hidden { display: none; }
/* 设置面板样式 - 合并优化 */
.settings-content { padding: 16px; overflow-y: auto; flex: 1; }
.settings-section { margin-bottom: 24px; }
.settings-section-title {
font-size: 12px; font-weight: 600; color: #6b7280; margin-bottom: 8px;
text-transform: uppercase; letter-spacing: 0.5px; padding-left: 4px; border-bottom: none;
}
.setting-item {
display: flex; justify-content: space-between; align-items: center;
padding: 12px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px;
border: 1px solid #f3f4f6; transition: all 0.2s;
}
.setting-item:hover { border-color: linear-gradient(135deg, #4285f4 0%, #34a853 100%); background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
.setting-item-info { flex: 1; margin-right: 12px; min-width: 0; display: flex; flex-direction: column; justify-content: center; }
.setting-item-label { font-size: 14px; font-weight: 500; color: #374151; margin-bottom: 2px; white-space: nowrap; }
.setting-item-desc { font-size: 12px; color: #9ca3af; line-height: 1.3; }
.setting-controls { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.setting-select {
padding: 6px 8px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 13px;
color: #374151; background: white; outline: none; transition: all 0.2s; height: 32px; box-sizing: border-box;
min-width: 100px;
}
.setting-select:focus { border-color: #4285f4; box-shadow: 0 0 0 2px rgba(66,133,244,0.1); }
.setting-toggle {
width: 44px; height: 24px; background: #d1d5db; border-radius: 12px; position: relative;
cursor: pointer; transition: all 0.3s; flex-shrink: 0;
}
.setting-toggle::after {
content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px;
background: white; border-radius: 50%; transition: all 0.3s; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.setting-toggle.active { background: #4285f4; } /* 默认蓝色,会被JS覆盖 */
.setting-toggle.active::after { left: 22px; }
/* 大纲面板样式 */
.outline-content {
display: flex; flex-direction: column; flex: 1; min-height: 200px; user-select: none; overflow: hidden;
}
/* 大纲固定工具栏 */
.outline-fixed-toolbar {
padding: 10px 12px; background: #f9fafb; border-bottom: 1px solid #e5e7eb;
flex-shrink: 0; display: flex; flex-direction: column; gap: 8px;
}
.outline-toolbar-row {
display: flex; align-items: center; gap: 8px;
}
.outline-toolbar-btn {
width: 28px; height: 28px; border: 1px solid #d1d5db; border-radius: 6px;
background: white; color: #6b7280; cursor: pointer; display: flex;
align-items: center; justify-content: center; font-size: 14px;
transition: all 0.2s; flex-shrink: 0;
}
.outline-toolbar-btn:hover { border-color: ${colors.primary}; color: ${colors.primary}; background: #f0f9ff; }
.outline-toolbar-btn.active { border-color: ${colors.primary}; color: white; background: ${colors.primary}; }
.outline-search-input {
flex: 1; height: 28px; padding: 0 10px; border: 1px solid #d1d5db; border-radius: 6px;
font-size: 13px; color: #374151; outline: none; transition: all 0.2s;
}
.outline-search-input:focus { border-color: ${colors.primary}; box-shadow: 0 0 0 2px rgba(66,133,244,0.1); }
.outline-search-input::placeholder { color: #9ca3af; }
.outline-search-clear {
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
width: 16px; height: 16px; border: none; background: #d1d5db; color: white;
border-radius: 50%; cursor: pointer; font-size: 10px; line-height: 16px; text-align: center;
}
.outline-search-clear:hover { background: #9ca3af; }
.outline-search-wrapper { position: relative; flex: 1; display: flex; align-items: center; }
.outline-search-result { font-size: 12px; color: #6b7280; margin-left: 8px; white-space: nowrap; }
.outline-result-bar {
padding: 6px 12px; background: #eff6ff; color: #1d4ed8; font-size: 12px;
border-bottom: 1px solid #dbeafe; text-align: center; flex-shrink: 0;
transition: all 0.3s;
}
/* 层级滑块 */
.outline-level-slider-container {
display: flex; align-items: center; gap: 6px; width: 100%;
}
.outline-level-slider {
flex: 1; height: 4px; -webkit-appearance: none; appearance: none;
background: #e5e7eb; border-radius: 2px; outline: none; cursor: pointer;
}
.outline-level-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
background: ${colors.primary}; cursor: pointer; border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.outline-level-slider::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%;
background: ${colors.primary}; cursor: pointer; border: 2px solid white;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.outline-level-dots {
display: flex; justify-content: space-between; align-items: center;
position: relative; flex: 1; height: 24px;
}
.outline-level-dot {
width: 12px; height: 12px; border-radius: 50%; background: #d1d5db;
cursor: pointer; transition: all 0.2s; position: relative; z-index: 2;
border: 2px solid white; box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.outline-level-dot:hover { background: ${colors.primary}; transform: scale(1.2); }
.outline-level-dot.active { background: ${colors.primary}; }
.outline-level-dot-tooltip {
position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
background: #374151; color: white; padding: 4px 8px; border-radius: 4px;
font-size: 11px; white-space: nowrap; opacity: 0; visibility: hidden;
transition: all 0.2s; pointer-events: none; margin-bottom: 4px;
}
.outline-level-dot:hover .outline-level-dot-tooltip { opacity: 1; visibility: visible; }
.outline-level-line {
position: absolute; left: 10px; right: 10px; top: 50%; height: 4px;
background: #e5e7eb; transform: translateY(-50%); z-index: 1; border-radius: 2px;
}
.outline-level-progress {
position: absolute; left: 0; top: 0; height: 100%; background: ${colors.primary};
border-radius: 2px; transition: width 0.2s;
}
/* 大纲列表区 */
.outline-list-wrapper { flex: 1; overflow-y: auto; padding: 8px 12px; }
.outline-list { display: flex; flex-direction: column; gap: 2px; }
.outline-item {
padding: 6px 10px 6px 10px; border-radius: 6px; cursor: pointer;
background: transparent; border: 1px solid transparent;
font-size: 13px; color: #374151; transition: all 0.15s;
display: flex; align-items: center; position: relative;
}
.outline-item:hover { background: #f3f4f6; }
.outline-item.highlight { background: #dbeafe; border-color: ${colors.primary}; }
.outline-item-toggle {
width: 24px; min-width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center;
color: #9ca3af; cursor: pointer; transition: all 0.2s ease;
font-size: 16px; flex-shrink: 0; margin-right: 2px; box-sizing: border-box; border-radius: 4px;
}
.outline-item-toggle:hover { color: ${colors.primary}; background-color: rgba(0,0,0,0.05); }
.outline-item-toggle.expanded { transform: rotate(90deg); color: ${colors.primary}; }
.outline-item-toggle.invisible { opacity: 0; cursor: default; pointer-events: none; visibility: visible !important; display: inline-flex !important; }
.outline-item-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 24px; }
.outline-item.collapsed-children { display: none; }
/* 大纲层级缩进 - 箭头跟随缩进,文字保持左对齐 */
.outline-level-1 { padding-left: 10px; font-weight: 600; font-size: 14px; }
.outline-level-2 { padding-left: 28px; font-weight: 500; }
.outline-level-3 { padding-left: 46px; }
.outline-level-4 { padding-left: 64px; font-size: 12px; }
.outline-level-5 { padding-left: 82px; font-size: 12px; color: #6b7280; }
.outline-level-6 { padding-left: 100px; font-size: 12px; color: #9ca3af; }
.outline-empty { text-align: center; color: #9ca3af; padding: 40px 20px; font-size: 14px; }
/* 大纲高亮效果 */
.outline-highlight { animation: outlineHighlight 2s ease-out; }
@keyframes outlineHighlight {
0% { background: rgba(66, 133, 244, 0.3); }
100% { background: transparent; }
}
`;
document.head.appendChild(style);
}
createUI() {
const existingPanel = document.getElementById('gemini-helper-panel');
const existingBar = document.querySelector('.selected-prompt-bar');
const existingBtn = document.querySelector('.quick-prompt-btn');
if (existingPanel) existingPanel.remove();
if (existingBar) existingBar.remove();
if (existingBtn) existingBtn.remove();
const panel = createElementSafely('div', { id: 'gemini-helper-panel' });
// Header
const header = createElementSafely('div', { className: 'prompt-panel-header' });
const title = createElementSafely('div', { className: 'prompt-panel-title' });
title.appendChild(createElementSafely('span', {}, '✨'));
title.appendChild(createElementSafely('span', {}, this.t('panelTitle')));
title.appendChild(createElementSafely('span', { className: 'site-indicator' }, this.siteAdapter.getName()));
const controls = createElementSafely('div', { className: 'prompt-panel-controls' });
const refreshBtn = createElementSafely('button', { className: 'prompt-panel-btn', id: 'refresh-prompts', title: this.t('refreshPrompts') }, '⟳');
const toggleBtn = createElementSafely('button', { className: 'prompt-panel-btn', id: 'toggle-panel', title: this.t('collapse') }, '−');
controls.appendChild(refreshBtn);
controls.appendChild(toggleBtn);
header.appendChild(title);
header.appendChild(controls);
// Tab 栏
const tabs = createElementSafely('div', { className: 'prompt-panel-tabs' });
// 根据设置的顺序渲染 Tab
const tabOrder = this.settings.tabOrder || DEFAULT_TAB_ORDER;
// 确保所有 Tab 都存在(防止新版本新增 Tab 或配置丢失)
const allTabs = new Set([...tabOrder, ...DEFAULT_TAB_ORDER]);
// 过滤掉未定义的 Tab ID
const validTabs = Array.from(allTabs).filter(id => TAB_DEFINITIONS[id]);
validTabs.forEach(tabId => {
const def = TAB_DEFINITIONS[tabId];
// 特殊处理:如果大纲被禁用,添加 hidden 类,但仍然渲染(为了保持 DOM 结构一致性,或者稍后在 switchTab 处理可见性)
// 这里稍微调整逻辑:创建 button,初始 class 根据状态决定
let className = 'prompt-panel-tab';
if (this.currentTab === tabId) className += ' active';
// 大纲特殊显隐逻辑
if (tabId === 'outline' && !this.settings.outline?.enabled) {
className += ' hidden';
}
const btn = createElementSafely('button', {
className: className,
'data-tab': tabId,
id: `${tabId}-tab`
});
// 添加图标和文本
btn.appendChild(createElementSafely('span', { style: 'margin-right: 6px;' }, def.icon));
btn.appendChild(document.createTextNode(this.t(def.labelKey)));
btn.addEventListener('click', () => this.switchTab(tabId));
tabs.appendChild(btn);
});
panel.appendChild(header);
panel.appendChild(tabs);
// 内容容器需按固定顺序创建(DOM 结构不受 Tab 顺序影响,只影响 Tab 按钮顺序)
// 1. 提示词面板内容区
const promptsContent = createElementSafely('div', {
className: `prompt-panel-content${this.currentTab === 'prompts' ? '' : ' hidden'}`,
id: 'prompts-content'
});
const searchBar = createElementSafely('div', { className: 'prompt-search-bar' });
const searchInput = createElementSafely('input', { className: 'prompt-search-input', id: 'prompt-search', type: 'text', placeholder: this.t('searchPlaceholder') });
searchBar.appendChild(searchInput);
const categories = createElementSafely('div', { className: 'prompt-categories', id: 'prompt-categories' });
const list = createElementSafely('div', { className: 'prompt-list', id: 'prompt-list' });
const addBtn = createElementSafely('button', { className: 'add-prompt-btn', id: 'add-prompt' });
addBtn.appendChild(createElementSafely('span', {}, '+'));
addBtn.appendChild(createElementSafely('span', {}, this.t('addPrompt')));
promptsContent.appendChild(searchBar);
promptsContent.appendChild(categories);
promptsContent.appendChild(list);
promptsContent.appendChild(addBtn);
// 2. 大纲面板内容区
const outlineContent = createElementSafely('div', {
className: `prompt-panel-content${this.currentTab === 'outline' ? '' : ' hidden'}`,
id: 'outline-content'
});
// 初始化大纲管理器
this.outlineManager = new OutlineManager({
container: outlineContent,
settings: this.settings,
onSettingsChange: () => this.saveSettings(),
i18n: (k) => this.t(k)
});
// 3. 设置面板内容区
const settingsContent = createElementSafely('div', {
className: `prompt-panel-content${this.currentTab === 'settings' ? '' : ' hidden'}`,
id: 'settings-content'
});
this.createSettingsContent(settingsContent);
panel.appendChild(promptsContent);
panel.appendChild(outlineContent);
panel.appendChild(settingsContent);
document.body.appendChild(panel);
// 选中提示词悬浮条
const selectedBar = createElementSafely('div', { className: 'selected-prompt-bar', style: 'user-select: none;' });
selectedBar.appendChild(createElementSafely('span', { style: 'user-select: none;' }, this.t('currentPrompt')));
selectedBar.appendChild(createElementSafely('span', { className: 'selected-prompt-text', id: 'selected-prompt-text', style: 'user-select: none;' }));
const clearBtn = createElementSafely('button', { className: 'clear-prompt-btn', id: 'clear-prompt' }, '×');
selectedBar.appendChild(clearBtn);
document.body.appendChild(selectedBar);
// 快捷按钮组(收起时显示)
const quickBtnGroup = createElementSafely('div', { className: 'quick-btn-group hidden', id: 'quick-btn-group' });
const quickBtn = createElementSafely('button', { className: 'quick-prompt-btn', title: this.t('panelTitle') }, '✨');
const quickScrollTop = createElementSafely('button', { className: 'quick-prompt-btn', title: this.t('scrollTop') }, '⬆');
const quickScrollBottom = createElementSafely('button', { className: 'quick-prompt-btn', title: this.t('scrollBottom') }, '⬇');
quickBtn.addEventListener('click', () => { this.togglePanel(); });
quickScrollTop.addEventListener('click', () => this.scrollToTop());
quickScrollBottom.addEventListener('click', () => this.scrollToBottom());
quickBtnGroup.appendChild(quickScrollTop);
quickBtnGroup.appendChild(quickBtn);
quickBtnGroup.appendChild(quickScrollBottom);
document.body.appendChild(quickBtnGroup);
// 快捷跳转按钮组 - 放在面板底部
const scrollNavContainer = createElementSafely('div', { className: 'scroll-nav-container', id: 'scroll-nav-container' });
const scrollTopBtn = createElementSafely('button', { className: 'scroll-nav-btn', id: 'scroll-top-btn', title: this.t('scrollTop') });
scrollTopBtn.appendChild(createElementSafely('span', {}, '⬆'));
scrollTopBtn.appendChild(createElementSafely('span', {}, this.t('scrollTop')));
const scrollBottomBtn = createElementSafely('button', { className: 'scroll-nav-btn', id: 'scroll-bottom-btn', title: this.t('scrollBottom') });
scrollBottomBtn.appendChild(createElementSafely('span', {}, '⬇'));
scrollBottomBtn.appendChild(createElementSafely('span', {}, this.t('scrollBottom')));
scrollTopBtn.addEventListener('click', () => this.scrollToTop());
scrollBottomBtn.addEventListener('click', () => this.scrollToBottom());
scrollNavContainer.appendChild(scrollTopBtn);
scrollNavContainer.appendChild(scrollBottomBtn);
panel.appendChild(scrollNavContainer);
this.refreshCategories();
this.refreshPromptList();
}
// Tab 切换
switchTab(tabName) {
this.currentTab = tabName;
// 更新 Tab 激活状态
document.querySelectorAll('.prompt-panel-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
// 切换内容区
document.getElementById('prompts-content')?.classList.toggle('hidden', tabName !== 'prompts');
document.getElementById('outline-content')?.classList.toggle('hidden', tabName !== 'outline');
document.getElementById('settings-content')?.classList.toggle('hidden', tabName !== 'settings');
// 更新刷新按钮的提示
const refreshBtn = document.getElementById('refresh-prompts');
if (refreshBtn) {
const titleMap = {
'prompts': this.t('refreshPrompts'),
'outline': this.t('refreshOutline'),
'settings': this.t('refreshSettings')
};
refreshBtn.title = titleMap[tabName] || this.t('refresh');
}
// 切换到大纲时自动刷新
if (tabName === 'outline') {
this.refreshOutline();
}
}
// 刷新大纲
refreshOutline() {
if (!this.settings.outline?.enabled) return;
const outline = this.siteAdapter.extractOutline(6);
if (this.outlineManager) {
this.outlineManager.update(outline);
}
}
// 创建设置面板内容
createSettingsContent(container) {
const content = createElementSafely('div', { className: 'settings-content' });
// 通用设置区:语言选择
const generalSection = createElementSafely('div', { className: 'settings-section' });
generalSection.appendChild(createElementSafely('div', { className: 'settings-section-title' }, this.t('settingsTitle')));
// 语言选择项
const langItem = createElementSafely('div', { className: 'setting-item' });
const langInfo = createElementSafely('div', { className: 'setting-item-info' });
langInfo.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t('languageLabel')));
langInfo.appendChild(createElementSafely('div', { className: 'setting-item-desc' }, this.t('languageDesc')));
const langSelect = createElementSafely('select', { className: 'setting-select', id: 'select-language' });
const currentLang = GM_getValue(SETTING_KEYS.LANGUAGE, 'auto');
[
{ value: 'auto', label: this.t('languageAuto') },
{ value: 'zh-CN', label: this.t('languageZhCN') },
{ value: 'zh-TW', label: this.t('languageZhTW') },
{ value: 'en', label: this.t('languageEn') }
].forEach(opt => {
const option = createElementSafely('option', { value: opt.value }, opt.label);
if (opt.value === currentLang) option.selected = true;
langSelect.appendChild(option);
});
langSelect.addEventListener('change', () => {
GM_setValue(SETTING_KEYS.LANGUAGE, langSelect.value);
// 更新当前语言并重新渲染 UI,实现即时生效
this.lang = detectLanguage();
this.i18n = I18N[this.lang];
this.createStyles();
this.createUI();
this.bindEvents();
// 切换到设置面板
this.switchTab('settings');
this.showToast(langSelect.value === 'auto' ? this.t('languageAuto') : langSelect.options[langSelect.selectedIndex].text);
});
langItem.appendChild(langInfo);
langItem.appendChild(langSelect);
generalSection.appendChild(langItem);
// 页面宽度设置
const widthSection = createElementSafely('div', { className: 'settings-section' });
widthSection.appendChild(createElementSafely('div', { className: 'settings-section-title' }, this.t('pageWidthLabel')));
// 启用页面加宽开关
const enableWidthItem = createElementSafely('div', { className: 'setting-item' });
const enableWidthInfo = createElementSafely('div', { className: 'setting-item-info' });
enableWidthInfo.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t('enablePageWidth')));
enableWidthInfo.appendChild(createElementSafely('div', { className: 'setting-item-desc' }, this.t('pageWidthDesc')));
const enableToggle = createElementSafely('div', {
className: 'setting-toggle' + (this.settings.pageWidth && this.settings.pageWidth.enabled ? ' active' : ''),
id: 'toggle-page-width'
});
enableToggle.addEventListener('click', () => {
this.settings.pageWidth.enabled = !this.settings.pageWidth.enabled;
enableToggle.classList.toggle('active', this.settings.pageWidth.enabled);
this.saveSettings();
// 应用宽度样式
if (this.widthStyleManager) {
this.widthStyleManager.updateConfig(this.settings.pageWidth);
}
this.showToast(this.settings.pageWidth.enabled ? this.t('settingOn') : this.t('settingOff'));
});
enableWidthItem.appendChild(enableWidthInfo);
enableWidthItem.appendChild(enableToggle);
widthSection.appendChild(enableWidthItem);
// 宽度值和单位设置
const widthValueItem = createElementSafely('div', { className: 'setting-item' });
const widthValueInfo = createElementSafely('div', { className: 'setting-item-info' });
widthValueInfo.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t('widthValue')));
const widthControls = createElementSafely('div', { className: 'setting-controls' });
const widthInput = createElementSafely('input', {
type: 'number',
className: 'setting-select',
id: 'width-value-input',
value: this.settings.pageWidth ? this.settings.pageWidth.value : '70',
style: 'width: 65px !important; min-width: 65px !important; text-align: right;'
});
const unitSelect = createElementSafely('select', {
className: 'setting-select',
id: 'width-unit-select',
style: 'width: 65px;'
});
['%', 'px'].forEach(unit => {
const option = createElementSafely('option', { value: unit }, unit);
if (this.settings.pageWidth && this.settings.pageWidth.unit === unit) {
option.selected = true;
}
unitSelect.appendChild(option);
});
// 限制值逻辑
const validateAndSave = () => {
let val = parseFloat(widthInput.value);
const unit = unitSelect.value;
if (unit === '%') {
if (val > 100) val = 100;
if (val < 10) val = 10; // 最小限制
} else {
if (val < 400) val = 400; // 像素最小限制
}
// 如果值被修正了,更新输入框
if (val !== parseFloat(widthInput.value)) {
widthInput.value = val;
}
this.settings.pageWidth.value = val.toString();
this.settings.pageWidth.unit = unit;
this.saveSettings();
if (this.widthStyleManager) {
this.widthStyleManager.updateConfig(this.settings.pageWidth);
}
};
// 输入变化事件(防抖)
let timeout;
widthInput.addEventListener('input', () => {
// 实时限制输入长度,避免太长
if (widthInput.value.length > 5) widthInput.value = widthInput.value.slice(0, 5);
// 实时限制百分比逻辑
if (unitSelect.value === '%' && parseFloat(widthInput.value) > 100) {
widthInput.value = '100';
}
clearTimeout(timeout);
timeout = setTimeout(validateAndSave, 500);
});
widthInput.addEventListener('change', validateAndSave); // 失去焦点或回车立即保存
unitSelect.addEventListener('change', () => {
// 切换单位时,提供合理的默认转换或限制
if (unitSelect.value === '%' && parseFloat(widthInput.value) > 100) {
widthInput.value = '70'; // 切换到%时,默认给个舒服的宽度
} else if (unitSelect.value === 'px' && parseFloat(widthInput.value) <= 100) {
widthInput.value = '1200'; // 切换到px时,默认给个舒服的宽度
}
validateAndSave();
this.showToast(`${this.t('widthValue')}: ${widthInput.value}${unitSelect.value}`);
});
widthControls.appendChild(widthInput);
widthControls.appendChild(unitSelect);
widthValueItem.appendChild(widthValueInfo);
widthValueItem.appendChild(widthControls);
widthSection.appendChild(widthValueItem);
content.appendChild(generalSection);
content.appendChild(widthSection);
// 大纲设置区
const outlineSection = createElementSafely('div', { className: 'settings-section' });
outlineSection.appendChild(createElementSafely('div', { className: 'settings-section-title' }, this.t('outlineSettings')));
// 启用大纲开关
const enableOutlineItem = createElementSafely('div', { className: 'setting-item' });
const enableOutlineInfo = createElementSafely('div', { className: 'setting-item-info' });
enableOutlineInfo.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t('enableOutline')));
const outlineToggle = createElementSafely('div', {
className: 'setting-toggle' + (this.settings.outline?.enabled ? ' active' : ''),
id: 'toggle-outline'
});
outlineToggle.addEventListener('click', () => {
this.settings.outline.enabled = !this.settings.outline.enabled;
outlineToggle.classList.toggle('active', this.settings.outline.enabled);
this.saveSettings();
// 显示/隐藏大纲 Tab
const outlineTab = document.getElementById('outline-tab');
if (outlineTab) {
outlineTab.classList.toggle('hidden', !this.settings.outline.enabled);
}
// 如果正在大纲 Tab 且被禁用,切换到提示词 Tab
if (!this.settings.outline.enabled && this.currentTab === 'outline') {
this.switchTab('prompts');
}
this.showToast(this.settings.outline.enabled ? this.t('settingOn') : this.t('settingOff'));
});
enableOutlineItem.appendChild(enableOutlineInfo);
enableOutlineItem.appendChild(outlineToggle);
outlineSection.appendChild(enableOutlineItem);
content.appendChild(outlineSection);
// 只在 Gemini Business 时添加清空输入框设置
if (this.siteAdapter instanceof GeminiBusinessAdapter) {
const clearItem = createElementSafely('div', { className: 'setting-item' });
const clearInfo = createElementSafely('div', { className: 'setting-item-info' });
clearInfo.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t('clearOnSendLabel')));
clearInfo.appendChild(createElementSafely('div', { className: 'setting-item-desc' }, this.t('clearOnSendDesc')));
const toggle = createElementSafely('div', {
className: 'setting-toggle' + (this.settings.clearTextareaOnSend ? ' active' : ''),
id: 'toggle-clear-on-send'
});
toggle.addEventListener('click', () => {
this.settings.clearTextareaOnSend = !this.settings.clearTextareaOnSend;
toggle.classList.toggle('active', this.settings.clearTextareaOnSend);
this.saveSettings();
this.showToast(this.settings.clearTextareaOnSend ? this.t('settingOn') : this.t('settingOff'));
});
clearItem.appendChild(clearInfo);
clearItem.appendChild(toggle);
generalSection.appendChild(clearItem);
}
// Tab 排序设置
const tabOrderSection = createElementSafely('div', { className: 'settings-section' });
tabOrderSection.appendChild(createElementSafely('div', { className: 'settings-section-title' }, this.t('tabOrderSettings')));
// 说明
const tabDesc = createElementSafely('div', {
className: 'setting-item-desc',
style: 'padding: 0 12px 8px 12px; margin-bottom: 4px;'
}, this.t('tabOrderDesc'));
tabOrderSection.appendChild(tabDesc);
const currentOrder = this.settings.tabOrder || DEFAULT_TAB_ORDER;
// 过滤有效定义
const validOrder = currentOrder.filter(id => TAB_DEFINITIONS[id]);
validOrder.forEach((tabId, index) => {
const def = TAB_DEFINITIONS[tabId];
const item = createElementSafely('div', { className: 'setting-item' });
const info = createElementSafely('div', { className: 'setting-item-info' });
info.appendChild(createElementSafely('div', { className: 'setting-item-label' }, this.t(def.labelKey)));
const controls = createElementSafely('div', { className: 'setting-controls' });
// 上移按钮
const upBtn = createElementSafely('button', {
className: 'prompt-panel-btn',
style: 'background: #f3f4f6; color: #4b5563; width: 32px; height: 32px; font-size: 16px; margin-right: 4px; border: 1px solid #e5e7eb;',
title: this.t('moveUp')
});
upBtn.textContent = '⬆';
upBtn.disabled = index === 0;
// 下移按钮
const downBtn = createElementSafely('button', {
className: 'prompt-panel-btn',
style: 'background: #f3f4f6; color: #4b5563; width: 32px; height: 32px; font-size: 16px; border: 1px solid #e5e7eb;',
title: this.t('moveDown')
});
downBtn.textContent = '⬇';
downBtn.disabled = index === validOrder.length - 1;
// 按钮状态样式修正 helper
const updateButtonStyle = (btn) => {
if (btn.disabled) {
btn.style.opacity = '0.4';
btn.style.cursor = 'not-allowed';
btn.style.background = '#f3f4f6';
} else {
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
};
updateButtonStyle(upBtn);
updateButtonStyle(downBtn);
// 事件绑定(仅在非禁用时生效,虽然 disabled 属性本身阻止了 click,但为了保险)
if (!upBtn.disabled) {
upBtn.onmouseover = () => { upBtn.style.background = '#e5e7eb'; upBtn.style.color = '#111827'; };
upBtn.onmouseout = () => { upBtn.style.background = '#f3f4f6'; upBtn.style.color = '#4b5563'; };
}
if (!downBtn.disabled) {
downBtn.onmouseover = () => { downBtn.style.background = '#e5e7eb'; downBtn.style.color = '#111827'; };
downBtn.onmouseout = () => { downBtn.style.background = '#f3f4f6'; downBtn.style.color = '#4b5563'; };
}
upBtn.addEventListener('click', () => {
if (index > 0) {
// 交换位置
const newOrder = [...validOrder];
[newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]];
this.settings.tabOrder = newOrder;
this.saveSettings();
this.createUI(); // 重新渲染界面
this.switchTab('settings'); // 保持在设置页
}
});
downBtn.addEventListener('click', () => {
if (index < validOrder.length - 1) {
// 交换位置
const newOrder = [...validOrder];
[newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]];
this.settings.tabOrder = newOrder;
this.saveSettings();
this.createUI(); // 重新渲染界面
this.switchTab('settings'); // 保持在设置页
}
});
controls.appendChild(upBtn);
controls.appendChild(downBtn);
item.appendChild(info);
item.appendChild(controls);
tabOrderSection.appendChild(item);
});
content.appendChild(tabOrderSection);
container.appendChild(content);
}
togglePanel() {
const panel = document.getElementById('gemini-helper-panel');
const quickBtnGroup = document.getElementById('quick-btn-group');
const toggleBtn = document.getElementById('toggle-panel');
this.isCollapsed = !this.isCollapsed;
if (this.isCollapsed) {
panel.classList.add('collapsed');
if (quickBtnGroup) quickBtnGroup.classList.remove('hidden');
if (toggleBtn) toggleBtn.textContent = '+';
} else {
panel.classList.remove('collapsed');
if (quickBtnGroup) quickBtnGroup.classList.add('hidden');
if (toggleBtn) toggleBtn.textContent = '−';
}
}
// 滚动到页面顶部
scrollToTop() {
if (this.isScrolling) return;
const scrollContainer = this.siteAdapter.getScrollContainer();
if (scrollContainer) {
this.isScrolling = true;
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
// 锁定 1 秒禁止操作,防止焦点漂移
setTimeout(() => { this.isScrolling = false; }, 1000);
}
}
// 滚动到页面底部
scrollToBottom() {
if (this.isScrolling) return;
const scrollContainer = this.siteAdapter.getScrollContainer();
if (scrollContainer) {
this.isScrolling = true;
scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
// 锁定 1 秒禁止操作
setTimeout(() => { this.isScrolling = false; }, 1000);
}
}
refreshCategories() {
const container = document.getElementById('prompt-categories');
if (!container) return;
const categories = this.getCategories();
clearElementSafely(container);
container.appendChild(createElementSafely('span', { className: 'category-tag active', 'data-category': 'all' }, this.t('allCategory')));
categories.forEach(cat => {
container.appendChild(createElementSafely('span', { className: 'category-tag', 'data-category': cat }, cat));
});
// 添加分类管理按钮
const manageBtn = createElementSafely('button', { className: 'category-manage-btn', title: this.t('categoryManage') }, this.t('manageCategory'));
manageBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.showCategoryModal();
});
container.appendChild(manageBtn);
}
// 显示分类管理弹窗
showCategoryModal() {
const categories = this.getCategories();
const modal = createElementSafely('div', { className: 'prompt-modal' });
const modalContent = createElementSafely('div', { className: 'prompt-modal-content category-modal-content' });
const modalHeader = createElementSafely('div', { className: 'prompt-modal-header' }, this.t('categoryManage'));
modalContent.appendChild(modalHeader);
const categoryList = createElementSafely('div', { className: 'category-list' });
if (categories.length === 0) {
categoryList.appendChild(createElementSafely('div', { className: 'category-empty' }, this.t('categoryEmpty')));
} else {
categories.forEach(cat => {
const count = this.prompts.filter(p => p.category === cat).length;
const item = createElementSafely('div', { className: 'category-item' });
const info = createElementSafely('div', { className: 'category-item-info' });
info.appendChild(createElementSafely('span', { className: 'category-item-name' }, cat));
info.appendChild(createElementSafely('span', { className: 'category-item-count' }, `${count} 个提示词`));
const actions = createElementSafely('div', { className: 'category-item-actions' });
const renameBtn = createElementSafely('button', { className: 'category-action-btn rename' }, this.t('rename'));
const deleteBtn = createElementSafely('button', { className: 'category-action-btn delete' }, this.t('delete'));
renameBtn.addEventListener('click', () => {
const newName = window.prompt(this.t('newCategoryName'), cat);
if (newName && newName.trim() && newName !== cat) {
this.renameCategory(cat, newName.trim());
modal.remove();
this.showCategoryModal();
}
});
deleteBtn.addEventListener('click', () => {
if (confirm(this.t('confirmDeleteCategory'))) {
this.deleteCategory(cat);
modal.remove();
this.showCategoryModal();
}
});
actions.appendChild(renameBtn);
actions.appendChild(deleteBtn);
item.appendChild(info);
item.appendChild(actions);
categoryList.appendChild(item);
});
}
modalContent.appendChild(categoryList);
const btnGroup = createElementSafely('div', { className: 'prompt-modal-btns' });
const closeBtn = createElementSafely('button', { className: 'prompt-modal-btn secondary' }, this.t('cancel'));
closeBtn.addEventListener('click', () => modal.remove());
btnGroup.appendChild(closeBtn);
modalContent.appendChild(btnGroup);
modal.appendChild(modalContent);
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
document.body.appendChild(modal);
}
// 重命名分类
renameCategory(oldName, newName) {
this.prompts.forEach(p => {
if (p.category === oldName) {
p.category = newName;
}
});
this.savePrompts();
this.refreshCategories();
this.refreshPromptList();
this.showToast(`分类已重命名为"${newName}"`);
}
// 删除分类(将关联提示词移至"未分类")
deleteCategory(name) {
this.prompts.forEach(p => {
if (p.category === name) {
p.category = '未分类';
}
});
this.savePrompts();
this.refreshCategories();
this.refreshPromptList();
this.showToast(`分类"${name}"已删除`);
}
refreshPromptList(filter = '') {
const container = document.getElementById('prompt-list');
if (!container) return;
const activeCategory = document.querySelector('.category-tag.active')?.dataset.category || 'all';
let filteredPrompts = this.prompts;
if (activeCategory !== 'all') filteredPrompts = filteredPrompts.filter(p => p.category === activeCategory);
if (filter) filteredPrompts = filteredPrompts.filter(p => p.title.toLowerCase().includes(filter.toLowerCase()) || p.content.toLowerCase().includes(filter.toLowerCase()));
clearElementSafely(container);
if (filteredPrompts.length === 0) {
container.appendChild(createElementSafely('div', { style: 'text-align: center; padding: 20px; color: #9ca3af;' }, '暂无提示词'));
return;
}
filteredPrompts.forEach((prompt, index) => {
const item = createElementSafely('div', { className: 'prompt-item', draggable: 'false', style: 'user-select: none;' });
item.dataset.promptId = prompt.id;
item.dataset.index = index;
if (this.selectedPrompt?.id === prompt.id) item.classList.add('selected');
const itemHeader = createElementSafely('div', { className: 'prompt-item-header' });
itemHeader.appendChild(createElementSafely('div', { className: 'prompt-item-title' }, prompt.title));
itemHeader.appendChild(createElementSafely('span', { className: 'prompt-item-category' }, prompt.category || '未分类'));
const itemContent = createElementSafely('div', { className: 'prompt-item-content' }, prompt.content);
const itemActions = createElementSafely('div', { className: 'prompt-item-actions' });
const dragBtn = createElementSafely('button', { className: 'prompt-action-btn drag-prompt', 'data-id': prompt.id, title: '拖动排序' }, '☰');
dragBtn.style.cursor = 'grab';
// 仅当按下拖拽按钮时才允许拖动
dragBtn.addEventListener('mousedown', () => {
item.setAttribute('draggable', 'true');
// 监听全局鼠标释放,恢复不可拖动
const upHandler = () => {
item.setAttribute('draggable', 'false');
window.removeEventListener('mouseup', upHandler);
};
window.addEventListener('mouseup', upHandler);
});
itemActions.appendChild(dragBtn);
itemActions.appendChild(createElementSafely('button', { className: 'prompt-action-btn copy-prompt', 'data-id': prompt.id, title: '复制' }, '📋'));
itemActions.appendChild(createElementSafely('button', { className: 'prompt-action-btn edit-prompt', 'data-id': prompt.id, title: '编辑' }, '✏'));
itemActions.appendChild(createElementSafely('button', { className: 'prompt-action-btn delete-prompt', 'data-id': prompt.id, title: '删除' }, '🗑'));
item.appendChild(itemHeader);
item.appendChild(itemContent);
item.appendChild(itemActions);
item.addEventListener('click', (e) => {
if (!e.target.closest('.prompt-item-actions')) this.selectPrompt(prompt, item);
});
// 拖拽事件处理
item.addEventListener('dragstart', (e) => {
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', item.innerHTML);
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const draggingItem = container.querySelector('.dragging');
if (draggingItem && draggingItem !== item) {
const rect = item.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
if (e.clientY < midpoint) {
container.insertBefore(draggingItem, item);
} else {
container.insertBefore(draggingItem, item.nextSibling);
}
}
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
item.setAttribute('draggable', 'false'); // 拖拽结束立即恢复
this.updatePromptOrder();
});
container.appendChild(item);
});
}
// 更新提示词顺序
updatePromptOrder() {
const container = document.getElementById('prompt-list');
const items = Array.from(container.querySelectorAll('.prompt-item'));
const newOrder = items.map(item => item.dataset.promptId);
// 重新排列 prompts 数组
const orderedPrompts = [];
newOrder.forEach(id => {
const prompt = this.prompts.find(p => p.id === id);
if (prompt) orderedPrompts.push(prompt);
});
this.prompts = orderedPrompts;
this.savePrompts();
this.showToast(this.t('orderUpdated'));
}
selectPrompt(prompt, itemElement) {
if (this.isScrolling) {
this.showToast(this.t('scrolling'));
return;
}
this.selectedPrompt = prompt;
document.querySelectorAll('.prompt-item').forEach(item => item.classList.remove('selected'));
itemElement.classList.add('selected');
// 显示当前提示词悬浮条
const selectedBar = document.querySelector('.selected-prompt-bar');
const selectedText = document.getElementById('selected-prompt-text');
if (selectedBar && selectedText) {
selectedText.textContent = prompt.title;
selectedBar.classList.add('show');
}
this.insertPromptToTextarea(prompt.content);
this.showToast(`${this.t('inserted')}: ${prompt.title}`);
}
insertPromptToTextarea(promptContent) {
if (this.isScrolling) {
this.showToast('页面正在滚动,请稍后再选择提示词');
return;
}
const promiseOrResult = this.siteAdapter.insertPrompt(promptContent);
// 处理异步返回 (Gemini Business 是异步的)
if (promiseOrResult instanceof Promise) {
promiseOrResult.then(success => {
if (!success) {
this.showToast('未找到输入框,请点击输入框后重试');
// 再次尝试查找
this.siteAdapter.findTextarea();
}
});
} else if (!promiseOrResult) {
this.showToast('未找到输入框,请点击输入框后重试');
this.siteAdapter.findTextarea();
}
}
clearSelectedPrompt() {
this.selectedPrompt = null;
document.querySelector('.selected-prompt-bar')?.classList.remove('show');
document.querySelectorAll('.prompt-item').forEach(item => item.classList.remove('selected'));
}
showEditModal(prompt = null) {
const isEdit = prompt !== null;
const modal = createElementSafely('div', { className: 'prompt-modal' });
const modalContent = createElementSafely('div', { className: 'prompt-modal-content' });
const modalHeader = createElementSafely('div', { className: 'prompt-modal-header' }, isEdit ? this.t('editPrompt') : this.t('addNewPrompt'));
const titleGroup = createElementSafely('div', { className: 'prompt-form-group' });
titleGroup.appendChild(createElementSafely('label', { className: 'prompt-form-label' }, this.t('title')));
const titleInput = createElementSafely('input', { className: 'prompt-form-input', type: 'text', value: isEdit ? prompt.title : '' });
titleGroup.appendChild(titleInput);
const categoryGroup = createElementSafely('div', { className: 'prompt-form-group' });
categoryGroup.appendChild(createElementSafely('label', { className: 'prompt-form-label' }, this.t('category')));
const categoryInput = createElementSafely('input', { className: 'prompt-form-input', type: 'text', value: isEdit ? (prompt.category || '') : '', placeholder: this.t('categoryPlaceholder') });
categoryGroup.appendChild(categoryInput);
const contentGroup = createElementSafely('div', { className: 'prompt-form-group' });
contentGroup.appendChild(createElementSafely('label', { className: 'prompt-form-label' }, this.t('content')));
const contentTextarea = createElementSafely('textarea', { className: 'prompt-form-textarea' });
contentTextarea.value = isEdit ? prompt.content : '';
contentGroup.appendChild(contentTextarea);
const modalActions = createElementSafely('div', { className: 'prompt-modal-actions' });
const cancelBtn = createElementSafely('button', { className: 'prompt-modal-btn secondary' }, this.t('cancel'));
const saveBtn = createElementSafely('button', { className: 'prompt-modal-btn primary' }, isEdit ? this.t('save') : this.t('add'));
modalActions.appendChild(cancelBtn);
modalActions.appendChild(saveBtn);
modalContent.appendChild(modalHeader);
modalContent.appendChild(titleGroup);
modalContent.appendChild(categoryGroup);
modalContent.appendChild(contentGroup);
modalContent.appendChild(modalActions);
modal.appendChild(modalContent);
document.body.appendChild(modal);
cancelBtn.addEventListener('click', () => modal.remove());
saveBtn.addEventListener('click', () => {
const title = titleInput.value.trim();
const content = contentTextarea.value.trim();
if (!title || !content) { alert(this.t('fillTitleContent')); return; }
if (isEdit) {
this.updatePrompt(prompt.id, { title, category: categoryInput.value.trim(), content });
this.showToast(this.t('promptUpdated'));
} else {
this.addPrompt({ title, category: categoryInput.value.trim(), content });
this.showToast(this.t('promptAdded'));
}
modal.remove();
});
modal.addEventListener('click', (e) => { if (e.target === modal) modal.remove(); });
}
showToast(message) {
const toast = createElementSafely('div', { className: 'prompt-toast' }, message);
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'toastSlideIn 0.3s reverse';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
findElementByComposedPath(e) {
if (!e) return null;
// 获取事件的完整传播路径(兼容没有 composedPath 的浏览器)
const path = typeof e.composedPath === 'function' ? e.composedPath() : (e.path || []);
// 获取提交按钮选择器数组并合并成 selector 字符串
const selectors = (this.siteAdapter && typeof this.siteAdapter.getSubmitButtonSelectors === 'function')
? this.siteAdapter.getSubmitButtonSelectors()
: [];
const combinedSelector = selectors.length ? selectors.join(', ') : '';
if (!combinedSelector) return null;
// 查找路径中第一个符合条件的元素
const foundElement = path.find(element =>
element && element instanceof Element && typeof element.matches === 'function' && element.matches(combinedSelector)
);
return foundElement || null;
}
bindEvents() {
const searchInput = document.getElementById('prompt-search');
if (searchInput) searchInput.addEventListener('input', (e) => this.refreshPromptList(e.target.value));
const categories = document.getElementById('prompt-categories');
if (categories) {
categories.addEventListener('click', (e) => {
if (e.target.classList.contains('category-tag')) {
document.querySelectorAll('.category-tag').forEach(tag => tag.classList.remove('active'));
e.target.classList.add('active');
this.refreshPromptList(document.getElementById('prompt-search')?.value || '');
}
});
}
document.getElementById('add-prompt')?.addEventListener('click', () => this.showEditModal());
document.getElementById('prompt-list')?.addEventListener('click', (e) => {
if (e.target.classList.contains('edit-prompt')) {
const prompt = this.prompts.find(p => p.id === e.target.dataset.id);
if (prompt) this.showEditModal(prompt);
} else if (e.target.classList.contains('delete-prompt')) {
if (confirm(this.t('confirmDelete'))) {
this.deletePrompt(e.target.dataset.id);
this.showToast(this.t('deleted'));
}
} else if (e.target.classList.contains('copy-prompt')) {
const prompt = this.prompts.find(p => p.id === e.target.dataset.id);
if (prompt) {
navigator.clipboard.writeText(prompt.content).then(() => {
this.showToast(this.t('copied'));
}).catch(() => {
// 降级方案
const textarea = document.createElement('textarea');
textarea.value = prompt.content;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
this.showToast(this.t('copied'));
});
}
}
});
document.getElementById('clear-prompt')?.addEventListener('click', () => {
this.clearSelectedPrompt();
// 针对 Gemini Business,根据设置决定是否用零宽字符清空
if (this.siteAdapter instanceof GeminiBusinessAdapter) {
if (this.settings.clearTextareaOnSend) {
this.siteAdapter.clearTextarea(); // 插入零宽字符
} else {
this.siteAdapter.clearTextareaNormal(); // 普通清空
}
} else {
// 其他适配器调用各自的 clearTextarea 方法
this.siteAdapter.clearTextarea();
}
this.showToast(this.t('cleared'));
});
document.getElementById('refresh-prompts')?.addEventListener('click', () => {
// 根据当前 Tab 智能刷新
if (this.currentTab === 'outline') {
this.refreshOutline();
this.showToast(this.t('refreshed'));
} else if (this.currentTab === 'prompts') {
this.refreshPromptList();
this.siteAdapter.findTextarea();
this.showToast(this.t('refreshed'));
} else {
// 设置 Tab:重新加载设置
this.settings = this.loadSettings();
this.siteAdapter.findTextarea();
// 重新渲染 UI 以反映新设置
this.createStyles();
this.createUI();
this.bindEvents();
this.switchTab('settings');
this.showToast(this.t('refreshed'));
}
});
document.getElementById('toggle-panel')?.addEventListener('click', () => this.togglePanel());
this.makeDraggable();
document.addEventListener('click', (e) => {
// 委托适配器检查是否为输入框,自动更新引用
if (this.siteAdapter.isValidTextarea(e.target)) {
this.siteAdapter.textarea = e.target;
} else {
const closest = e.target.closest('[contenteditable="true"], .ProseMirror, textarea');
if (closest && this.siteAdapter.isValidTextarea(closest)) {
this.siteAdapter.textarea = closest;
}
}
// 检测是否点击了发送按钮
const found = this.findElementByComposedPath(e);
let matched = !!found;
// 如果 composedPath 没命中,尝试使用 closest 回退(兼容 Shadow DOM 之外的情况)
if (!matched && e && e.target && typeof e.target.closest === 'function') {
const selectors = (this.siteAdapter && typeof this.siteAdapter.getSubmitButtonSelectors === 'function')
? this.siteAdapter.getSubmitButtonSelectors()
: [];
const combined = selectors.length ? selectors.join(', ') : '';
if (combined) {
try {
matched = !!e.target.closest(combined);
} catch (err) {
matched = false;
}
}
}
if (matched) {
// 如果有选中的提示词,清除悬浮条
if (this.selectedPrompt) {
setTimeout(() => { this.clearSelectedPrompt(); }, 100);
}
// 针对 Gemini Business 适配器,根据设置决定是否调用 clearTextarea 修复中文输入问题
if (this.siteAdapter instanceof GeminiBusinessAdapter && this.settings.clearTextareaOnSend) {
setTimeout(() => { this.siteAdapter.clearTextarea(); }, 200);
}
}
});
// 监听 Enter 键发送(Ctrl+Enter 或直接 Enter),兼容 Shadow DOM:从事件传播路径查找真实输入元素
document.addEventListener('keydown', (e) => {
// 只在按下 Enter(非 Shift+Enter)时处理
if (!(e.key === 'Enter' && !e.shiftKey)) return;
// 获取事件传播路径,兼容 composedPath 或 e.path
const path = typeof e.composedPath === 'function' ? e.composedPath() : (e.path || [e.target]);
let foundEditor = null;
for (const node of path) {
if (!node || !(node instanceof Element)) continue;
// 严格判定:先检查显式的可编辑特征(contenteditable / role=textbox / ProseMirror / TEXTAREA)
try {
const isStrictEditable = (
(typeof node.getAttribute === 'function' && node.getAttribute('contenteditable') === 'true') ||
(typeof node.getAttribute === 'function' && node.getAttribute('role') === 'textbox') ||
(node.classList && node.classList.contains && node.classList.contains('ProseMirror')) ||
(node.tagName === 'TEXTAREA')
);
if (isStrictEditable) {
foundEditor = node;
break;
}
} catch (err) {
// 忽略检测错误,继续后续判定
}
// 次级判定:调用适配器的 isValidTextarea(适配器可能有更严格或特殊逻辑)
try {
if (this.siteAdapter.isValidTextarea(node)) {
foundEditor = node;
break;
}
} catch (err) {
// 忽略 isValidTextarea 抛出的意外错误
}
// 最后的兜底:检查常见选择器匹配
try {
if (node.matches && node.matches('[contenteditable="true"], .ProseMirror, textarea')) {
foundEditor = node;
break;
}
} catch (err) {
// 忽略 matches 抛出的错误
}
}
if (foundEditor) {
// 更新适配器的 textarea 引用,防止后续操作找不到元素
try { this.siteAdapter.textarea = foundEditor; } catch (err) { /* 忽略 */ }
// 如果有选中的提示词,清除悬浮条
if (this.selectedPrompt) {
setTimeout(() => { this.clearSelectedPrompt(); }, 100);
}
// 针对 Gemini Business 适配器,根据设置决定是否调用 clearTextarea 修复中文输入问题
if (this.siteAdapter instanceof GeminiBusinessAdapter && this.settings.clearTextareaOnSend) {
setTimeout(() => { this.siteAdapter.clearTextarea(); }, 200);
}
}
});
}
makeDraggable() {
const panel = document.getElementById('gemini-helper-panel');
const header = panel?.querySelector('.prompt-panel-header');
if (!panel || !header) return;
let isDragging = false, currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.prompt-panel-controls')) return;
e.preventDefault(); // 阻止文本选中
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
isDragging = true;
// 拖动时禁止全局文本选中
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
panel.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
// 恢复文本选中
document.body.style.userSelect = '';
}
});
}
}
function init() {
// 初始化站点注册表
const siteRegistry = new SiteRegistry();
siteRegistry.register(new GeminiBusinessAdapter()); // 优先检测
siteRegistry.register(new GeminiAdapter());
siteRegistry.register(new GensparkAdapter());
const currentAdapter = siteRegistry.detect();
if (!currentAdapter) {
console.log('Gemini Helper: 未匹配到当前站点,跳过初始化。');
return;
}
console.log(`Gemini Helper: 已匹配站点 - ${currentAdapter.getName()}`);
setTimeout(() => {
try {
new GeminiHelper(currentAdapter);
} catch (error) {
console.error('Gemini Helper 启动失败', error);
}
}, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();