Gemini - Quick Navigation for AI Conversations

Add navigation buttons to the Gemini conversation page: switch up and down between AI answers, and jump to the top/bottom with one click

// ==UserScript==
// @name         Gemini - Quick Navigation for AI Conversations
// @name:zh-CN   Gemini - AI对话快速导航
// @namespace    http://tampermonkey.net/
// @version      5.1
// @description  Add navigation buttons to the Gemini conversation page: switch up and down between AI answers, and jump to the top/bottom with one click
// @description:zh-CN  在Gemini对话页面添加导航按钮:上下切换AI回答,一键到顶部/底部
// @author       Epodak
// @match        https://gemini.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 创建样式
    const style = document.createElement('style');
    style.textContent = `
        .nav-buttons-container {
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            z-index: 10000;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .nav-button {
            width: 48px;
            height: 48px;
            border-radius: 50%;
            border: 2px solid #4285f4;
            background: white;
            color: #4285f4;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 20px;
            font-weight: bold;
            transition: all 0.3s ease;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        .extreme-button {
            width: 40px;
            height: 40px;
            font-size: 16px;
            border-color: #ea4335;
        }

        .extreme-button:hover {
            background: #ea4335;
            color: white;
        }

        .nav-button:hover {
            background: #4285f4;
            color: white;
            transform: scale(1.1);
        }

        .nav-button.disabled {
            opacity: 0.5;
            pointer-events: none;
        }
    `;
    document.head.appendChild(style);

    // 创建按钮
    const navContainer = document.createElement('div');
    navContainer.className = 'nav-buttons-container';

    const topButton = document.createElement('div');
    topButton.className = 'nav-button extreme-button';
    topButton.innerHTML = '⤴';
    topButton.title = '跳转到顶部';

    const upButton = document.createElement('div');
    upButton.className = 'nav-button';
    upButton.innerHTML = '↑';
    upButton.title = '上一个AI回答';

    const downButton = document.createElement('div');
    downButton.className = 'nav-button';
    downButton.innerHTML = '↓';
    downButton.title = '下一个AI回答';

    const bottomButton = document.createElement('div');
    bottomButton.className = 'nav-button extreme-button';
    bottomButton.innerHTML = '⤵';
    bottomButton.title = '跳转到底部';

    navContainer.append(topButton, upButton, downButton, bottomButton);
    document.body.appendChild(navContainer);

    // 获取所有AI回答
    function getAllResponses() {
        return Array.from(document.querySelectorAll('model-response'));
    }

    // 获取当前中心的AI回答
    function getCurrentResponse() {
        const responses = getAllResponses();
        if (!responses.length) return null;

        const center = window.innerHeight / 2;
        let closest = responses[0];
        let minDistance = Infinity;

        for (const response of responses) {
            const rect = response.getBoundingClientRect();
            const distance = Math.abs(rect.top + rect.height / 2 - center);
            if (distance < minDistance) {
                minDistance = distance;
                closest = response;
            }
        }
        return closest;
    }

    // 跳转到指定回答
    function scrollTo(response) {
        if (response) {
            response.scrollIntoView({ behavior: 'smooth', block: 'start' });
        }
    }

    // 上一个回答
    function goPrevious() {
        const responses = getAllResponses();
        if (!responses.length) return;

        const current = getCurrentResponse();
        const index = responses.indexOf(current);

        if (index > 0) {
            scrollTo(responses[index - 1]);
        } else {
            window.scrollTo({ top: 0, behavior: 'smooth' });
        }
    }

    // 下一个回答
    function goNext() {
        const responses = getAllResponses();
        if (!responses.length) return;

        const current = getCurrentResponse();
        const index = responses.indexOf(current);

        if (index < responses.length - 1) {
            scrollTo(responses[index + 1]);
        }
    }

    // 查找实际的滚动容器
    function findScrollContainer() {
        // 从model-response元素向上查找有滚动的父容器
        const firstResponse = document.querySelector('model-response');
        if (firstResponse) {
            let element = firstResponse.parentElement;
            while (element && element !== document.body) {
                const hasScroll = element.scrollHeight > element.clientHeight;
                const overflowY = window.getComputedStyle(element).overflowY;

                if (hasScroll && (overflowY === 'auto' || overflowY === 'scroll')) {
                    return element;
                }
                element = element.parentElement;
            }
        }
        return null;
    }

    // 跳转到顶部
    function goTop() {
        const scrollContainer = findScrollContainer();
        if (scrollContainer) {
            scrollContainer.scrollTop = 0;
        } else {
            window.scrollTo({ top: 0, behavior: 'auto' });
        }
    }

    // 跳转到底部(优化版:先快速到底部,再定位到最后AI回答)
    function goBottom() {
        // 1. 先快速跳转到页面最底部(模拟End键)
        const scrollContainer = findScrollContainer();
        if (scrollContainer) {
            scrollContainer.scrollTop = scrollContainer.scrollHeight;
        } else {
            window.scrollTo({ top: document.body.scrollHeight, behavior: 'auto' });
        }

        // 2. 等待一下让内容加载,然后定位到最后一个AI回答
        setTimeout(() => {
            const responses = getAllResponses();
            if (responses.length > 0) {
                scrollTo(responses[responses.length - 1]);
            }
        }, 200);
    }

    // 更新按钮状态
    function updateButtons() {
        const responses = getAllResponses();
        const current = getCurrentResponse();

        if (!responses.length || !current) {
            upButton.classList.add('disabled');
            downButton.classList.add('disabled');
            return;
        }

        const index = responses.indexOf(current);

        // 只有在页面顶部且是第一个回答时禁用上一个按钮
        upButton.classList.toggle('disabled',
            window.scrollY <= 10 && index === 0);

        // 下一个按钮基本不禁用
        downButton.classList.remove('disabled');

        // TOP和BOTTOM按钮始终可用
        topButton.classList.remove('disabled');
        bottomButton.classList.remove('disabled');
    }

    // 事件监听
    topButton.onclick = goTop;
    upButton.onclick = goPrevious;
    downButton.onclick = goNext;
    bottomButton.onclick = goBottom;

    // 滚动监听
    let timer;
    window.addEventListener('scroll', () => {
        clearTimeout(timer);
        timer = setTimeout(updateButtons, 100);
    });

    // DOM变化监听
    new MutationObserver(() => {
        setTimeout(updateButtons, 200);
    }).observe(document.body, { childList: true, subtree: true });

    // 键盘快捷键
    document.addEventListener('keydown', (e) => {
        if (e.target.matches('input, textarea, [contenteditable="true"]')) return;

        if ((e.ctrlKey || e.metaKey) && e.key === 'ArrowUp') {
            e.preventDefault();
            goPrevious();
        }
        if ((e.ctrlKey || e.metaKey) && e.key === 'ArrowDown') {
            e.preventDefault();
            goNext();
        }
        // 移除Ctrl+Home快捷键,因为它在Gemini中不工作
        // 保留Ctrl+End是为了与goBottom功能一致
        if ((e.ctrlKey || e.metaKey) && e.key === 'End') {
            e.preventDefault();
            goBottom();
        }
    });

    // 初始化
    setTimeout(updateButtons, 1000);

})();