// ==UserScript== // @name Gemini Minimal Scrubber // @namespace https://git.js0ny.net // @version 1.8.1 // @description Sidebar navigation with text preview. (Trusted Types Compliant) // @author js0ny // @match https://gemini.google.com/* // @grant none // @run-at document-idle // @icon https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com // ==/UserScript== (function() { 'use strict'; const CONFIG = { debounceTime: 500, gap: '8px', tooltipWidth: '380px', hitAreaHeight: '32px', baseColor: 'rgba(255, 255, 255, 0.25)', // 浅色模式请改 rgba(0,0,0,0.2) hoverColor: '#a8c7fa', }; // 1. 注入 CSS (使用标准 API,避开 CSP 拦截) const style = document.createElement('style'); style.textContent = ` #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; } .scrub-item { position: relative; height: ${CONFIG.hitAreaHeight}; width: 32px; display: flex; align-items: center; justify-content: flex-end; cursor: pointer; pointer-events: auto; } .scrub-visual { width: 6px; height: 24px; background-color: ${CONFIG.baseColor}; border-radius: 99px; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } .scrub-item:hover .scrub-visual { background-color: ${CONFIG.hoverColor}; width: 10px; height: 32px; box-shadow: 0 0 10px ${CONFIG.hoverColor}; } .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; opacity: 0; visibility: hidden; pointer-events: none; transition: all 0.2s; color: #e3e3e3; font-size: 14px; font-family: sans-serif; 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; text-transform: uppercase; margin-bottom: 4px; } .tooltip-text { display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; word-wrap: break-word; } .tooltip-text.user { color: #d2e3fc; } `; document.head.appendChild(style); let container = null; let debounceTimer = null; // 辅助函数:安全创建元素 (代替 innerHTML) function createEl(tag, className, text) { const el = document.createElement(tag); if (className) el.className = className; if (text) el.textContent = text; return el; } function getCleanText(element) { if (!element) return ""; try { const p = element.querySelector("p"); const raw = (p && p.textContent) ? p.textContent : (element.textContent || ""); return raw.replace(/\s+/g, ' ').trim(); } catch (e) { return ""; } } function createTooltipBlock(labelStr, textStr, isUser) { const block = createEl('div', 'tooltip-block'); const label = createEl('div', 'tooltip-label', labelStr); const text = createEl('div', isUser ? 'tooltip-text user' : 'tooltip-text model', textStr); block.appendChild(label); block.appendChild(text); return block; } 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"); // 清理多余节点 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]; // 创建新的 Scrub Item if (!item) { item = createEl('div', 'scrub-item'); // 1. Tooltip 容器 const tooltip = createEl('div', 'scrub-tooltip'); item.appendChild(tooltip); // 2. 视觉胶囊 const visual = createEl('div', 'scrub-visual'); item.appendChild(visual); // 点击事件 item.addEventListener('click', (e) => { e.stopPropagation(); uNode.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); container.appendChild(item); } // 检查是否需要更新 Tooltip (减少 DOM 操作) if (item.dataset.uText !== uText || item.dataset.rText !== rText) { const tooltip = item.querySelector('.scrub-tooltip'); tooltip.textContent = ''; // 安全清空 // 安全构建 User 部分 tooltip.appendChild(createTooltipBlock('User', uText, true)); // 安全构建 Gemini 部分 if (rText) { tooltip.appendChild(createTooltipBlock('Gemini', rText, false)); } item.dataset.uText = uText; item.dataset.rText = rText; } }); } const observer = new MutationObserver(() => { clearTimeout(debounceTimer); debounceTimer = setTimeout(renderScrubber, CONFIG.debounceTime); }); observer.observe(document.body, { childList: true, subtree: true }); // 启动 setTimeout(renderScrubber, 1000); })();