feat(gemini-scrubber)

This commit is contained in:
js0ny 2025-12-07 15:06:59 +00:00
parent bdcbd3d3aa
commit a33b941e2a

214
gemini-scrubber.js Normal file
View file

@ -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 = `
<div class="tooltip-block">
<div class="tooltip-label">User</div>
<div class="tooltip-text user">${uText}</div>
</div>
${rText ? `
<div class="tooltip-block">
<div class="tooltip-label">Gemini</div>
<div class="tooltip-text model">${rText}</div>
</div>` : ''}
`;
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 = `
<div class="scrub-tooltip">${newTooltipContent}</div>
<div class="scrub-visual"></div>
`;
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);
})();