diff --git a/gemini-scrubber.js b/gemini-scrubber.js new file mode 100644 index 0000000..263f7be --- /dev/null +++ b/gemini-scrubber.js @@ -0,0 +1,214 @@ +// ==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); + +})();