167 lines
6.2 KiB
JavaScript
167 lines
6.2 KiB
JavaScript
// ==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);
|
||
})();
|