您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A lightweight, feature-rich question sidebar for ChatGPT with resizing, dark mode, hover-previews, pinning, and state persistence.
// ==UserScript== // @name ChatGPT Question Sidebar Navigation // @namespace vanilla-js-enhanced-fixed // @version 2.3.0 // @description A lightweight, feature-rich question sidebar for ChatGPT with resizing, dark mode, hover-previews, pinning, and state persistence. // @match https://chatgpt.com/* // @grant GM_addStyle // @license MIT // ==/UserScript== (function () { 'use strict'; // --- 1. STYLING --- const styles = ` :root { --q-nav-bg: #fff; --q-nav-text: #333; --q-nav-text-secondary: #555; --q-nav-border: #e5e5e5; --q-nav-hover-bg: #f0f0f0; --q-nav-active-bg: #e7f3ff; --q-nav-active-text: #1a73e8; --q-nav-pin-color: #f6ad55; --q-nav-scrollbar-thumb: #ccc; --q-nav-scrollbar-track: #f1f1f1; } html.dark #q-nav-container, html.dark #q-nav-tooltip { --q-nav-bg: #2a2a2a; --q-nav-text: #f0f0f0; --q-nav-text-secondary: #bbb; --q-nav-border: #444; --q-nav-hover-bg: #3a3a3a; --q-nav-active-bg: #1a3c5f; --q-nav-active-text: #6ea7f1; --q-nav-pin-color: #f6ad55; --q-nav-scrollbar-thumb: #555; --q-nav-scrollbar-track: #333; } #q-nav-container { position: fixed; top: 10vh; right: 16px; height: auto; max-height: 70vh; background: var(--q-nav-bg); border: 1px solid var(--q-nav-border); border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); padding: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; font-size: 14px; z-index: 9999; transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out; color: var(--q-nav-text); display: flex; flex-direction: column; min-width: 180px; max-width: 50vw; } #q-nav-container.q-nav-hidden { transform: translateX(calc(100% - 20px)); opacity: 0.4; } #q-nav-container.q-nav-hidden:hover { transform: translateX(0); opacity: 1; box-shadow: 0 6px 20px rgba(0,0,0,0.2); } #q-nav-resizer { position: absolute; left: -5px; top: 0; width: 10px; height: 100%; cursor: col-resize; z-index: 10000; } #q-nav-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; border-bottom: 1px solid var(--q-nav-border); font-weight: 600; user-select: none; } #q-nav-toggle { cursor: pointer; padding: 2px; } #q-nav-list-wrapper { overflow-y: auto; margin-top: 10px; scrollbar-width: thin; scrollbar-color: var(--q-nav-scrollbar-thumb) var(--q-nav-scrollbar-track); } #q-nav-list-wrapper::-webkit-scrollbar { width: 6px; } #q-nav-list-wrapper::-webkit-scrollbar-track { background: var(--q-nav-scrollbar-track); border-radius: 3px; } #q-nav-list-wrapper::-webkit-scrollbar-thumb { background: var(--q-nav-scrollbar-thumb); border-radius: 3px; } #q-nav-list-wrapper::-webkit-scrollbar-thumb:hover { background: #888; } .q-nav-section-header { font-size: 12px; font-weight: bold; color: var(--q-nav-text-secondary); margin: 10px 0 5px; padding: 0 5px; text-transform: uppercase; } .q-nav-section-header:first-child { margin-top: 0; } .q-nav-section-divider { border: 0; border-top: 1px solid var(--q-nav-border); margin: 10px 0; } #q-nav-pinned-list, #q-nav-questions-list { list-style: none; padding: 0; margin: 0; } #q-nav-list-wrapper li { position: relative; display: flex; align-items: center; padding: 8px 24px 8px 5px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-radius: 4px; color: var(--q-nav-text-secondary); } #q-nav-list-wrapper li:hover { background-color: var(--q-nav-hover-bg); } #q-nav-list-wrapper li.q-nav-active { background-color: var(--q-nav-active-bg); color: var(--q-nav-active-text); font-weight: 500; } .q-nav-pin-icon { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); opacity: 0; transition: opacity 0.15s; fill: var(--q-nav-text-secondary); } #q-nav-list-wrapper li:hover .q-nav-pin-icon { opacity: 0.6; } .q-nav-pin-icon:hover { opacity: 1 !important; fill: var(--q-nav-pin-color) !important; } .q-nav-pinned .q-nav-pin-icon { opacity: 1; fill: var(--q-nav-pin-color); } #q-nav-tooltip { position: fixed; opacity: 0; transition: opacity 0.2s ease-in-out; background: var(--q-nav-bg); border: 1px solid var(--q-nav-border); border-radius: 6px; padding: 8px 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); max-width: 400px; font-size: 13px; z-index: 10001; pointer-events: none; white-space: pre-wrap; line-height: 1.5; color: var(--q-nav-text); } `; const ICONS = { open: '👁️', closed: '👁️🗨️', pin: `<svg class="q-nav-pin-icon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>` }; let sidebarElement = null; let tooltipElement = null; let questionElements = []; let activeIndex = -1; let scrollContainer = null; let isResizing = false; let pageObserver = null; let themeObserver = null; let currentPath = location.pathname; let settings = { isOpen: JSON.parse(localStorage.getItem('qNavSettings_isOpen')) ?? true, width: localStorage.getItem('qNavSettings_width') || '250px' }; function saveSettings() { localStorage.setItem('qNavSettings_isOpen', JSON.stringify(settings.isOpen)); localStorage.setItem('qNavSettings_width', settings.width); } function getConversationId() { try { return location.pathname.split('/c/')[1].split('/')[0]; } catch (e) { return null; } } function loadPinnedItems(convoId) { if (!convoId) return []; return JSON.parse(localStorage.getItem(`qNavPinned_${convoId}`)) || []; } function savePinnedItems(convoId, items) { if (!convoId) return; localStorage.setItem(`qNavPinned_${convoId}`, JSON.stringify(items)); } function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } function getChatContainer() { return document.querySelector("main .flex.flex-col.text-sm"); } function queryQuestionElements() { const container = getChatContainer(); if (!container) return []; return Array.from(container.querySelectorAll('div[data-message-author-role="user"]')); } function createSidebar() { if (document.getElementById('q-nav-container')) return; GM_addStyle(styles); sidebarElement = document.createElement('div'); sidebarElement.id = 'q-nav-container'; document.body.appendChild(sidebarElement); sidebarElement.innerHTML = ` <div id="q-nav-resizer"></div> <div id="q-nav-header"> <span>📄 Questions</span> <span id="q-nav-toggle" title="Toggle Sidebar"></span> </div> <div id="q-nav-list-wrapper"> <div id="q-nav-pinned-section" style="display: none;"> <div class="q-nav-section-header">Pinned</div> <ul id="q-nav-pinned-list"></ul> <hr class="q-nav-section-divider" /> </div> <div id="q-nav-questions-section" style="display: none;"> <div class="q-nav-section-header">Questions</div> <ul id="q-nav-questions-list"></ul> </div> </div> `; tooltipElement = document.createElement('div'); tooltipElement.id = 'q-nav-tooltip'; document.body.appendChild(tooltipElement); sidebarElement.style.width = settings.width; if (!settings.isOpen) sidebarElement.classList.add('q-nav-hidden'); sidebarElement.querySelector('#q-nav-toggle').innerHTML = settings.isOpen ? ICONS.open : ICONS.closed; addEventListeners(); checkDarkMode(); } function updateSidebar() { if (!sidebarElement) return; const convoId = getConversationId(); const pinnedItems = loadPinnedItems(convoId); const pinnedList = sidebarElement.querySelector('#q-nav-pinned-list'); const questionsList = sidebarElement.querySelector('#q-nav-questions-list'); const pinnedSection = sidebarElement.querySelector('#q-nav-pinned-section'); const questionsSection = sidebarElement.querySelector('#q-nav-questions-section'); pinnedList.innerHTML = ''; questionsList.innerHTML = ''; questionElements = queryQuestionElements(); const questionData = questionElements.map((el, index) => { const textContent = el.querySelector('.whitespace-pre-wrap')?.innerText.trim() || `Question ${index + 1}`; const answerEl = el.closest('article[data-turn-id]')?.nextElementSibling?.querySelector('[data-message-author-role="assistant"] .markdown.prose'); const previewText = answerEl ? answerEl.innerText.trim().substring(0, 250) + (answerEl.innerText.length > 250 ? '...' : '') : ''; return { el, index, textContent, previewText }; }); const pinnedData = []; const unpinnedData = []; questionData.forEach(item => { if (pinnedItems.includes(item.textContent)) { pinnedData.push(item); } else { unpinnedData.push(item); } }); const renderItem = (item, isPinned) => { const listItem = document.createElement('li'); listItem.textContent = item.textContent; listItem.dataset.index = item.index; listItem.dataset.text = item.textContent; listItem.dataset.preview = item.previewText; listItem.title = item.textContent; listItem.innerHTML = `${item.textContent}${ICONS.pin}`; if (isPinned) { listItem.classList.add('q-nav-pinned'); pinnedList.appendChild(listItem); } else { questionsList.appendChild(listItem); } }; pinnedData.forEach(item => renderItem(item, true)); unpinnedData.forEach(item => renderItem(item, false)); pinnedSection.style.display = pinnedData.length > 0 ? 'block' : 'none'; questionsSection.style.display = unpinnedData.length > 0 ? 'block' : 'none'; updateActiveHighlight(); } function handleSidebarInteraction(event) { const target = event.target; if (target.closest('.q-nav-pin-icon')) { event.stopPropagation(); const listItem = target.closest('li'); const text = listItem.dataset.text; const convoId = getConversationId(); let pinnedItems = loadPinnedItems(convoId); if (pinnedItems.includes(text)) { pinnedItems = pinnedItems.filter(item => item !== text); } else { pinnedItems.push(text); } savePinnedItems(convoId, pinnedItems); updateSidebar(); } else if (target.closest('li')) { if (isResizing) return; const index = parseInt(target.closest('li').dataset.index, 10); if (!isNaN(index) && questionElements[index]) { questionElements[index].scrollIntoView({ behavior: 'smooth', block: 'start' }); updateActiveHighlight(index); } } } const throttledUpdateActiveHighlight = throttle(updateActiveHighlight, 100); function updateActiveHighlight(forceIndex = null) { if (!sidebarElement || !scrollContainer) return; let newActiveIndex = -1; if (forceIndex !== null) { newActiveIndex = forceIndex; } else { const threshold = scrollContainer.getBoundingClientRect().top + 100; for (let i = questionElements.length - 1; i >= 0; i--) { if (questionElements[i].getBoundingClientRect().top <= threshold) { newActiveIndex = i; break; } } if (scrollContainer.scrollHeight - scrollContainer.scrollTop <= scrollContainer.clientHeight + 5) { newActiveIndex = questionElements.length - 1; } } if (newActiveIndex !== activeIndex) { activeIndex = newActiveIndex; const listItems = sidebarElement.querySelectorAll('#q-nav-list-wrapper li'); let activeLi = null; listItems.forEach(li => { const isActive = parseInt(li.dataset.index) === activeIndex; li.classList.toggle('q-nav-active', isActive); if (isActive) activeLi = li; }); if (activeLi) { activeLi.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } } function addEventListeners() { const toggle = sidebarElement.querySelector('#q-nav-toggle'); const resizer = sidebarElement.querySelector('#q-nav-resizer'); const listWrapper = sidebarElement.querySelector('#q-nav-list-wrapper'); toggle.addEventListener('click', () => { settings.isOpen = !settings.isOpen; sidebarElement.classList.toggle('q-nav-hidden'); toggle.innerHTML = settings.isOpen ? ICONS.open : ICONS.closed; saveSettings(); }); resizer.addEventListener('mousedown', (e) => { e.preventDefault(); isResizing = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; const startX = e.clientX; const startWidth = sidebarElement.offsetWidth; const doDrag = (dragEvent) => { const newWidth = startWidth - (dragEvent.clientX - startX); if (newWidth > 180 && newWidth < window.innerWidth * 0.5) { settings.width = `${newWidth}px`; sidebarElement.style.width = settings.width; } }; const stopDrag = () => { document.removeEventListener('mousemove', doDrag); document.removeEventListener('mouseup', stopDrag); document.body.style.cursor = ''; document.body.style.userSelect = ''; saveSettings(); setTimeout(() => { isResizing = false; }, 100); }; document.addEventListener('mousemove', doDrag); document.addEventListener('mouseup', stopDrag); }); listWrapper.addEventListener('click', handleSidebarInteraction); listWrapper.addEventListener('mouseover', e => { const li = e.target.closest('li'); if (li && li.dataset.preview) { tooltipElement.textContent = li.dataset.preview; tooltipElement.style.opacity = '1'; } }); listWrapper.addEventListener('mouseout', () => { tooltipElement.style.opacity = '0'; }); listWrapper.addEventListener('mousemove', e => { if (tooltipElement.style.opacity === '1') { const tooltipRect = tooltipElement.getBoundingClientRect(); let x = e.clientX + 15; let y = e.clientY + 15; if (x + tooltipRect.width > window.innerWidth - 10) { x = e.clientX - tooltipRect.width - 15; } if (y + tooltipRect.height > window.innerHeight - 10) { y = e.clientY - tooltipRect.height - 15; } tooltipElement.style.left = `${x}px`; tooltipElement.style.top = `${y}px`; } }); scrollContainer = getChatContainer()?.parentElement; if (scrollContainer) { scrollContainer.addEventListener('scroll', throttledUpdateActiveHighlight); } } function checkDarkMode() { const isDark = document.documentElement.classList.contains('dark'); sidebarElement?.classList.toggle('q-nav-dark', isDark); tooltipElement?.classList.toggle('q-nav-dark', isDark); } function initialize() { const chatContainer = getChatContainer(); if (chatContainer && queryQuestionElements().length > 0) { if (!sidebarElement) { createSidebar(); } updateSidebar(); if (!pageObserver) { pageObserver = new MutationObserver(throttle(updateSidebar, 500)); pageObserver.observe(chatContainer, { childList: true, subtree: true }); } } else { destroy(); } } function destroy() { if (sidebarElement) { sidebarElement.remove(); sidebarElement = null; } if (tooltipElement) { tooltipElement.remove(); tooltipElement = null; } if (pageObserver) { pageObserver.disconnect(); pageObserver = null; } if (scrollContainer) { scrollContainer.removeEventListener('scroll', throttledUpdateActiveHighlight); scrollContainer = null; } questionElements = []; activeIndex = -1; } setInterval(() => { const newPath = location.pathname; const chatContainer = getChatContainer(); if (newPath !== currentPath) { currentPath = newPath; destroy(); setTimeout(initialize, 2000); } else if (!sidebarElement && chatContainer && queryQuestionElements().length > 0) { initialize(); } else if (sidebarElement && !chatContainer) { destroy(); } }, 500); themeObserver = new MutationObserver(checkDarkMode); themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); })();