// ==UserScript== // @name Gemini Minimal Scrubber // @namespace https://git.js0ny.net // @version 1.6 // @description Sidebar navigation with text preview. No scroll-spy, just click-to-jump. // @author js0ny // @match https://gemini.google.com/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const CONFIG = { debounceTime: 200, gap: '8px', tooltipWidth: '380px', hitAreaHeight: '32px', baseColor: 'rgba(255, 255, 255, 0.25)', hoverColor: '#a8c7fa', }; GM_addStyle(` #gemini-scrubber { position: fixed; right: 16px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: ${CONFIG.gap}; z-index: 99999; padding: 10px 8px; pointer-events: none; /* 容器穿透 */ } /* 1. 交互层 (热区) */ .scrub-item { position: relative; height: ${CONFIG.hitAreaHeight}; width: 32px; /* 稍微加宽热区 */ display: flex; align-items: center; justify-content: flex-end; cursor: pointer; pointer-events: auto; /* 恢复点击 */ } /* 2. 视觉层 (胶囊) */ .scrub-visual { width: 6px; height: 24px; background-color: ${CONFIG.baseColor}; border-radius: 99px; transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); box-shadow: 0 2px 5px rgba(0,0,0,0.2); opacity: 0.6; } /* Hover 效果:变宽、变色、变高 */ .scrub-item:hover .scrub-visual { background-color: ${CONFIG.hoverColor}; width: 10px; height: 32px; opacity: 1; box-shadow: 0 0 10px ${CONFIG.hoverColor}; } /* 3. 预览框 (Tooltip) */ .scrub-tooltip { position: absolute; right: 36px; top: 50%; transform: translateY(-50%) translateX(10px); width: ${CONFIG.tooltipWidth}; background-color: #1e1e1e; border: 1px solid #444; border-radius: 12px; padding: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.6); opacity: 0; visibility: hidden; pointer-events: none; transition: all 0.2s ease-out; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; font-family: "Google Sans", system-ui, sans-serif; font-size: 14px; line-height: 1.6; color: #e3e3e3; z-index: 100000; } .scrub-item:hover .scrub-tooltip { opacity: 1; visibility: visible; transform: translateY(-50%) translateX(0); } .tooltip-block { margin-bottom: 12px; } .tooltip-block:last-child { margin-bottom: 0; } .tooltip-label { font-size: 11px; color: #8e918f; font-weight: 700; letter-spacing: 0.5px; margin-bottom: 4px; text-transform: uppercase; } .tooltip-text { word-wrap: break-word; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; } .tooltip-text.user { color: #d2e3fc; } `); let container = null; let debounceTimer = null; function getCleanText(element) { if (!element) return ""; const p = element.querySelector("p"); // 优先取 p,否则取纯文本,替换多余空白 const raw = (p && p.textContent.trim()) ? p.textContent : element.textContent; return raw.replace(/\s+/g, ' ').trim(); } function renderScrubber() { if (!container) { container = document.createElement('div'); container.id = 'gemini-scrubber'; document.body.appendChild(container); } const uNodes = document.querySelectorAll("user-query-content"); const rNodes = document.querySelectorAll("model-response"); // 没内容时清空 if (uNodes.length === 0) { container.innerHTML = ''; return; } // 清理多余节点 while (container.children.length > uNodes.length) { container.removeChild(container.lastChild); } uNodes.forEach((uNode, idx) => { const rNode = rNodes[idx]; const uText = getCleanText(uNode); const rText = rNode ? getCleanText(rNode) : "..."; let item = container.children[idx]; const newTooltipContent = `
User
${uText}
${rText ? `
Gemini
${rText}
` : ''} `; if (item) { // diff: update only on change const currentU = item.dataset.uText; const currentR = item.dataset.rText; if (currentU !== uText || currentR !== rText) { const tooltip = item.querySelector('.scrub-tooltip'); if (tooltip) tooltip.innerHTML = newTooltipContent; item.dataset.uText = uText; item.dataset.rText = rText; } } else { item = document.createElement('div'); item.className = 'scrub-item'; item.dataset.uText = uText; item.dataset.rText = rText; item.innerHTML = `
${newTooltipContent}
`; item.addEventListener('click', (e) => { e.stopPropagation(); uNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); container.appendChild(item); } }); } const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(renderScrubber, CONFIG.debounceTime); }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(renderScrubber, 1000); })();