您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A unified interface for scrolling, navigation in ChatGPT, GROK, and DEEPSEEK AI chats with multiple themes. 为ChatGPT、GROK和DEEPSEEK AI聊天提供统一的滚动导航界面,支持多种主题风格和自定义设置。
// ==UserScript== // @name ChatGPT, GROK, and DEEPSEEK AI WEB Chat Scroll Navigator Tool(AI网页聊天智能滚动导航工具) // @namespace http://tampermonkey.net/ // @version 0.24 // @description A unified interface for scrolling, navigation in ChatGPT, GROK, and DEEPSEEK AI chats with multiple themes. 为ChatGPT、GROK和DEEPSEEK AI聊天提供统一的滚动导航界面,支持多种主题风格和自定义设置。 // @author Lepturus // @match *://chatgpt.com/* // @match *://chat.deepseek.com/* // @match *://grok.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== (function () { 'use strict'; // --- 1. CONFIGURATION & STATE --- // const CONFIG = { platforms: { chatgpt: { container: '.flex.basis-auto.flex-col.grow.overflow-hidden div.relative.h-full div.overflow-y-auto' }, grok: { container: '.scrollbar-gutter-stable' }, deepseek: { container: '.scrollable' }, generic: { container: 'html, body' } }, defaults: { showAutoScrollBtn: true, showSectionNavBtn: true, language: 'en', themeColor: '#007AFF', themeStyle: 'minimal', scrollSpeed: 5, showProgressIndicator: true, alwaysShowButtons: true, iconSet: 'minimal', buttonPosition: 'bottom-right' }, iconSets: { minimal: { settings: '⚙️', // 齿轮 section: '📄', // 文档 autoscroll: '⏯️', // 播放/暂停 scrollTop: '⬆️', // 向上箭头 scrollBottom: '⬇️' // 向下箭头 }, colorful: { settings: '🎨', // 调色板 section: '📖', // 书本 autoscroll: '🚀', // 火箭 scrollTop: '👆', // 向上手指 scrollBottom: '👇' // 向下手指 }, tech: { settings: '🔧', // 扳手 section: '📊', // 图表 autoscroll: '⚡', // 闪电 scrollTop: '🔼', // 三角形上 scrollBottom: '🔽' // 三角形下 }, forest: { settings: '🌿', // 叶子 section: '🌳', // 树 autoscroll: '🌊', // 波浪 scrollTop: '⛰️', // 山峰 scrollBottom: '🌱' // 幼苗 }, anime: { settings: '✨', // 星星 section: '📑', // 书签 autoscroll: '🎵', // 音乐 scrollTop: '↑', // 简洁向上箭头 scrollBottom: '↓' // 简洁向下箭头 } }, themes: { minimal: { name: { en: 'Minimal', zh: '简约风格' }, bgColor: '#2c2c2e', hoverColor: '#444', activeColor: 'var(--enh-nav-theme)', textColor: 'white', shadow: '0 4px 12px rgba(0,0,0,0.3)', panelBg: '#1e1e1e', panelBorder: '1px solid #e0e0e0', selectColor: 'black', }, colorful: { name: { en: 'Colorful', zh: '多彩风格' }, bgColor: 'linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%)', hoverColor: 'linear-gradient(135deg, #ff6a6e 0%, #f8c0b4 100%)', activeColor: 'linear-gradient(135deg, #ff4b50 0%, #f6b0a0 100%)', textColor: '#7c4dff', shadow: '0 8px 16px rgba(124, 77, 255, 0.4)', panelBg: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)', panelBorder: '1px solid rgba(124, 77, 255, 0.3)', selectColor: 'rgba(255,255,255,0.2)', }, tech: { name: { en: 'Tech', zh: '科技风格' }, bgColor: 'rgba(0, 20, 40, 0.8)', hoverColor: 'rgba(0, 100, 200, 0.8)', activeColor: 'var(--enh-nav-theme)', textColor: '#00e5ff', shadow: '0 0 15px rgba(0, 229, 255, 0.7)', panelBg: 'rgba(0, 30, 60, 0.95)', panelBorder: '1px solid rgba(0, 229, 255, 0.5)', selectColor: 'rgba(255,255,255,0.2)', }, forest: { name: { en: 'Forest', zh: '森林风格' }, bgColor: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', hoverColor: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', activeColor: 'linear-gradient(135deg, #2ed573 0%, #1ed6c8 100%)', textColor: '#004d40', shadow: '0 6px 14px rgba(0, 77, 64, 0.4)', panelBg: 'linear-gradient(135deg, #7bc6cc 0%, #be93c5 100%)', panelBorder: '1px solid rgba(0, 77, 64, 0.3)', selectColor: 'rgba(255,255,255,0.2)', }, anime: { name: { en: 'Cute', zh: '可爱风格' }, bgColor: 'linear-gradient(135deg, #ffcbf2 0%, #e0c3fc 100%)', hoverColor: 'linear-gradient(135deg, #ffafcc 0%, #cdb4db 100%)', activeColor: 'linear-gradient(135deg, #ff85a1 0%, #b8a1d9 100%)', textColor: '#ff006e', shadow: '0 8px 20px rgba(255, 0, 110, 0.4)', panelBg: 'linear-gradient(135deg, #f6d5f7 0%, #fbe9d7 100%)', panelBorder: '1px solid rgba(255, 0, 110, 0.3)', selectColor: 'rgba(255,255,255,0.2)', } }, i18n: { zh: { // Main Titles & Tooltips mainTitle: '导航', autoScroll: '自动滚动', pauseAutoScroll: '暂停滚动', sectionNav: '章节导航', settings: '设置', scrollToTop: '滚动到顶部', scrollToBottom: '滚动到底部', // Settings Panel settingsTitle: '脚本设置', showHideButtons: '显示/隐藏功能按钮', showReadingButton: '阅读模式', showAutoScrollButton: '自动滚动', showSectionNavButton: '章节导航', showProgressIndicator: '显示进度指示器', alwaysShowButtons: '常驻显示按钮', language: '语言', themeColor: '主题颜色', themeStyle: '主题风格', scrollSpeed: '自动滚动速度', speedValue: (val) => ({ 1: '很慢', 3: '慢', 5: '中等', 8: '快', 10: '很快' })[val] || '自定义', // Section Nav Panel navTitle: '页面导航', noHeadings: '未找到章节标题。', iconSet: '图标风格', iconSetMinimal: '简约', iconSetColorful: '多彩', iconSetTech: '科技', iconSetForest: '森林', iconSetAnime: '可爱', buttonPosition: '按钮位置', positionBottomRight: '右下', positionTopRight: '右上', positionMiddleRight: '右中' }, en: { // Main Titles & Tooltips mainTitle: 'Navigate', autoScroll: 'Auto-Scroll', pauseAutoScroll: 'Pause Auto-Scroll', sectionNav: 'Section Nav', settings: 'Settings', scrollToTop: 'Scroll to Top', scrollToBottom: 'Scroll to Bottom', // Settings Panel settingsTitle: 'Script Settings', showHideButtons: 'Show/Hide Feature Buttons', showReadingButton: 'Reading Mode', showAutoScrollButton: 'Auto-Scroll', showSectionNavButton: 'Section Nav', showProgressIndicator: 'Show Progress Indicator', alwaysShowButtons: 'Always Show Buttons', language: 'Language', themeColor: 'Theme Color', themeStyle: 'Theme Style', scrollSpeed: 'Auto-Scroll Speed', speedValue: (val) => ({ 1: 'Very Slow', 3: 'Slow', 5: 'Medium', 8: 'Fast', 10: 'Very Fast' })[val] || 'Custom', // Section Nav Panel navTitle: 'Section Navigation', noHeadings: 'No section headings found.', iconSet: 'Icon Style', iconSetMinimal: 'Minimal', iconSetColorful: 'Colorful', iconSetTech: 'Tech', iconSetForest: 'Forest', iconSetAnime: 'Cute', buttonPosition: 'Button Position', positionBottomRight: 'Bottom Right', positionTopRight: 'Top Right', positionMiddleRight: 'Middle Right' } } }; let STATE = { isInitialized: false, isAutoScrolling: false, autoScrollInterval: null, scrollContainer: null, settings: {}, currentPlatform: 'generic', }; // --- 2. CORE LOGIC --- // /** Gets a setting value, falling back to default */ function getSetting(key) { return GM_getValue(key, CONFIG.defaults[key]); } /** Loads all settings into the STATE object */ function loadSettings() { STATE.settings = { showAutoScrollBtn: getSetting('showAutoScrollBtn'), showSectionNavBtn: getSetting('showSectionNavBtn'), language: getSetting('language'), themeColor: getSetting('themeColor'), iconSet: getSetting('iconSet'), themeStyle: getSetting('themeStyle'), scrollSpeed: getSetting('scrollSpeed'), showProgressIndicator: getSetting('showProgressIndicator'), alwaysShowButtons: getSetting('alwaysShowButtons'), buttonPosition: getSetting('buttonPosition') }; } /** Finds the correct scrollable element on the page */ function findScrollContainer() { const { host } = window.location; let platform = 'generic'; if (host.includes('chatgpt.com')) platform = 'chatgpt'; else if (host.includes('grok.com')) platform = 'grok'; else if (host.includes('deepseek.com')) platform = 'deepseek'; STATE.currentPlatform = platform; const selector = CONFIG.platforms[platform].container; if (platform === 'deepseek') { const containers = document.querySelectorAll(selector); return containers.length > 1 ? containers[1] : containers[0]; } return platform === 'generic' ? (document.scrollingElement || document.documentElement) : document.querySelector(selector); } /** Handles the main scroll button click */ function handleScrollClick() { if (!STATE.scrollContainer) return; const isNearTop = STATE.scrollContainer.scrollTop < 100; STATE.scrollContainer.scrollTo({ top: isNearTop ? STATE.scrollContainer.scrollHeight : 0, behavior: 'smooth' }); } function updateIcons() { const iconSet = CONFIG.iconSets[STATE.settings.iconSet] || CONFIG.iconSets.minimal; const settingsBtn = document.getElementById('enh-nav-settings-btn'); if (settingsBtn) settingsBtn.textContent = iconSet.settings; const sectionBtn = document.getElementById('enh-nav-section-btn'); if (sectionBtn) sectionBtn.textContent = iconSet.section; const autoscrollBtn = document.getElementById('enh-nav-autoscroll-btn'); if (autoscrollBtn) autoscrollBtn.textContent = iconSet.autoscroll; updateScrollArrow(); } function updateScrollArrow() { if (!STATE.scrollContainer) return; const arrowEl = document.getElementById('enh-nav-scroll-arrow'); const iconSet = CONFIG.iconSets[STATE.settings.iconSet] || CONFIG.iconSets.minimal; const isNearTop = STATE.scrollContainer.scrollTop < 100; if (arrowEl) arrowEl.textContent = isNearTop ? iconSet.scrollBottom : iconSet.scrollTop; } /** Toggles Auto-Scroll */ function toggleAutoScroll() { STATE.isAutoScrolling = !STATE.isAutoScrolling; const btn = document.getElementById('enh-nav-autoscroll-btn'); if (btn) btn.classList.toggle('active', STATE.isAutoScrolling); if (STATE.isAutoScrolling) { STATE.autoScrollInterval = setInterval(() => { if (!STATE.scrollContainer) return; const atBottom = STATE.scrollContainer.scrollTop + STATE.scrollContainer.clientHeight >= STATE.scrollContainer.scrollHeight - 2; if (atBottom) { toggleAutoScroll(); // Stop scrolling } else { const scrollAmount = Math.max(1, STATE.settings.scrollSpeed / 5); STATE.scrollContainer.scrollBy(0, scrollAmount); } }, 20); } else { clearInterval(STATE.autoScrollInterval); } updateUIText(); // Update title } /** Applies the selected button position */ function applyButtonPosition(position) { const container = document.getElementById('enh-nav-container'); const hoverArea = document.getElementById('enh-nav-hover-area'); if (!container || !hoverArea) return; container.classList.remove( 'enh-nav-pos-bottom-right', 'enh-nav-pos-top-right', 'enh-nav-pos-middle-right' ); hoverArea.classList.remove( 'enh-nav-pos-bottom-right', 'enh-nav-pos-top-right', 'enh-nav-pos-middle-right' ); container.classList.add(`enh-nav-pos-${position}`); hoverArea.classList.add(`enh-nav-pos-${position}`); updatePanelPositions(position); } /** Updates panel positions based on button position */ function updatePanelPositions(position) { const panels = document.querySelectorAll('.enh-nav-panel'); panels.forEach(panel => { panel.classList.remove( 'enh-nav-pos-bottom-right', 'enh-nav-pos-top-right', 'enh-nav-pos-middle-right' ); panel.classList.add(`enh-nav-pos-${position}`); }); } /** Updates the UI text based on the current language without reloading */ function updateUIText() { const lang = STATE.settings.language; const translations = CONFIG.i18n[lang]; document.querySelectorAll('[data-i18n-key]').forEach(el => { const key = el.dataset.i18nKey; const prop = el.dataset.i18nProp || 'textContent'; if (translations[key]) el[prop] = translations[key]; }); // fix theme style lang not change const themeSelect = document.getElementById('enh-nav-theme-style'); if (themeSelect) { Array.from(themeSelect.options).forEach(option => { const themeId = option.value; const theme = CONFIG.themes[themeId]; if (theme) { option.textContent = theme.name[lang] || theme.name.en; } }); } const autoScrollBtn = document.getElementById('enh-nav-autoscroll-btn'); if (autoScrollBtn) autoScrollBtn.title = STATE.isAutoScrolling ? translations.pauseAutoScroll : translations.autoScroll; const mainBtn = document.getElementById('enh-nav-main-btn'); if (mainBtn && STATE.scrollContainer) { const isNearTop = STATE.scrollContainer.scrollTop < 100; mainBtn.title = isNearTop ? translations.scrollToBottom : translations.scrollToTop; } // Update speed value text const speedValueEl = document.getElementById('enh-nav-speed-value'); if (speedValueEl) { speedValueEl.textContent = translations.speedValue(STATE.settings.scrollSpeed); } } // --- 3. UI CREATION & UPDATES --- // /** Injects all CSS into the page */ function injectStyles() { const theme = CONFIG.themes[STATE.settings.themeStyle] || CONFIG.themes.minimal; GM_addStyle(` :root { --enh-nav-theme: ${STATE.settings.themeColor}; --enh-nav-bg: ${theme.bgColor}; --enh-nav-hover: ${theme.hoverColor}; --enh-nav-active: ${theme.activeColor}; --enh-nav-text: ${theme.textColor}; --enh-nav-shadow: ${theme.shadow}; --enh-nav-panel-bg: ${theme.panelBg}; --enh-nav-panel-border: ${theme.panelBorder}; --enh-nav-select: ${theme.selectColor}; } #enh-nav-container { position: fixed; right: 15px; bottom: 15px; z-index: 99999; display: flex !important; flex-direction: column; align-items: center; transition: opacity 0.3s ease; } #enh-nav-container.hidden { opacity: 0; pointer-events: none; } #enh-nav-container:hover, #enh-nav-container.always-visible { opacity: 1; pointer-events: auto; } #enh-nav-hover-area { position: fixed; right: 0; bottom: 0; width: 60px; height: 60px; z-index: 99998; } .enh-nav-btn { background: var(--enh-nav-bg); color: var(--enh-nav-text); border-radius: 50%; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; font-size: 24px; cursor: pointer; border: none; box-shadow: var(--enh-nav-shadow); transition: all 0.3s ease; user-select: none; } .enh-nav-btn:hover { transform: scale(1.1); background: var(--enh-nav-hover); } .enh-nav-btn.active { background: var(--enh-nav-active); } #enh-nav-main-btn { position: relative; font-weight: bold; } #enh-nav-progress-circle { position: absolute; width: 100%; height: 100%; transform: rotate(-90deg); } #enh-nav-progress-circle-bar { stroke: var(--enh-nav-theme); stroke-width: 4; fill: transparent; transition: stroke-dashoffset 0.1s linear; } #enh-nav-progress-text { font-size: 12px; font-weight: bold; color: var(--enh-nav-text); } #enh-nav-menu { display: flex; flex-direction: column; align-items: center; margin-bottom: 10px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transform-origin: bottom center; transform: scale(0.8) translateY(20px); opacity: 0; pointer-events: none; } #enh-nav-container:hover #enh-nav-menu { transform: scale(1) translateY(0); opacity: 1; pointer-events: auto; } .enh-nav-menu-btn { width: 40px; height: 40px; font-size: 16px; margin-top: 10px; } .enh-nav-panel { position: fixed; right: 80px; bottom: 15px; width: 210px; background: var(--enh-nav-panel-bg); color: var(--enh-nav-text); border-radius: 16px; padding: 11px; box-shadow: var(--enh-nav-shadow); border: var(--enh-nav-panel-border); opacity: 0; transform: translateX(20px); transition: all 0.3s ease; pointer-events: none; z-index: 99998; backdrop-filter: blur(10px); } .enh-nav-panel.visible { opacity: 1; transform: translateX(0); pointer-events: auto; } .enh-nav-panel h3 { margin: 0 0 11px; font-size: 15px; border-bottom: 1px solid rgba(255,255,255,0.2); padding-bottom: 10px; text-align: center; } .enh-nav-panel .close-btn { position: absolute; top: 15px; right: 15px; cursor: pointer; font-size: 16px; opacity: 0.7; transition: all 0.2s; } .enh-nav-panel .close-btn:hover { opacity: 1; transform: scale(1.1); } .enh-nav-setting-row { margin-bottom: 9px; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.1); } .enh-nav-setting-row label { display: flex; justify-content: space-between; align-items: center; font-size: 14px; width: 100%; } .enh-nav-panel input[type=checkbox] { transform: scale(1.2); accent-color: var(--enh-nav-theme); } .enh-nav-panel input[type=color] { width: 40px; height: 25px; border: none; background: none; padding: 0; cursor: pointer; } .enh-nav-panel input[type=range] { width: 100%; margin-top: 5px; accent-color: var(--enh-nav-theme); } .enh-nav-panel select { background: var(--enh-nav-select); color: var(--enh-nav-text); border: 1px solid rgba(255,255,255,0.2); border-radius: 8px; padding: 5px 10px; cursor: pointer; } #enh-nav-section-panel { width: 350px; height: 500px; max-height: 70vh; right: 90px; transform: translateX(30px) scale(0.95); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } #enh-nav-section-panel.visible { transform: translateX(0) scale(1); } #enh-nav-section-panel ul { list-style: none; padding: 0; margin: 0; max-height: calc(100% - 60px); overflow-x: hidden; overflow-y: auto; } #enh-nav-section-panel ul::-webkit-scrollbar { width: 8px; } #enh-nav-section-panel ul::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.1); border-radius: 4px; } #enh-nav-section-panel ul::-webkit-scrollbar-thumb { background: var(--enh-nav-theme); border-radius: 4px; } #enh-nav-section-panel ul::-webkit-scrollbar-thumb:hover { background: var(--enh-nav-active); } #enh-nav-section-panel li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding: 12px 15px; cursor: pointer; border-radius: 10px; font-size: 14px; margin-bottom: 8px; transition: all 0.3s ease; background: rgba(255,255,255,0.05); opacity: 0; transform: translateY(10px); transition-delay: calc(var(--index, 0) * 0.05s); } #enh-nav-section-panel.visible li { opacity: 1; transform: translateY(0); } #enh-nav-section-panel li:hover { background-color: var(--enh-nav-theme); color: white; transform: translateX(5px) translateY(0); } #enh-nav-section-panel li[data-i18n-key="noHeadings"] { text-align: center; padding: 30px; font-size: 16px; opacity: 0.7; white-space: normal; transform: none; } .theme-preview { display: inline-block; width: 20px; height: 20px; border-radius: 50%; margin-right: 8px; vertical-align: middle; } .speed-container { display: flex; flex-direction: column; margin-top: 5px; } /* 位置样式 */ #enh-nav-container.enh-nav-pos-bottom-right { right: 15px; bottom: 15px; top: auto; transform: none; } #enh-nav-container.enh-nav-pos-top-right { right: 15px; top: 15px; bottom: auto; transform: none; } #enh-nav-container.enh-nav-pos-middle-right { right: 15px; top: 50%; bottom: auto; transform: translateY(-50%); } /* 菜单方向 */ #enh-nav-container.enh-nav-pos-bottom-right #enh-nav-menu { flex-direction: column; margin-bottom: 10px; margin-top: 0; } #enh-nav-container.enh-nav-pos-top-right #enh-nav-menu, #enh-nav-container.enh-nav-pos-middle-right #enh-nav-menu { flex-direction: column-reverse; margin-bottom: 0; margin-top: 10px; } #enh-nav-container.enh-nav-pos-top-right:hover #enh-nav-menu, #enh-nav-container.enh-nav-pos-middle-right:hover #enh-nav-menu { transform: scale(1) translateY(0); } /* 面板位置 */ #enh-nav-container.enh-nav-pos-bottom-right .enh-nav-panel { bottom: 15px; top: auto; } #enh-nav-container.enh-nav-pos-top-right .enh-nav-panel { top: 15px; bottom: auto; } #enh-nav-container.enh-nav-pos-middle-right .enh-nav-panel { top: 50%; bottom: auto; transform: translateX(20px) translateY(-50%); } #enh-nav-container.enh-nav-pos-middle-right .enh-nav-panel.visible { transform: translateX(0) translateY(-50%); } /* 悬停区域位置 */ #enh-nav-hover-area.enh-nav-pos-bottom-right { right: 0; bottom: 0; top: auto; } #enh-nav-hover-area.enh-nav-pos-top-right { right: 0; top: 0; bottom: auto; } #enh-nav-hover-area.enh-nav-pos-middle-right { right: 0; top: 50%; bottom: auto; transform: translateY(-50%); } /* 按钮位置样式*/ #enh-nav-container.enh-nav-pos-bottom-right { right: 15px; bottom: 15px; top: auto; transform: none; } #enh-nav-container.enh-nav-pos-top-right { right: 15px; top: 15px; bottom: auto; transform: none; } #enh-nav-container.enh-nav-pos-middle-right { right: 15px; top: 50%; bottom: auto; transform: translateY(-50%); } /* 调整菜单方向 */ #enh-nav-container.enh-nav-pos-bottom-right #enh-nav-menu { flex-direction: column; margin-bottom: 10px; margin-top: 0; } #enh-nav-container.enh-nav-pos-top-right #enh-nav-menu { flex-direction: column; margin-bottom: 10px; margin-top: 0; } #enh-nav-container.enh-nav-pos-middle-right #enh-nav-menu { flex-direction: column; margin-bottom: 10px; margin-top: 0; } /* 调整面板位置 */ #enh-nav-container.enh-nav-pos-bottom-right .enh-nav-panel { right: 80px; bottom: 15px; top: auto; } #enh-nav-container.enh-nav-pos-top-right .enh-nav-panel { right: 80px; top: 80px; /* 在按钮下方 */ bottom: auto; } #enh-nav-container.enh-nav-pos-middle-right .enh-nav-panel { right: 80px; top: calc(50% + 40px); /* 在按钮下方 */ bottom: auto; transform: translateX(20px) translateY(-50%); } #enh-nav-container.enh-nav-pos-middle-right .enh-nav-panel.visible { transform: translateX(0) translateY(-50%); } /* 确保面板在正确位置 */ .enh-nav-panel { transform: translateX(20px); } .enh-nav-panel.visible { transform: translateX(0); } /* 调整悬停区域位置 */ #enh-nav-hover-area.enh-nav-pos-bottom-right { right: 0; bottom: 0; top: auto; width: 80px; height: 80px; } #enh-nav-hover-area.enh-nav-pos-top-right { right: 0; top: 0; bottom: auto; width: 80px; height: 80px; } #enh-nav-hover-area.enh-nav-pos-middle-right { right: 0; top: 50%; bottom: auto; transform: translateY(-50%); width: 80px; height: 80px; } `); } /** Creates the main UI elements */ function createUI() { const container = document.createElement('div'); container.id = 'enh-nav-container'; if (!STATE.settings.alwaysShowButtons) { container.classList.add('hidden'); } else { container.classList.add('always-visible'); } const hoverArea = document.createElement('div'); hoverArea.id = 'enh-nav-hover-area'; hoverArea.classList.add(`enh-nav-pos-${STATE.settings.buttonPosition || 'bottom-right'}`); document.body.appendChild(hoverArea); const progressRadius = 22; const progressCircumference = 2 * Math.PI * progressRadius; container.innerHTML = ` <!-- Menu Buttons (appear on hover) --> <div id="enh-nav-menu"> <button id="enh-nav-settings-btn" class="enh-nav-btn enh-nav-menu-btn" title="Settings" data-i18n-prop="title" data-i18n-key="settings">⚙️</button> <button id="enh-nav-section-btn" class="enh-nav-btn enh-nav-menu-btn" title="Section Nav" data-i18n-prop="title" data-i18n-key="sectionNav">📄</button> <button id="enh-nav-autoscroll-btn" class="enh-nav-btn enh-nav-menu-btn" title="Auto-Scroll" data-i18n-prop="title" data-i18n-key="autoScroll">⏯️</button> </div> <!-- Main Scroll Button --> <button id="enh-nav-main-btn" class="enh-nav-btn" title="Scroll to Bottom"> <span id="enh-nav-scroll-arrow">⬇️</span> <span id="enh-nav-progress-text">0%</span> <svg id="enh-nav-progress-circle" width="48" height="48"> <circle id="enh-nav-progress-circle-bar" r="${progressRadius}" cx="24" cy="24" stroke-dasharray="${progressCircumference}" stroke-dashoffset="${progressCircumference}"></circle> </svg> </button> <!-- Settings Panel --> <div id="enh-nav-settings-panel" class="enh-nav-panel" enh-nav-pos-${STATE.settings.buttonPosition || 'bottom-right'}"> <span class="close-btn">✖️</span> <h3 data-i18n-key="settingsTitle">Script Settings</h3> <div class="enh-nav-setting-row"> <strong data-i18n-key="showHideButtons">Show/Hide Buttons</strong> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="showAutoScrollButton">Auto-Scroll</span><input type="checkbox" id="enh-nav-toggle-autoscroll"></label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="showSectionNavButton">Section Nav</span><input type="checkbox" id="enh-nav-toggle-section"></label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="showProgressIndicator">Show Progress Indicator</span><input type="checkbox" id="enh-nav-toggle-progress"></label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="alwaysShowButtons">Always Show Buttons</span><input type="checkbox" id="enh-nav-toggle-always-show"></label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="buttonPosition">Button Position</span> <select id="enh-nav-button-position"> <option value="bottom-right" data-i18n-key="positionBottomRight">Bottom Right</option> <option value="top-right" data-i18n-key="positionTopRight">Top Right</option> <option value="middle-right" data-i18n-key="positionMiddleRight">Middle Right</option> </select> </label> </div> <hr style="border-color: rgba(255,255,255,0.2); margin: 15px 0;"> <div class="enh-nav-setting-row"> <label><span data-i18n-key="language">Language</span> <select id="enh-nav-lang-select"> <option value="en">English</option> <option value="zh">中文</option> </select> </label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="themeStyle">Theme Style</span> <select id="enh-nav-theme-style"> ${Object.entries(CONFIG.themes).map(([id, theme]) => `<option value="${id}">${theme.name[STATE.settings.language] || theme.name.en}</option>` ).join('')} </select> </label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="iconSet">Icon Style</span> <select id="enh-nav-icon-set"> <option value="minimal" data-i18n-key="iconSetMinimal">Minimal</option> <option value="colorful" data-i18n-key="iconSetColorful">Colorful</option> <option value="tech" data-i18n-key="iconSetTech">Tech</option> <option value="forest" data-i18n-key="iconSetForest">Forest</option> <option value="anime" data-i18n-key="iconSetAnime">Anime</option> </select> </label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="themeColor">Theme Color</span><input type="color" id="enh-nav-theme-color"></label> </div> <div class="enh-nav-setting-row"> <label><span data-i18n-key="scrollSpeed">Scroll Speed</span> <span id="enh-nav-speed-value"></span></label> <div class="speed-container"> <input type="range" id="enh-nav-scroll-speed" min="1" max="10" step="1"> </div> </div> </div> <!-- Section Nav Panel --> <div id="enh-nav-section-panel" class="enh-nav-panel" enh-nav-pos-${STATE.settings.buttonPosition || 'bottom-right'}"> <span class="close-btn">✖️</span> <h3 data-i18n-key="navTitle">Page Navigation</h3> <ul id="enh-nav-section-list"></ul> </div> `; document.body.appendChild(container); // Apply initial visibility from settings document.getElementById('enh-nav-autoscroll-btn').style.display = STATE.settings.showAutoScrollBtn ? 'flex' : 'none'; document.getElementById('enh-nav-section-btn').style.display = STATE.settings.showSectionNavBtn ? 'flex' : 'none'; // Apply initial progress indicator setting updateProgressIndicatorVisibility(); } /** Updates the circular progress bar and scroll direction arrow */ function updateScrollState() { if (!STATE.scrollContainer) return; const { scrollTop, scrollHeight, clientHeight } = STATE.scrollContainer; const scrollableHeight = scrollHeight - clientHeight; const arrowEl = document.getElementById('enh-nav-scroll-arrow'); const textEl = document.getElementById('enh-nav-progress-text'); const circleBar = document.getElementById('enh-nav-progress-circle-bar'); if (scrollableHeight <= 0) { if (arrowEl) arrowEl.style.display = 'block'; if (textEl) textEl.style.display = 'none'; if (arrowEl) arrowEl.textContent = '↕️'; return; } const percent = Math.min(Math.round((scrollTop / scrollableHeight) * 100), 100); const isNearTop = scrollTop < 100; const isNearBottom = scrollTop > scrollableHeight - 100; // Show arrow or percentage based on settings const showProgress = STATE.settings.showProgressIndicator; if (isNearTop || isNearBottom || !showProgress) { if (arrowEl) arrowEl.style.display = 'block'; if (textEl) textEl.style.display = 'none'; if (arrowEl) arrowEl.textContent = isNearTop ? '⬇️' : '⬆️'; } else { if (arrowEl) arrowEl.style.display = 'none'; if (textEl) textEl.style.display = 'block'; if (textEl) textEl.textContent = `${percent}%`; } // Update progress circle if (circleBar) { const radius = circleBar.r.baseVal.value; const circumference = 2 * Math.PI * radius; const offset = circumference - (percent / 100) * circumference; circleBar.style.strokeDashoffset = offset; } // Update main button title for accessibility updateScrollArrow(); updateUIText(); } /** Updates progress indicator visibility based on setting */ function updateProgressIndicatorVisibility() { const textEl = document.getElementById('enh-nav-progress-text'); const circleBar = document.getElementById('enh-nav-progress-circle-bar'); const showProgress = STATE.settings.showProgressIndicator; if (textEl && circleBar) { textEl.style.display = showProgress ? 'block' : 'none'; circleBar.style.display = showProgress ? 'block' : 'none'; } updateScrollState(); // Refresh the display } /** Populates the section navigation panel */ function updateSectionNav() { const list = document.getElementById('enh-nav-section-list'); const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); // 包含所有标题级别 list.innerHTML = ''; const unwantedTexts = [ '脚本设置', 'Script Settings', '页面导航', 'Section Navigation', 'Upgrade to SuperGrok', 'Chat history', 'Chats', 'You said:', 'ChatGPT said:', '导航', 'Navigate', '设置', 'Settings' ]; if (headings.length === 0) { list.innerHTML = `<li data-i18n-key="noHeadings" style="opacity: 0.6; text-align: center; padding: 30px;"> ${CONFIG.i18n[STATE.settings.language].noHeadings} </li>`; return; } let hasValidHeadings = false; let index = 0; headings.forEach(h => { const text = h.textContent.trim(); const id = h.id || h.textContent.toLowerCase().replace(/\s+/g, '-'); if (text === '' || unwantedTexts.includes(text)) return; hasValidHeadings = true; const item = document.createElement('li'); item.textContent = text; const level = parseInt(h.tagName.substring(1)); item.style.paddingLeft = `${(level - 1) * 15 + 15}px`; item.style.fontSize = `${18 - level * 2}px`; item.style.fontWeight = level <= 2 ? 'bold' : 'normal'; item.style.setProperty('--index', index); index++; item.addEventListener('mouseenter', () => { h.style.transition = 'background-color 0.2s'; h.style.backgroundColor = 'var(--enh-nav-theme)'; }); item.addEventListener('mouseleave', () => { setTimeout(() => { h.style.backgroundColor = ''; }, 200); }); item.onclick = () => { const element = document.getElementById(id) || h; element.scrollIntoView({ behavior: 'smooth', block: 'start' }); const originalBg = element.style.backgroundColor; element.style.backgroundColor = 'var(--enh-nav-theme)'; element.style.transition = 'background-color 0.3s'; }; list.appendChild(item); }); if (!hasValidHeadings) { list.innerHTML = `<li data-i18n-key="noHeadings" style="opacity: 0.6; text-align: center; padding: 30px;"> ${CONFIG.i18n[STATE.settings.language].noHeadings} </li>`; } } /** Toggles the visibility of a panel */ function togglePanel(panelId, forceState) { const panel = document.getElementById(panelId); if (!panel) return; // Hide other panels // document.querySelectorAll('.enh-nav-panel').forEach(p => { // if (p.id !== panelId) p.classList.remove('visible'); // }); const isVisible = panel.classList.toggle('visible', forceState); // Toggle current panel // If we are opening the section nav, refresh its content if (isVisible && panelId === 'enh-nav-section-panel') { updateSectionNav(); } } /** Applies the selected theme style */ function applyThemeStyle(themeId) { STATE.settings.themeStyle = themeId; GM_setValue('themeStyle', themeId); injectStyles(); // Re-inject styles with the new theme } // --- 4. EVENT LISTENERS --- // /** Binds all event listeners to the UI */ function attachEventListeners() { // Main Actions document.getElementById('enh-nav-main-btn').addEventListener('click', handleScrollClick); document.getElementById('enh-nav-autoscroll-btn').addEventListener('click', toggleAutoScroll); // Panel Toggles document.getElementById('enh-nav-settings-btn').addEventListener('click', () => togglePanel('enh-nav-settings-panel')); document.getElementById('enh-nav-section-btn').addEventListener('click', () => togglePanel('enh-nav-section-panel')); // Close Panel Buttons document.querySelectorAll('.enh-nav-panel .close-btn').forEach(btn => { btn.addEventListener('click', (e) => e.currentTarget.parentElement.classList.remove('visible')); }); // Settings Controls const settingsMap = { 'enh-nav-toggle-autoscroll': { key: 'showAutoScrollBtn', target: '#enh-nav-autoscroll-btn', type: 'toggle' }, 'enh-nav-toggle-section': { key: 'showSectionNavBtn', target: '#enh-nav-section-btn', type: 'toggle' }, 'enh-nav-toggle-progress': { key: 'showProgressIndicator', type: 'value', callback: updateProgressIndicatorVisibility }, 'enh-nav-icon-set': { key: 'iconSet', type: 'value', callback: updateIcons }, 'enh-nav-button-position': { key: 'buttonPosition', type: 'value', callback: (value) => { applyButtonPosition(value); setTimeout(() => { updatePanelPositions(value); }, 100); } }, 'enh-nav-toggle-always-show': { key: 'alwaysShowButtons', type: 'toggle', callback: (value) => { const container = document.getElementById('enh-nav-container'); const hoverArea = document.getElementById('enh-nav-hover-area'); if (value) { container.classList.add('always-visible'); container.classList.remove('hidden'); if (hoverArea) hoverArea.style.display = 'none'; } else { container.classList.remove('always-visible'); container.classList.add('hidden'); if (hoverArea) hoverArea.style.display = 'block'; } } }, 'enh-nav-lang-select': { key: 'language', type: 'value', callback: updateUIText }, 'enh-nav-theme-style': { key: 'themeStyle', type: 'value', callback: applyThemeStyle }, 'enh-nav-theme-color': { key: 'themeColor', type: 'value', callback: (value) => { document.documentElement.style.setProperty('--enh-nav-theme', value); updateUIText(); } }, 'enh-nav-scroll-speed': { key: 'scrollSpeed', type: 'value', callback: updateUIText }, }; for (const [id, config] of Object.entries(settingsMap)) { const el = document.getElementById(id); if (!el) continue; const eventType = (el.type === 'checkbox' || el.tagName === 'SELECT') ? 'change' : 'input'; // Set initial value if (el.type === 'checkbox') el.checked = STATE.settings[config.key]; else el.value = STATE.settings[config.key]; // Add listener el.addEventListener(eventType, (e) => { const value = (e.target.type === 'checkbox') ? e.target.checked : e.target.value; GM_setValue(config.key, value); STATE.settings[config.key] = value; // Apply change instantly if (config.type === 'toggle' && config.target) { document.querySelector(config.target).style.display = value ? 'flex' : 'none'; } if (config.callback) { config.callback(value); } }); } const hoverArea = document.getElementById('enh-nav-hover-area'); const container = document.getElementById('enh-nav-container'); if (hoverArea && container) { hoverArea.addEventListener('mouseenter', () => { if (!STATE.settings.alwaysShowButtons) { container.classList.remove('hidden'); } }); container.addEventListener('mouseleave', () => { if (!STATE.settings.alwaysShowButtons) { container.classList.add('hidden'); } }); } } // --- 5. INITIALIZATION --- // /** Main function to start the script */ function init() { if (document.getElementById('enh-nav-container')) return; STATE.scrollContainer = findScrollContainer(); if (!STATE.scrollContainer) { setTimeout(init, 1000); // Retry if container not found yet return; } loadSettings(); injectStyles(); updateIcons(); createUI(); attachEventListeners(); updateUIText(); updateScrollState(); applyButtonPosition(STATE.settings.buttonPosition); const scrollTarget = (STATE.currentPlatform === 'generic') ? window : STATE.scrollContainer; scrollTarget.addEventListener('scroll', updateScrollState, { passive: true }); const observer = new MutationObserver(() => { const currentContainer = findScrollContainer(); if (STATE.scrollContainer !== currentContainer) { STATE.scrollContainer = currentContainer; const newTarget = (STATE.currentPlatform === 'generic') ? window : STATE.scrollContainer; newTarget.addEventListener('scroll', updateScrollState, { passive: true }); updateScrollState(); } }); observer.observe(document.body, { childList: true, subtree: true }); console.log("Enhanced AI Chat Scroll Navigator initialized."); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();