// ==UserScript== // @name ChatGPT 用户消息导航栏 // @namespace https://chatgpt.com/ // @version 4.0 // @description 在ChatGPT页面右侧显示用户消息导航,支持点击定位和悬浮预览 // @match https://chatgpt.com/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const style = document.createElement('style'); style.textContent = ` /* ===== 主题变量:浅色(默认) ===== */ :root { --gn-bg: rgba(240, 240, 240, 0.95); --gn-text: #555; --gn-text-hover: #111; --gn-active-hover: #2563eb; --gn-dash: #bbb; --gn-shadow: rgba(0,0,0,0.1); --gn-scroll: #ccc; --gn-tip-bg: rgba(255,255,255,0.97); --gn-tip-text: #333; --gn-tip-border: #ddd; } /* 系统深色 */ @media (prefers-color-scheme: dark) { :root { --gn-bg: rgba(40,42,48,0.95); --gn-text: #999; --gn-text-hover: #fff; --gn-active-hover: #93c5fd; --gn-dash: #555; --gn-shadow: rgba(0,0,0,0.3); --gn-scroll: #555; --gn-tip-bg: rgba(30,30,30,0.97); --gn-tip-text: #ddd; --gn-tip-border: #444; } } /* ChatGPT 显式深色 */ html.dark { --gn-bg: rgba(40,42,48,0.95); --gn-text: #999; --gn-text-hover: #fff; --gn-active-hover: #93c5fd; --gn-dash: #555; --gn-shadow: rgba(0,0,0,0.3); --gn-scroll: #555; --gn-tip-bg: rgba(30,30,30,0.97); --gn-tip-text: #ddd; --gn-tip-border: #444; } /* ChatGPT 显式浅色 */ html.light { --gn-bg: rgba(240,240,240,0.95); --gn-text: #555; --gn-text-hover: #111; --gn-active-hover: #2563eb; --gn-dash: #bbb; --gn-shadow: rgba(0,0,0,0.1); --gn-scroll: #ccc; --gn-tip-bg: rgba(255,255,255,0.97); --gn-tip-text: #333; --gn-tip-border: #ddd; } /* ===== 导航栏 ===== */ #gpt-nav-bar { position: fixed; right: 16px; top: 50%; transform: translateY(-50%); width: 24px; max-height: 70vh; overflow-y: auto; overflow-x: hidden; z-index: 99999; transition: width 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease, border-radius 0.3s ease, padding 0.3s ease; background: transparent; padding: 8px 6px; scrollbar-width: none; } #gpt-nav-bar::-webkit-scrollbar { display: none; } #gpt-nav-bar.expanded { width: 260px; background: var(--gn-bg); border-radius: 12px; padding: 16px 6px 16px 16px; box-shadow: 0 2px 16px var(--gn-shadow); scrollbar-width: thin; scrollbar-color: var(--gn-scroll) transparent; } #gpt-nav-bar.expanded::-webkit-scrollbar { display: block; width: 4px; } #gpt-nav-bar.expanded::-webkit-scrollbar-thumb { background: var(--gn-scroll); border-radius: 2px; } .gpt-nav-item { display: flex; align-items: center; justify-content: flex-end; padding: 8px 0; cursor: pointer; overflow: hidden; gap: 1em; } .gpt-nav-text { flex: 0 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; color: var(--gn-text); font-size: 13px; line-height: 1.4; opacity: 0; transition: opacity 0.25s, color 0.15s; } #gpt-nav-bar.expanded .gpt-nav-text { opacity: 1; } #gpt-nav-bar.expanded .gpt-nav-item:hover .gpt-nav-text { color: var(--gn-text-hover); } .gpt-nav-dash { flex-shrink: 0; color: var(--gn-dash); font-size: 14px; line-height: 1; transition: color 0.15s; } .gpt-nav-item.active .gpt-nav-dash { color: #3b82f6; } .gpt-nav-item.active .gpt-nav-text { color: #3b82f6; } #gpt-nav-bar.expanded .gpt-nav-item.active:hover .gpt-nav-text { color: var(--gn-active-hover); } /* ===== Tooltip ===== */ #gpt-nav-tooltip { position: fixed; max-width: 300px; padding: 8px 12px; background: var(--gn-tip-bg); color: var(--gn-tip-text); font-size: 13px; line-height: 1.5; border-radius: 8px; border: 1px solid var(--gn-tip-border); z-index: 100000; pointer-events: none; display: none; } #gpt-nav-tooltip .gpt-nav-tooltip-inner { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 6; overflow: hidden; word-break: break-word; overflow-wrap: anywhere; white-space: pre-line; } @keyframes gpt-bubble-flash { 0% { filter: brightness(1); } 50% { filter: brightness(1.45); } 100% { filter: brightness(1); } } .gpt-bubble-flash { animation: gpt-bubble-flash 0.6s ease !important; } `; document.head.appendChild(style); const navBar = document.createElement('div'); navBar.id = 'gpt-nav-bar'; document.body.appendChild(navBar); const tooltip = document.createElement('div'); tooltip.id = 'gpt-nav-tooltip'; document.body.appendChild(tooltip); function ensureNavMounted() { if (!document.body.contains(navBar)) { document.body.appendChild(navBar); } if (!document.body.contains(tooltip)) { document.body.appendChild(tooltip); } } let currentActiveIdx = -1; let tooltipTimer = null; let hoveredItem = null; let mouseInNav = false; // ========== 展开/收起 ========== const EDGE_ZONE = 60; navBar.addEventListener('mouseenter', () => { mouseInNav = true; }); navBar.addEventListener('mouseleave', () => { mouseInNav = false; clearTooltip(); hoveredItem = null; }); document.addEventListener('mousemove', (e) => { const dist = window.innerWidth - e.clientX; if (dist < EDGE_ZONE || mouseInNav) { navBar.classList.add('expanded'); } else { navBar.classList.remove('expanded'); tooltip.style.display = 'none'; } }); // ========== Tooltip ========== navBar.addEventListener('mouseover', (e) => { const item = e.target.closest('.gpt-nav-item'); if (item === hoveredItem) return; clearTooltip(); hoveredItem = item; if (!item) return; const fullText = item.dataset.full; if (!fullText) return; tooltipTimer = setTimeout(() => { const textSpan = item.querySelector('.gpt-nav-text'); if (!textSpan || textSpan.scrollWidth <= textSpan.clientWidth) return; tooltip.innerHTML = ''; const tooltipInner = document.createElement('div'); tooltipInner.className = 'gpt-nav-tooltip-inner'; tooltipInner.textContent = fullText; tooltip.appendChild(tooltipInner); tooltip.style.display = 'block'; const rect = item.getBoundingClientRect(); const navRect = navBar.getBoundingClientRect(); tooltip.style.top = Math.max(10, Math.min(rect.top, window.innerHeight - 220)) + 'px'; tooltip.style.right = (window.innerWidth - navRect.left + 8) + 'px'; tooltip.style.left = 'auto'; }, 1000); }); function clearTooltip() { clearTimeout(tooltipTimer); tooltipTimer = null; tooltip.style.display = 'none'; } // ========== 点击定位 + 气泡闪烁 ========== navBar.addEventListener('click', (e) => { const item = e.target.closest('.gpt-nav-item'); if (!item) return; const idx = parseInt(item.dataset.idx, 10); const msgs = getUserMessages(); if (!msgs[idx]) return; const bubble = findBubble(msgs[idx]); msgs[idx].scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { bubble.classList.remove('gpt-bubble-flash'); void bubble.offsetWidth; bubble.classList.add('gpt-bubble-flash'); setTimeout(() => bubble.classList.remove('gpt-bubble-flash'), 700); }, 350); }); function findBubble(msgEl) { return msgEl.querySelector('[class*="rounded"][class*="bg-"]') || msgEl.querySelector('.whitespace-pre-wrap')?.closest('[class*="rounded"]') || msgEl; } function getUserMessages() { return [...document.querySelectorAll('[data-message-author-role="user"]')]; } function getMessageText(el) { const inner = el.querySelector('.whitespace-pre-wrap') || el; return (inner.textContent || '').trim(); } // ========== 刷新导航列表 ========== function refreshNav() { ensureNavMounted(); const msgs = getUserMessages(); navBar.innerHTML = ''; if (!msgs.length) { navBar.style.display = 'none'; return; } navBar.style.display = ''; msgs.forEach((msg, i) => { const text = getMessageText(msg); if (!text) return; const div = document.createElement('div'); div.className = 'gpt-nav-item'; div.dataset.idx = i; div.dataset.full = text; const textSpan = document.createElement('span'); textSpan.className = 'gpt-nav-text'; textSpan.textContent = text; const dashSpan = document.createElement('span'); dashSpan.className = 'gpt-nav-dash'; dashSpan.textContent = '—'; div.appendChild(textSpan); div.appendChild(dashSpan); navBar.appendChild(div); }); currentActiveIdx = -1; updateCurrentMessage(); setTimeout(updateCurrentMessage, 500); } // ========== 当前位置追踪 ========== function updateCurrentMessage() { const msgs = getUserMessages(); if (!msgs.length) return; const target = window.innerHeight * 0.4; let bestIdx = 0, bestDist = Infinity; msgs.forEach((msg, i) => { const rect = msg.getBoundingClientRect(); const dist = Math.abs(rect.top + rect.height / 2 - target); if (dist < bestDist) { bestDist = dist; bestIdx = i; } }); if (bestIdx !== currentActiveIdx) { currentActiveIdx = bestIdx; navBar.querySelectorAll('.gpt-nav-item').forEach(item => { item.classList.toggle('active', parseInt(item.dataset.idx) === currentActiveIdx); }); } } let scrollThrottle = null; document.addEventListener('scroll', () => { if (!scrollThrottle) { scrollThrottle = setTimeout(() => { updateCurrentMessage(); scrollThrottle = null; }, 100); } }, true); // ========== DOM 变化 + URL 变化监听 + 用户消息数量变更监听 + 最新消息编辑变化========== let debounceTimer = null; let lastUrl = location.href; let lastUserMsgCount = -1; let lastUserMsgText = ''; function maybeRefreshNav() { const msgs = getUserMessages(); const count = msgs.length; const latestText = count ? getMessageText(msgs[count - 1]) : ''; if (count === lastUserMsgCount && latestText === lastUserMsgText) { updateCurrentMessage(); return; } lastUserMsgCount = count; lastUserMsgText = latestText; refreshNav(); } new MutationObserver((mutations) => { if (mutations.every(m => navBar.contains(m.target) || tooltip.contains(m.target))) return; clearTimeout(debounceTimer); if (location.href !== lastUrl) { lastUrl = location.href; lastUserMsgCount = -1; lastUserMsgText = ''; debounceTimer = setTimeout(refreshNav, 500); } else { debounceTimer = setTimeout(maybeRefreshNav, 300); } }).observe(document.body, { childList: true, subtree: true }); setTimeout(maybeRefreshNav, 1000); })();