// ==UserScript== // @name Universal AI Conversation Navigation Sidebar (Trusted Types & Modern UI) // @namespace npm/ai-question-navigator // @version 5.1 // @author Pitroytech (UI) & Bui Quoc Dung (Logic) // @description Floating sidebar for ChatGPT, Claude, Gemini, etc. Left-click: Scroll to Prompt. Right-click: Scroll to Answer (skips prompt). Sleek UI with Custom Tooltips. Fixed for Google Trusted Types. // @match https://chatgpt.com/* // @match https://gemini.google.com/* // @match https://notebooklm.google.com/* // @match https://grok.com/* // @match https://claude.ai/* // @match https://www.kimi.com/* // @match https://chat.deepseek.com/* // @match https://chat.qwen.ai/* // @match https://chat.z.ai/* // @match https://chat.mistral.ai/* // @match https://www.perplexity.ai/* // @match https://poe.com/* // @match https://paperfigureqa.allen.ai/* // @grant none // ==/UserScript== (function () { 'use strict'; // ====== CONFIGURATION ====== const RIGHT_OFFSET_PX = 24; const TOP_OFFSET_VH = 12; // Starts at 12vh from top const SIDEBAR_WIDTH = 280; const HEADER_HEIGHT = 40; // ====== SITE CONFIGURATION ====== const SITE_CONFIG = { chatgpt: { match: /chatgpt\.com/, msgSelector: 'div[data-message-author-role="user"]' }, gemini: { match: /gemini\.google\.com/, msgSelector: '.query-text, .user-query, div[data-test-id="user-query"], h2[data-test-id="user-query"], [data-testid="user-message"]' }, notebooklm: { match: /notebooklm\.google\.com/, msgSelector: 'chat-message .from-user-container' }, grok: { match: /grok\.com/, msgSelector: '.relative.group.flex.flex-col.justify-center.items-end' }, claude: { match: /claude\.ai/, msgSelector: '.font-user-message' }, deepseek: { match: /chat\.deepseek\.com/, msgSelector: '.ds-markdown.ds-markdown--user, .user-message' }, kimi: { match: /www\.kimi\.com/, msgSelector: '.user-content' }, glm: { match: /chat\.z\.ai/, msgSelector: '.chat-user' }, qwen: { match: /chat\.qwen\.ai/, msgSelector: '.chat-user-message' }, mistral: { match: /chat\.mistral\.ai/, msgSelector: 'div[data-message-author-role="user"]' }, perplexity: { match: /perplexity\.ai/, msgSelector: '.group\\/query, div[data-testid="thread-title"]' }, poe: { match: /poe\.com/, msgSelector: '[class*="ChatMessagesView_tupleGroupContainer"] > div > div:first-child' }, paperfigure: { match: /paperfigureqa\.allen\.ai/, msgSelector: '.MuiPaper-root' }, }; const currentSite = Object.values(SITE_CONFIG).find(s => s.match.test(window.location.hostname)); if (!currentSite) return; const DOM_ID = 'ai-nav-sidebar-v5'; // ====== MODERN CSS ====== const CSS = ` #${DOM_ID} { position: fixed; top: ${TOP_OFFSET_VH}vh; right: ${RIGHT_OFFSET_PX}px; width: ${SIDEBAR_WIDTH}px; max-height: 70vh; display: flex; flex-direction: column; background: #0f1117; /* Very dark slate */ border: 1px solid #2d3342; border-radius: 12px; box-shadow: 0 10px 40px -10px rgba(0,0,0,0.6); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; z-index: 2147483647; transition: width 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease, transform 0.2s; overflow: visible; /* Allow tooltip to pop out */ opacity: 1; } #${DOM_ID} * { box-sizing: border-box; } /* Collapsed State */ #${DOM_ID}.collapsed { width: 44px; border-color: transparent; background: rgba(15, 17, 23, 0.6); backdrop-filter: blur(4px); } #${DOM_ID}.collapsed:hover { background: #0f1117; border-color: #2d3342; opacity: 1; } #${DOM_ID}.collapsed:not(:hover) { opacity: 0.5; } #${DOM_ID}.collapsed .list-container { display: none; } #${DOM_ID}.collapsed .header-title { display: none; } #${DOM_ID}.collapsed .header { justify-content: center; padding: 0; } /* Header */ #${DOM_ID} .header { height: ${HEADER_HEIGHT}px; display: flex; align-items: center; justify-content: space-between; padding: 0 16px; border-bottom: 1px solid #2d3342; cursor: grab; flex-shrink: 0; user-select: none; } #${DOM_ID} .header:active { cursor: grabbing; } #${DOM_ID} .header-title { font-size: 13px; font-weight: 600; color: #94a3b8; letter-spacing: 0.5px; text-transform: uppercase; } #${DOM_ID} .pin-btn { color: #64748b; cursor: pointer; font-size: 14px; padding: 4px; border-radius: 4px; transition: all 0.2s; } #${DOM_ID} .pin-btn:hover { color: #f8fafc; background: rgba(255,255,255,0.1); } #${DOM_ID}.pinned .pin-btn { color: #38bdf8; } /* List */ #${DOM_ID} .list-container { flex: 1; overflow-y: auto; padding: 8px; scrollbar-width: thin; scrollbar-color: #334155 transparent; } #${DOM_ID} .list-container::-webkit-scrollbar { width: 4px; } #${DOM_ID} .list-container::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; } /* Items */ #${DOM_ID} .item { position: relative; display: flex; align-items: center; padding: 8px 12px; margin-bottom: 2px; border-radius: 6px; cursor: pointer; color: #cbd5e1; font-size: 13px; line-height: 1.4; transition: background 0.15s, color 0.15s; } #${DOM_ID} .item:hover { background: #1e293b; color: #fff; } #${DOM_ID} .item.active { background: rgba(56, 189, 248, 0.1); color: #38bdf8; font-weight: 500; border-left: 2px solid #38bdf8; padding-left: 10px; /* Compensate border */ } /* Split Click Visualization */ #${DOM_ID} .item:hover::after { content: ''; position: absolute; top: 15%; bottom: 15%; left: 50%; width: 1px; background: rgba(255,255,255,0.1); pointer-events: none; } #${DOM_ID} .item-idx { min-width: 24px; color: #64748b; font-size: 11px; pointer-events: none; } #${DOM_ID} .item.active .item-idx { color: #38bdf8; } #${DOM_ID} .item-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; } /* Custom Tooltip */ #ai-nav-tooltip { position: fixed; max-width: 300px; background: #1e293b; border: 1px solid #475569; color: #f1f5f9; padding: 12px; border-radius: 8px; font-size: 12px; line-height: 1.5; box-shadow: 0 4px 20px rgba(0,0,0,0.5); z-index: 2147483648; pointer-events: none; opacity: 0; transform: translateX(10px); transition: opacity 0.2s, transform 0.2s; } #ai-nav-tooltip.visible { opacity: 1; transform: translateX(0); } #ai-nav-tooltip .tooltip-hint { margin-top: 8px; padding-top: 8px; border-top: 1px solid #334155; font-size: 10px; color: #94a3b8; display: flex; justify-content: space-between; } `; // ====== UTILS (Trusted Types Safe) ====== function el(tag, cls, text) { const e = document.createElement(tag); if (cls) e.className = cls; if (text) e.textContent = text; return e; } // Trusted Types Safe Clear function clearChildren(node) { while (node.firstChild) { node.removeChild(node.firstChild); } } // Find the scrollable container (Standard vs Custom divs) function getScrollParent(node) { if (!node) return window; let parent = node.parentElement; while (parent) { const style = window.getComputedStyle(parent); if (style.overflowY === 'auto' || style.overflowY === 'scroll') { return parent; } parent = parent.parentElement; } return window; // Default fallback } // ====== UI BUILDER ====== function createUI() { // Cleanup old versions const old = document.getElementById(DOM_ID); if (old) old.remove(); const oldTip = document.getElementById('ai-nav-tooltip'); if (oldTip) oldTip.remove(); // Styles const style = document.createElement('style'); style.textContent = CSS; document.head.appendChild(style); // Sidebar const sb = el('div', '', ''); sb.id = DOM_ID; // Header const header = el('div', 'header'); header.appendChild(el('span', 'header-title', 'Navigation')); const pinBtn = el('span', 'pin-btn', '📌'); pinBtn.title = "Toggle Pin"; header.appendChild(pinBtn); sb.appendChild(header); // List Container const listCont = el('div', 'list-container'); const list = el('div', 'list'); listCont.appendChild(list); sb.appendChild(listCont); // Tooltip Element const tooltip = el('div', ''); tooltip.id = 'ai-nav-tooltip'; document.body.appendChild(tooltip); document.body.appendChild(sb); return { sb, list, pinBtn, tooltip }; } // ====== LOGIC ====== function init() { const { sb, list, pinBtn, tooltip } = createUI(); let isPinned = false; // Load saved pos const savedTop = localStorage.getItem('ai-nav-top-v5'); if (savedTop) sb.style.top = savedTop; // --- Interaction --- sb.addEventListener('mouseenter', () => { if (!sb.classList.contains('dragging')) sb.classList.remove('collapsed'); }); sb.addEventListener('mouseleave', () => { if (!isPinned && !sb.classList.contains('dragging')) sb.classList.add('collapsed'); hideTooltip(); }); pinBtn.addEventListener('click', (e) => { e.stopPropagation(); isPinned = !isPinned; sb.classList.toggle('pinned', isPinned); if(!isPinned) sb.classList.add('collapsed'); }); // Draggable Logic let isDrag = false, startY, startTop; const header = sb.querySelector('.header'); header.addEventListener('mousedown', (e) => { if (e.target === pinBtn) return; isDrag = true; sb.classList.add('dragging'); startY = e.clientY; startTop = sb.getBoundingClientRect().top; document.body.style.userSelect = 'none'; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); }); function onDrag(e) { if (!isDrag) return; const delta = e.clientY - startY; sb.style.top = Math.max(0, startTop + delta) + 'px'; } function stopDrag() { isDrag = false; sb.classList.remove('dragging'); document.body.style.userSelect = ''; localStorage.setItem('ai-nav-top-v5', sb.style.top); document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); } // --- Tooltip Logic --- function showTooltip(text, rect) { clearChildren(tooltip); // Trusted Types Safe const txtDiv = el('div', '', text); const hintDiv = el('div', 'tooltip-hint'); hintDiv.appendChild(el('span', '', 'L: Go to Prompt')); hintDiv.appendChild(el('span', '', 'R: Go to Answer')); tooltip.appendChild(txtDiv); tooltip.appendChild(hintDiv); const sbRect = sb.getBoundingClientRect(); // Position to the left of sidebar tooltip.style.top = (rect.top) + 'px'; tooltip.style.left = (sbRect.left - 320) + 'px'; // 300px width + 20px gap tooltip.classList.add('visible'); } function hideTooltip() { tooltip.classList.remove('visible'); } // --- List Logic --- function updateList() { const els = Array.from(document.querySelectorAll(currentSite.msgSelector)); const texts = els.map(e => (e.innerText || e.textContent || '').trim().replace(/\s+/g, ' ')).filter(t => t.length > 0); // Only redraw if length changed (simple diff check) if (list.children.length === texts.length) return; clearChildren(list); // Trusted Types Safe texts.forEach((text, i) => { const item = el('div', 'item'); const idx = el('span', 'item-idx', (i+1)+'.'); const txt = el('span', 'item-text', text); item.appendChild(idx); item.appendChild(txt); // Tooltip Events item.addEventListener('mouseenter', () => showTooltip(text, item.getBoundingClientRect())); item.addEventListener('mouseleave', hideTooltip); // Click Logic item.addEventListener('click', (e) => { const rect = item.getBoundingClientRect(); const clickX = e.clientX - rect.left; const isLeft = clickX < (rect.width / 2); const targetEl = els[i]; // 1. Ensure element is in view logic (Always scroll prompt to top first) targetEl.scrollIntoView({ behavior: 'auto', block: 'start' }); if (!isLeft) { // RIGHT CLICK: User wants to see the ANSWER. // We just scrolled the Prompt to the Top. // Now we need to scroll DOWN by the height of the prompt (+ padding) // to reveal the answer which is underneath. const scrollParent = getScrollParent(targetEl); const promptHeight = targetEl.getBoundingClientRect().height; // Wait for the immediate scroll to finish before scrolling down // This delay ensures the math is correct after the view resets requestAnimationFrame(() => { scrollParent.scrollBy({ top: promptHeight + 20, behavior: 'smooth' }); }); } highlight(i); }); list.appendChild(item); }); highlight(); } function highlight(forceIdx = -1) { const items = Array.from(list.children); const els = Array.from(document.querySelectorAll(currentSite.msgSelector)); let activeIdx = forceIdx; if (activeIdx === -1) { // Find which prompt is currently "closest" to top of screen let minDist = Infinity; els.forEach((el, i) => { const r = el.getBoundingClientRect(); // Check if it's roughly in the viewport if (r.top > -500 && r.top < window.innerHeight) { const dist = Math.abs(r.top); // Distance from top if (dist < minDist) { minDist = dist; activeIdx = i; } } }); } items.forEach((it, i) => { if (i === activeIdx) { it.classList.add('active'); if (forceIdx !== -1) it.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } else { it.classList.remove('active'); } }); } // Initialize state sb.classList.add('collapsed'); // Observers const observer = new MutationObserver(() => setTimeout(updateList, 500)); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('scroll', () => setTimeout(() => highlight(), 100), { passive: true }); // Initial paint delay setTimeout(updateList, 1500); } // Start setTimeout(init, 1000); })();