Gemini Chat Navigator

Gemini 侧边栏目录

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Gemini Chat Navigator
// @version      1.0.0
// @description  Gemini 侧边栏目录
// @author       Russell
// @match        https://gemini.google.com/*
// @grant        none
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. 样式定义 (保持浅色风格) ---
    const css = `
        #gemini-nav-toggle {
            position: fixed; top: 50%; right: 0; transform: translateY(-50%);
            width: 30px; height: 50px; background: #f0f4f9; color: #444746;
            border: 1px solid #e0e0e0; border-right: none; border-radius: 8px 0 0 8px;
            cursor: pointer; z-index: 9999; display: flex; align-items: center; justify-content: center;
            font-size: 16px; box-shadow: -2px 1px 4px rgba(0,0,0,0.1); transition: all 0.3s ease;
        }
        #gemini-nav-toggle:hover { background: #e3e3e3; width: 40px; }

        #gemini-nav-sidebar {
            position: fixed; top: 0; right: -260px; width: 260px; height: 100vh;
            background: #ffffff; border-left: 1px solid #e0e0e0; z-index: 9998;
            transition: right 0.3s ease; display: flex; flex-direction: column;
            color: #1f1f1f; font-family: 'Google Sans', sans-serif;
            box-shadow: -5px 0 15px rgba(0,0,0,0.1);
        }
        body.nav-open #gemini-nav-sidebar { right: 0; }
        body.nav-open #gemini-nav-toggle {
            right: 260px; border-radius: 50%; width: 40px; height: 40px;
            margin-right: -20px; color: #1f1f1f; background: #fff; border: 1px solid #e0e0e0;
        }

        .nav-header {
            padding: 15px; border-bottom: 1px solid #f0f0f0; font-size: 16px;
            font-weight: bold; background: #f8f9fa; display: flex; justify-content: space-between; align-items: center;
        }
        .nav-quick-actions { display: flex; padding: 10px; gap: 10px; border-bottom: 1px solid #f0f0f0; }
        .nav-quick-btn {
            flex: 1; padding: 8px; background: #ffffff; border: 1px solid #c4c7c5;
            border-radius: 4px; color: #444746; cursor: pointer; text-align: center; font-size: 12px;
        }
        .nav-quick-btn:hover { background: #f0f4f9; color: #1f1f1f; border-color: #1f1f1f; }

        .nav-list { flex: 1; overflow-y: auto; padding: 10px; }
        .nav-item {
            padding: 10px; margin-bottom: 5px; border-radius: 6px; cursor: pointer;
            font-size: 13px; line-height: 1.4; color: #444746; transition: background 0.2s;
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-left: 3px solid transparent;
        }
        .nav-item:hover { background: #f0f4f9; color: #0b57d0; border-left: 3px solid #0b57d0; }
        .nav-item span.index { color: #8e918f; margin-right: 8px; font-size: 11px; font-weight: bold; }

        .nav-list::-webkit-scrollbar { width: 6px; }
        .nav-list::-webkit-scrollbar-track { background: #fff; }
        .nav-list::-webkit-scrollbar-thumb { background: #dcdcdc; border-radius: 3px; }
    `;

    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // --- 2. 创建 UI ---
    const sidebar = document.createElement('div');
    sidebar.id = 'gemini-nav-sidebar';
    sidebar.innerHTML = `
        <div class="nav-header">
            <span>📑 导航目录</span>
            <span style="font-size:14px; color:#0b57d0; cursor:pointer;" id="refresh-toc" title="刷新目录">↻</span>
        </div>
        <div class="nav-quick-actions">
            <button class="nav-quick-btn" id="btn-top">⬆️ 顶部</button>
            <button class="nav-quick-btn" id="btn-bottom">⬇️ 底部</button>
        </div>
        <div class="nav-list" id="nav-list-content">
            <div style="padding:20px; text-align:center; color:#888;">正在扫描...</div>
        </div>
    `;

    const toggleBtn = document.createElement('button');
    toggleBtn.id = 'gemini-nav-toggle';
    toggleBtn.innerHTML = '☰';
    document.body.appendChild(sidebar);
    document.body.appendChild(toggleBtn);

    toggleBtn.addEventListener('click', () => document.body.classList.toggle('nav-open'));
    document.getElementById('btn-top').onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
    document.getElementById('btn-bottom').onclick = () => window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
    document.getElementById('refresh-toc').onclick = generateTableOfContents;

    // --- 3. 核心:扫描并生成目录 (修复重复问题) ---
    function generateTableOfContents() {
        const listContainer = document.getElementById('nav-list-content');

        // 获取所有可能的元素
        let userQueries = document.querySelectorAll('[data-test-id="user-query"], .user-query-container');

        if (userQueries.length === 0) {
            listContainer.innerHTML = '<div style="padding:10px; color:#888; text-align:center;">暂未检测到对话</div>';
            return;
        }

        listContainer.innerHTML = '';

        let lastText = ""; // 用于记录上一条的内容,防止重复
        let validIndex = 0; // 实际显示的序号

        userQueries.forEach((queryEl) => {
            // 获取文本并清理多余空格
            let text = queryEl.innerText || queryEl.textContent;
            text = text.replace(/\s+/g, ' ').trim();

            // === 修复核心逻辑:去重 ===
            // 1. 如果文本为空,跳过
            // 2. 如果文本和上一条记录的完全一样,跳过 (说明是嵌套的div)
            // 3. 如果文本太短(小于2个字)可能是图标或空行,跳过
            if (!text || text === lastText || text.length < 2) {
                return;
            }

            // 更新记录
            lastText = text;
            validIndex++;

            // 截取前 18 个字
            const shortText = text.length > 18 ? text.substring(0, 18) + "..." : text;

            const item = document.createElement('div');
            item.className = 'nav-item';
            item.innerHTML = `<span class="index">#${validIndex}</span>${shortText}`;

            item.onclick = () => {
                queryEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
                // 高亮效果
                queryEl.style.transition = "background 0.5s";
                const originalBg = queryEl.style.backgroundColor;
                queryEl.style.backgroundColor = "#e8f0fe";
                setTimeout(() => { queryEl.style.backgroundColor = originalBg; }, 800);
            };

            listContainer.appendChild(item);
        });
    }

    // --- 4. 自动监听 ---
    let timeout;
    const observer = new MutationObserver(() => {
        clearTimeout(timeout);
        timeout = setTimeout(generateTableOfContents, 1000);
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(generateTableOfContents, 2000);

})();