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