您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a clickable conversation index on ChatGPT page
// ==UserScript== // @name ChatGPT Conversation Navigator 对话目录导航器 // @name:zh ChatGPT 对话目录导航器 // @name:en ChatGPT Conversation Navigator // @namespace http://tampermonkey.net/ // @version 1.0.9 // @description Add a clickable conversation index on ChatGPT page // @description:zh 为 ChatGPT 页面添加可点击的对话索引 // @description:en Add a clickable conversation index on ChatGPT page // @author tianyw0 // @match https://chatgpt.com/c/* // @grant GM_addStyle // @license MIT // @homepageURL https://github.com/tianyw0/ai-conversation-navigator // @supportURL https://github.com/tianyw0/ai-conversation-navigator/issues // ==/UserScript== (function() { 'use strict'; const MAX_RETRIES = 50; const RETRY_INTERVAL = 1000; let retryCount = 0; const utils = { log(message, type = 'info') { let prefix; switch(type) { case 'error': prefix = '[ChatGPT Navigator] ❌'; console.error(`${prefix} ${message}`); break; case 'warn': prefix = '[ChatGPT Navigator] ⚠️'; console.warn(`${prefix} ${message}`); break; default: prefix = '[ChatGPT Navigator] 🚀'; console.log(`${prefix} ${message}`); } } }; const initializeNavigator = () => { // 原有的初始化逻辑 const chatContainer = document.querySelector('article[data-testid]'); if (!chatContainer && retryCount < MAX_RETRIES) { utils.log(`未找到聊天容器,${RETRY_INTERVAL/1000}秒后重试 (${retryCount + 1}/${MAX_RETRIES})`, 'warn'); retryCount++; setTimeout(initializeNavigator, RETRY_INTERVAL); return; } if (!chatContainer) { utils.log('无法找到聊天容器,初始化失败', 'error'); return; } utils.log('成功找到聊天容器,开始初始化导航'); const existingSidebar = document.getElementById('chatgpt-nav-sidebar'); if (existingSidebar) { utils.log('检测到现有导航栏,正在重置'); existingSidebar.innerHTML = ''; } else { utils.log('创建新的导航栏'); createNavigationSidebar(); } // 找到所有奇数 data-testid 的元素 const existingMessages = Array.from( document.querySelectorAll('article[data-testid^="conversation-turn-"]') ).filter(el => { const id = el.dataset.testid.split('-').pop(); return Number(id) % 2 === 1; }); utils.log(`找到 ${existingMessages.length} 条提问`); existingMessages.forEach(node => createNavigationItem(node)); // 移除加载状态 const loadingElement = document.querySelector('.nav-loading'); if (loadingElement) { loadingElement.remove(); } setupObserver(); setupScrollSpy(); // 添加滚动跟踪 utils.log('导航初始化完成'); }; const setupObserver = () => { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1 && node.tagName === 'ARTICLE' && node.hasAttribute('data-testid')) { createNavigationItem(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true, }); }; const createNavigationSidebar = () => { // 1. 找到目标容器和所有需要的元素 const threadContainer = document.getElementById('thread'); const presentationDiv = threadContainer?.querySelector('div[role="presentation"]'); if (!presentationDiv) { utils.log('未找到目标容器', 'warn'); return; } // 保存原始的三个 div const originalDivs = Array.from(presentationDiv.children); if (originalDivs.length !== 3) { utils.log('目标容器结构不符合预期', 'warn'); return; } const [div1, div2, div3] = originalDivs; // 2. 清空 presentation div presentationDiv.innerHTML = ''; // 3. 创建导航栏和新的 flex 容器 const sidebar = document.createElement('div'); sidebar.id = 'chatgpt-nav-sidebar'; // 设置导航栏样式 sidebar.style.position = 'absolute'; sidebar.style.zIndex = '1000'; sidebar.style.left = '0'; sidebar.style.top = '0'; sidebar.style.width = 'min(260px, 25%)'; sidebar.style.height = 'calc(100% - 104px)'; sidebar.style.marginTop = '104px'; sidebar.style.flexShrink = '0'; sidebar.style.overflowY = 'auto'; sidebar.style.fontSize = '16px'; sidebar.style.fontWeight = '500'; sidebar.style.borderRadius = '0'; sidebar.style.backdropFilter = 'none'; // 检测当前主题并设置相应的背景色 updateSidebarTheme(sidebar); // 创建新的 flex 容器包装 div2 和导航栏 const newDiv2 = document.createElement('div'); newDiv2.style.display = 'flex'; newDiv2.style.flexDirection = 'row'; newDiv2.style.width = '100%'; newDiv2.style.height = '100%'; newDiv2.style.overflow = 'hidden'; newDiv2.style.position = 'relative'; // 添加相对定位作为定位上下文 newDiv2.appendChild(sidebar); newDiv2.appendChild(div2); // 4. 重新组装所有元素 presentationDiv.appendChild(div1); presentationDiv.appendChild(newDiv2); presentationDiv.appendChild(div3); // 添加样式 const style = document.createElement('style'); style.id = 'chatgpt-nav-styles'; style.textContent = getStylesByTheme(); document.head.appendChild(style); // 添加加载状态 const loading = document.createElement('div'); loading.className = 'nav-loading'; sidebar.appendChild(loading); }; // 获取当前主题 const getCurrentTheme = () => { // 检查是否有深色模式的标记 // 可以通过检查body的class或者某些特定元素的样式来判断 const isDarkMode = document.documentElement.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches; return isDarkMode ? 'dark' : 'light'; }; // 根据主题更新侧边栏样式 const updateSidebarTheme = (sidebar) => { const theme = getCurrentTheme(); if (theme === 'dark') { sidebar.style.backgroundColor = '#212121'; sidebar.style.borderRight = '1px solid rgba(255,255,255,0.08)'; } else { sidebar.style.backgroundColor = '#FFFFFF'; sidebar.style.borderRight = '1px solid rgba(0,0,0,0.08)'; } }; // 根据主题获取样式 const getStylesByTheme = () => { const theme = getCurrentTheme(); // 基础样式 let styles = ` #chatgpt-nav-sidebar { opacity: 0; transform: translateX(-20px); animation: slideIn 0.3s ease forwards; transition: opacity 0.3s ease, transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease; left: 20px !important; right: auto !important; } @media (max-width: 1024px) { #chatgpt-nav-sidebar { display: none; } } @keyframes slideIn { to { opacity: 1; transform: translateX(0); } } #chatgpt-nav-sidebar::-webkit-scrollbar { width: 4px; } #chatgpt-nav-sidebar::-webkit-scrollbar-track { background-color: transparent; } #chatgpt-nav-sidebar a { display: block; padding: 8px 12px; margin: 0; border-radius: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; line-height: 1.5; font-size: 16px; font-weight: 400; transition: background 0.2s, border-radius 0.2s, color 0.2s; background-color: transparent; } #chatgpt-nav-sidebar .nav-item-wrapper { padding: 2px 0; width: calc(100% - 24px); margin-left: 12px; } #chatgpt-nav-sidebar .nav-item-wrapper:last-child { border-bottom: none; } @keyframes spin { to { transform: rotate(360deg); } } @keyframes dots { 0% { content: "加载中"; } 33% { content: "加载中."; } 66% { content: "加载中.."; } 100% { content: "加载中..."; } } `; // 根据主题添加特定样式 if (theme === 'dark') { styles += ` #chatgpt-nav-sidebar { background: #212121; border-right: 1px solid rgba(255,255,255,0.08); } #chatgpt-nav-sidebar::-webkit-scrollbar-thumb { background-color: rgba(217, 217, 227, 0.2); border-radius: 2px; } #chatgpt-nav-sidebar a { color: #ececf1; text-decoration: none; } #chatgpt-nav-sidebar a:hover { background-color: #303030; border-radius: 1.5rem; } #chatgpt-nav-sidebar .nav-index { color: #6e6e80; margin-right: 6px; font-size: 15px; } #chatgpt-nav-sidebar .nav-item-wrapper { border-bottom: 1px solid rgba(255,255,255,0.06); } #chatgpt-nav-sidebar a.active { background-color: #303030; border-radius: 1.5rem; font-weight: 600; color: #fff; } .nav-loading { display: flex; justify-content: center; align-items: center; padding: 20px; color: #ececf1; gap: 8px; } .nav-loading::before { content: ""; width: 16px; height: 16px; border: 2px solid rgba(217, 217, 227, 0.2); border-top-color: #ececf1; border-radius: 50%; animation: spin 1s linear infinite; } `; } else { styles += ` #chatgpt-nav-sidebar { background: #FFFFFF; border-right: 1px solid rgba(0,0,0,0.08); } #chatgpt-nav-sidebar::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.2); border-radius: 2px; } #chatgpt-nav-sidebar a { color: #0D0D0D; text-decoration: none; } #chatgpt-nav-sidebar a:hover { background-color: #F4F4F4; border-radius: 1.5rem; } #chatgpt-nav-sidebar .nav-index { color: #6e6e80; margin-right: 6px; font-size: 15px; } #chatgpt-nav-sidebar .nav-item-wrapper { border-bottom: 1px solid rgba(0,0,0,0.06); } #chatgpt-nav-sidebar a.active { background-color: #F4F4F4; border-radius: 1.5rem; font-weight: 600; color: #0D0D0D; } .nav-loading { display: flex; justify-content: center; align-items: center; padding: 20px; color: #0D0D0D; gap: 8px; } .nav-loading::before { content: ""; width: 16px; height: 16px; border: 2px solid rgba(0, 0, 0, 0.1); border-top-color: #0D0D0D; border-radius: 50%; animation: spin 1s linear infinite; } `; } // 共用的加载动画样式 styles += ` .nav-loading::after { content: "加载中"; animation: dots 1.5s infinite; } `; return styles; }; // 监听主题变化并更新样式 const setupThemeObserver = () => { // 监听 body 的 class 变化来检测主题切换 const observer = new MutationObserver(() => { const sidebar = document.getElementById('chatgpt-nav-sidebar'); if (sidebar) { updateSidebarTheme(sidebar); // 更新样式表 const styleElement = document.getElementById('chatgpt-nav-styles'); if (styleElement) { styleElement.textContent = getStylesByTheme(); } } }); observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); // 监听系统主题变化 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { const sidebar = document.getElementById('chatgpt-nav-sidebar'); if (sidebar) { updateSidebarTheme(sidebar); // 更新样式表 const styleElement = document.getElementById('chatgpt-nav-styles'); if (styleElement) { styleElement.textContent = getStylesByTheme(); } } }); }; function createNavigationItem(node) { const sidebar = document.getElementById('chatgpt-nav-sidebar'); if (!sidebar) { utils.log('导航栏不存在,无法创建导航项', 'error'); return; } const dataTestId = node.getAttribute('data-testid'); if (!dataTestId) { utils.log('节点缺少 data-testid 属性', 'warn'); return; } const isUserQuestion = parseInt(dataTestId.split('-')[2]) % 2 === 1; const id = `nav-${dataTestId}`; // 转义HTML内容 const escapeHtml = (unsafe) => { return unsafe .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); }; const textContent = (node.querySelector('.whitespace-pre-wrap')?.innerText.trim() || node.innerText.trim()) .split(/[\n\r]+/) .join(' ') .replace(/\s+/g, ' '); const displayText = textContent.length > 20 ? escapeHtml(textContent.slice(0, 20)) + '...' : escapeHtml(textContent); if (!isUserQuestion) { utils.log('非用户提问,跳过', 'warn'); return; } // 检查是否已存在相同 ID 的导航项 const existingNavItem = sidebar.querySelector(`[data-nav-id="${id}"]`); if (existingNavItem) { // 比较内容是否相同 const existingText = existingNavItem.getAttribute('title'); if (existingText !== textContent) { utils.log(`导航项 ${id} 内容已更新,正在更新显示`, 'info'); // 更新现有导航项的内容 existingNavItem.innerHTML = `<span class="nav-index">${existingNavItem.querySelector('.nav-index').textContent}</span> ${displayText}`; existingNavItem.setAttribute('title', textContent); } else { utils.log(`导航项 ${id} 已存在且内容相同,跳过`, 'info'); } return; } // 创建新的导航项 const existingNavItems = sidebar.querySelectorAll('.nav-item-wrapper'); const navItemsCount = existingNavItems.length + 1; const wrapper = document.createElement('div'); wrapper.className = 'nav-item-wrapper'; const navItem = document.createElement('div'); navItem.innerHTML = `<a href="#${id}" data-nav-id="${id}" title="${escapeHtml(textContent)}"><span class="nav-index">${navItemsCount}.</span> ${displayText}</a>`; wrapper.appendChild(navItem); sidebar.appendChild(wrapper); node.id = id; }; // 优化滚动跟踪功能 const setupScrollSpy = () => { const sidebar = document.getElementById('chatgpt-nav-sidebar'); if (!sidebar) return; let ticking = false; let navLinks = []; let contentSections = []; let isMouseOverSidebar = false; // 用于跟踪鼠标是否在导航栏上 // 监听鼠标进入和离开导航栏的事件 sidebar.addEventListener('mouseenter', () => { isMouseOverSidebar = true; }); sidebar.addEventListener('mouseleave', () => { isMouseOverSidebar = false; }); // 获取所有导航链接和对应内容区域 const updateAnchors = () => { navLinks = Array.from(sidebar.querySelectorAll('a[data-nav-id]')); contentSections = navLinks.map(link => { const id = link.getAttribute('data-nav-id'); return document.getElementById(id); }).filter(Boolean); utils.log(`滚动跟踪:找到 ${navLinks.length} 个导航项和 ${contentSections.length} 个内容区域`); }; // 更新活动状态 const updateActiveState = () => { if (!ticking) { window.requestAnimationFrame(() => { // 如果没有导航项或内容区域不匹配,重新获取 if (navLinks.length === 0 || navLinks.length !== contentSections.length) { updateAnchors(); } if (contentSections.length === 0) { ticking = false; return; } // 找到当前在视口中的内容 let activeIndex = -1; const offset = 120; // 增加顶部偏移量,考虑到页面顶部的导航栏高度 // 从后往前查找,找到第一个在视口上方的元素 for (let i = 0; i < contentSections.length; i++) { const section = contentSections[i]; if (!section) continue; const rect = section.getBoundingClientRect(); // 如果元素顶部在视口内或刚好在视口上方 if (rect.top <= offset && rect.bottom > 0) { activeIndex = i; break; } } // 如果没找到可见元素,尝试找最接近顶部的元素 if (activeIndex === -1 && contentSections.length > 0) { let minDistance = Infinity; for (let i = 0; i < contentSections.length; i++) { const section = contentSections[i]; if (!section) continue; const rect = section.getBoundingClientRect(); const distance = Math.abs(rect.top - offset); if (distance < minDistance) { minDistance = distance; activeIndex = i; } } } // 更新导航项状态 navLinks.forEach((link, index) => { if (index === activeIndex) { link.classList.add('active'); // 只有当鼠标不在导航栏时才自动滚动 if (!isMouseOverSidebar) { setTimeout(() => { link.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }, 100); } } else { link.classList.remove('active'); } }); ticking = false; }); ticking = true; } }; // 节流函数 const throttle = (func, limit) => { let lastFunc; let lastRan; return function() { const context = this; const args = arguments; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function() { if ((Date.now() - lastRan) >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; }; // 监听滚动事件,使用节流 const scrollHandler = throttle(() => { if (!isMouseOverSidebar) { updateActiveState(); } }, 200); // 200ms 的节流间隔 window.addEventListener('scroll', scrollHandler, { passive: true }); // 监听导航栏变化和内容变化 const sidebarObserver = new MutationObserver(() => { updateAnchors(); updateActiveState(); }); sidebarObserver.observe(sidebar, { childList: true, subtree: true, attributes: true }); // 监听整个文档变化,以捕获内容区域的变化 const contentObserver = new MutationObserver((mutations) => { // 检查是否有相关变化 let needsUpdate = false; for (const mutation of mutations) { if (mutation.type === 'childList' || (mutation.type === 'attributes' && mutation.attributeName === 'id')) { needsUpdate = true; break; } } if (needsUpdate) { updateAnchors(); updateActiveState(); } }); contentObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] }); // 初始化:多次尝试更新以确保捕获到所有内容 updateAnchors(); updateActiveState(); // 延迟再次更新以处理异步加载的内容 setTimeout(() => { updateAnchors(); updateActiveState(); }, 500); setTimeout(() => { updateAnchors(); updateActiveState(); }, 1500); // 添加窗口大小变化监听 window.addEventListener('resize', updateActiveState, { passive: true }); }; // 删除这里重复的 createNavigationItem 函数声明 // const createNavigationItem = (node) => { ... } // 添加 URL 变化监听 let lastUrl = location.href; const urlObserver = new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; utils.log('检测到页面 URL 变化,重新初始化导航'); setTimeout(initializeNavigator, 500); // 延迟执行以等待页面内容加载 } }); // 开始监听 URL 变化 urlObserver.observe(document.querySelector('body'), { childList: true, subtree: true }); // 初始化执行 if (document.readyState === 'loading') { window.addEventListener('DOMContentLoaded', () => { initializeNavigator(); setupThemeObserver(); // 添加主题观察器 }); } else { initializeNavigator(); setupThemeObserver(); // 添加主题观察器 } })();