QuickNav for Google AI Studio

Restores classic chat navigation in Google AI Studio, adding essential UI controls for precise, message-by-message browsing, and a powerful message index menu for efficient conversation navigation. This script operates entirely locally in your browser, does not collect any personal data, and makes no requests to external servers.

当前为 2025-10-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         QuickNav for Google AI Studio
// @namespace    http://tampermonkey.net/
// @version      18.19
// @description  Restores classic chat navigation in Google AI Studio, adding essential UI controls for precise, message-by-message browsing, and a powerful message index menu for efficient conversation navigation. This script operates entirely locally in your browser, does not collect any personal data, and makes no requests to external servers.
// @author       Axl_script
// @homepageURL  https://greasyfork.org/en/scripts/548346-quicknav-for-google-ai-studio
// @contributionURL https://nowpayments.io/embeds/donation-widget?api_key=0fe4e67c-64aa-4a74-b2d2-a91608b1ccc6
// @match        https://aistudio.google.com/*
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const ChatNavigator = {
        allTurns: [],
        currentIndex: -1,
        menuFocusedIndex: -1,
        isDownButtonAtEndToggle: false,
        JUMP_DISTANCE: 5,
        scrollSyncTimeout: null,
        isScrollingProgrammatically: false,
        isQueueProcessing: false,
        isUnstickingFromBottom: false,
        originalScrollTop: 0,
        originalCurrentIndex: -1,
        loadingQueue: [],
        totalToLoad: 0,
        mainLoopInterval: null,
        activeObserver: null,
        observedElement: null,
        holdTimeout: null,
        holdInterval: null,
        isBottomHoldActive: false,
        bottomHoldTimeout: null,
        bottomHoldInterval: null,

        init() {
            this.injectStyles();
            this.setupScrollListener();
            if (document.getElementById('quicknav-badge-floater')) return;
            const badgeFloater = document.createElement('div');
            badgeFloater.id = 'quicknav-badge-floater';
            const badgeIndex = document.createElement('div');
            badgeIndex.id = 'quicknav-badge-index';
            const badgePercentage = document.createElement('div');
            badgePercentage.id = 'quicknav-badge-percentage';
            badgeFloater.append(badgeIndex, badgePercentage);
            document.body.appendChild(badgeFloater);
            if (this.mainLoopInterval) clearInterval(this.mainLoopInterval);
            this.mainLoopInterval = setInterval(() => this.mainLoop(), 750);
        },

        mainLoop() {
            const chatContainer = document.querySelector('ms-autoscroll-container');

            if (chatContainer) {
                if (!this.activeObserver || this.observedElement !== chatContainer) {
                    this.cleanupChatObserver();
                    this.initializeChatObserver(chatContainer);
                    this.buildTurnIndex();
                }
            } else {
                if (this.activeObserver) {
                    this.cleanupChatObserver();
                    this.allTurns = [];
                    this.currentIndex = -1;
                }
            }
            this.ensureUIState();
        },

        ensureUIState() {
            const targetNode = document.querySelector('ms-prompt-input-wrapper');
            const uiExists = document.getElementById('chat-nav-container');

            if (targetNode && !uiExists) {
                this.createAndInjectUI(targetNode);
            } else if (!targetNode && uiExists) {
                uiExists.remove();
                const menu = document.getElementById('chat-nav-menu-container');
                if (menu) menu.remove();
            }
        },

        initializeChatObserver(container) {
            this.observedElement = container;
            this.activeObserver = new MutationObserver(mutations => {
                const hasRelevantChanges = mutations.some(mutation => {
                    const checkNode = mutation.target.nodeType === Node.ELEMENT_NODE ? mutation.target : mutation.target.parentElement;
                    if (checkNode && checkNode.closest('.edit')) {
                        return false;
                    }
                    return mutation.type === 'childList';
                });

                if (hasRelevantChanges) {
                    this.buildTurnIndex();
                }
            });
            this.activeObserver.observe(container, { childList: true, subtree: true });
        },

        cleanupChatObserver() {
            if (this.activeObserver) {
                this.activeObserver.disconnect();
            }
            this.activeObserver = null;
            this.observedElement = null;
        },

        getTurnType(turnElement) {
            const turnContainer = turnElement.querySelector('.chat-turn-container');
            if (!turnContainer) return 'unknown';
            if (turnContainer.classList.contains('user')) return 'user_prompt';
            if (turnContainer.classList.contains('model')) {
                return turnElement.querySelector('ms-thought-chunk') ? 'model_thought' : 'model_response';
            }
            return 'unknown';
        },

        buildTurnIndex() {
            const allFoundTurns = Array.from(document.querySelectorAll('ms-chat-turn'));
            const freshTurns = allFoundTurns.filter(turn => {
                const type = this.getTurnType(turn);
                return type === 'user_prompt' || type === 'model_response';
            });

            if (freshTurns.length !== this.allTurns.length || !this.arraysEqual(this.allTurns, freshTurns)) {
                const contentCache = new Map();
                this.allTurns.forEach(turn => {
                    if (turn.id && turn.cachedContent) {
                        contentCache.set(turn.id, { content: turn.cachedContent, isFallback: turn.isFallbackContent });
                    }
                });
                this.allTurns = freshTurns;
                this.allTurns.forEach(turn => {
                    if (turn.id && contentCache.has(turn.id)) {
                        const cachedData = contentCache.get(turn.id);
                        turn.cachedContent = cachedData.content;
                        turn.isFallbackContent = cachedData.isFallback;
                    } else {
                        turn.cachedContent = null;
                        turn.isFallbackContent = false;
                    }
                });
                this.updateCounterDisplay();
                if (this.currentIndex >= this.allTurns.length) this.synchronizeCurrentIndexFromView();
            } else {
                this.updateCounterDisplay();
            }
        },

        arraysEqual(a, b) {
            if (a.length !== b.length) return false;
            for (let i = 0; i < a.length; i++) {
                if (a[i] !== b[i]) return false;
            }
            return true;
        },

        setupScrollListener() {
            document.addEventListener('scroll', () => {
                if (this.isScrollingProgrammatically || this.isQueueProcessing || this.isUnstickingFromBottom) return;
                this.updateScrollPercentage();
                clearTimeout(this.scrollSyncTimeout);
                this.scrollSyncTimeout = setTimeout(() => this.synchronizeCurrentIndexFromView(), 150);
            }, true);
        },

        injectStyles() {
            if (document.getElementById('chat-nav-styles')) return;
            const styleSheet = document.createElement("style");
            styleSheet.id = 'chat-nav-styles';
            styleSheet.textContent = `
                @keyframes google-text-flow {
                    to { background-position: 200% center; }
                }
                #chat-nav-container { display: flex; justify-content: center; align-items: center; gap: 12px; margin: 2px auto; width: 100%; box-sizing: border-box; }
                .counter-wrapper { position: relative; pointer-events: none; z-index: 9999; }

                .chat-nav-button, #chat-nav-counter {
                    background-color: transparent;
                    border: 1px solid var(--ms-on-surface-variant, #888888);
                    transition: transform 0.1s ease-out, background-color 0.15s ease-in-out;
                    pointer-events: auto;
                    user-select: none;
                    cursor: pointer;
                }

                .chat-nav-button {
                    color: var(--ms-on-surface-variant, #888888);
                    flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%;
                }

                #nav-up, #nav-down {
                    color: #8ab4f8;
                    border-color: #8ab4f8;
                }

                #chat-nav-counter {
                    font-family: 'Google Sans', sans-serif;
                    font-size: 12px;
                    padding: 4px 8px;
                    border-radius: 8px;
                    display: inline-flex;
                    align-items: baseline;
                    color: var(--ms-on-surface-variant, #888888);
                }

                .chat-nav-button:hover,
                .chat-nav-button:focus {
                    background-color: var(--ms-surface-3, #F1F3F4);
                    outline: none;
                }

                #nav-top:hover, #nav-bottom:hover,
                #nav-top:focus, #nav-bottom:focus {
                    background-color: rgba(136, 136, 136, 0.15);
                }

                #nav-up:hover, #nav-down:hover,
                #nav-up:focus, #nav-down:focus,
                #chat-nav-counter:hover, #chat-nav-counter:focus {
                    background-color: rgba(138, 180, 248, 0.15);
                    outline: none;
                }

                .chat-nav-button:active { transform: scale(0.95); }
                #nav-bottom.auto-click-active { transform: scale(0.9); background-color: #d1c4e9; border-color: #9575cd; color: #4527a0; }

                #chat-nav-current-num.chat-nav-current-grey {
                    color: var(--ms-on-surface-variant, #888888);
                }
                #chat-nav-current-num.chat-nav-current-blue {
                    color: #8ab4f8;
                    font-weight: 500;
                }
                #chat-nav-total-num {
                    color: #8ab4f8;
                    font-weight: 500;
                }

                .prompt-turn-highlight, .response-turn-highlight { position: relative; border-radius: 12px; }
                .prompt-turn-highlight::after, .response-turn-highlight::after { content: ""; display: table; clear: both; }
                .prompt-turn-highlight { box-shadow: 0 0 0 1px #9aa0a6 !important; }
                .response-turn-highlight { box-shadow: 0 0 0 1px #8ab4f8 !important; }
                #quicknav-badge-floater { position: fixed; z-index: 99998; visibility: hidden; display: flex; flex-direction: column; align-items: center; pointer-events: none; padding: 4px 7px; border-radius: 8px; font-family: 'Google Sans', sans-serif; }
                #quicknav-badge-index { font-size: 13px; font-weight: 500; line-height: 1.2; }
                #quicknav-badge-percentage { font-size: 10px; font-weight: 400; line-height: 1.2; border-top: 1px solid rgba(255, 255, 255, 0.3); margin-top: 3px; padding-top: 3px; }
                .prompt-badge-bg { background-color: #5f6368; color: #FFFFFF; }
                .response-badge-bg { background-color: #174ea6; color: #FFFFFF; }
                #chat-nav-menu-container { display: none; flex-direction: column; position: fixed; background-color: #191919; border: 2px solid #8ab4f8; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); max-height: 90vh; z-index: 99999; max-width: 800px; min-width: 300px; pointer-events: auto; box-sizing: border-box; }
                #chat-nav-menu-container:focus { outline: none; }
                #chat-nav-menu { list-style: none; margin: 0; padding: 0 8px 8px 8px; overflow-y: auto; scroll-behavior: smooth; flex-grow: 1; border-radius: 0 0 10px 10px; background-color: #191919; }
                .chat-nav-menu-item { display: flex; align-items: center; color: #e8eaed; padding: 8px 12px; margin: 2px 0; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: 'Google Sans', sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background-color: #202124; }
                .chat-nav-menu-item:hover { background-color: #3c4043; }
                .chat-nav-menu-item.menu-item-focused { background-color: #5f6368; }
                .menu-item-number { font-weight: 500; margin-right: 8px; flex-shrink: 0; }
                .prompt-number-color { color: #9aa0a6; }
                .response-number-color { color: #8ab4f8; }
                .menu-item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
                .prompt-item-bg { border-left: 3px solid #9aa0a6; margin-left: 32px; }
                .response-item-bg { border-left: 3px solid #8ab4f8; border-bottom: 1px solid #8ab4f8; }
                .chat-nav-menu-header { flex-shrink: 0; background-color: #191919; z-index: 1; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid #8ab4f8; border-radius: 10px 10px 0 0; }
                .header-controls { flex: 1; display: flex; align-items: center; }
                .header-controls.left { justify-content: flex-start; }
                .header-controls.right { justify-content: flex-end; }
                .quicknav-title { flex: 2; text-align: center; font-family: 'Google Sans', 'Inter Tight', sans-serif; font-size: 14px; font-weight: 600; background: linear-gradient(90deg, #8ab4f8, #e67c73, #f7cb73, #57bb8a, #8ab4f8); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent; animation: google-text-flow 10s linear infinite; user-select: none; }
                .header-button { font-family: 'Google Sans', sans-serif; text-decoration: none; font-size: 12px; color: #e8eaed; background-color: #3c4043; padding: 4px 10px; border-radius: 16px; transition: background-color 0.15s ease-in-out, opacity 0.15s ease-in-out; border: none; cursor: pointer; }
                .header-button:hover:not(:disabled) { background-color: #5f6368; }
                .header-button:disabled { opacity: 0.5; cursor: not-allowed; }
                #chat-nav-menu::-webkit-scrollbar { width: 8px; }
                #chat-nav-menu::-webkit-scrollbar-track { background: #202124; }
                #chat-nav-menu::-webkit-scrollbar-thumb { background-color: #5f6368; border-radius: 4px; }
                #chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #9aa0a6; }
                #chat-nav-loader-status { font-family: 'Google Sans', sans-serif; font-size: 12px; color: #9aa0a6; padding: 4px 10px; }
            `;
            document.head.appendChild(styleSheet);
        },

        createAndInjectUI(targetNode) {
            const navContainer = document.createElement('div');
            navContainer.id = 'chat-nav-container';

            const pathTop = 'M12 4l-6 6 1.41 1.41L12 6.83l4.59 4.58L18 10z M12 12l-6 6 1.41 1.41L12 14.83l4.59 4.58L18 18z';
            const pathUp = 'M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z';
            const pathDown = 'M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10z';
            const pathBottom = 'M12 12l-6-6 1.41-1.41L12 9.17l4.59-4.58L18 6z M12 20l-6-6 1.41-1.41L12 17.17l4.59-4.58L18 14z';

            const btnTop = this.createButton('nav-top', 'Go to first message (Home)', pathTop);
            const btnUp = this.createButton('nav-up', 'Navigate to previous message', pathUp);

            const counterWrapper = document.createElement('div');
            counterWrapper.className = 'counter-wrapper';

            const counter = document.createElement('span');
            counter.id = 'chat-nav-counter';
            counter.tabIndex = 0;
            counter.setAttribute('role', 'button');
            counter.setAttribute('aria-haspopup', 'true');
            counter.setAttribute('aria-expanded', 'false');

            const currentNumSpan = document.createElement('span');
            currentNumSpan.id = 'chat-nav-current-num';
            counter.appendChild(currentNumSpan);

            const separatorSpan = document.createElement('span');
            separatorSpan.id = 'chat-nav-separator';
            separatorSpan.textContent = ' / ';
            counter.appendChild(separatorSpan);

            const totalNumSpan = document.createElement('span');
            totalNumSpan.id = 'chat-nav-total-num';
            counter.appendChild(totalNumSpan);

            const btnDown = this.createButton('nav-down', 'Navigate to next message', pathDown);
            const btnBottom = this.createButton('nav-bottom', 'Go to last message (End)', pathBottom);

            let menuContainer = document.getElementById('chat-nav-menu-container');
            if (!menuContainer) {
                menuContainer = document.createElement('div');
                menuContainer.id = 'chat-nav-menu-container';
                menuContainer.tabIndex = -1;
                menuContainer.setAttribute('role', 'menu');
                document.body.appendChild(menuContainer);
            }

            counterWrapper.append(counter);
            navContainer.append(btnTop, btnUp, counterWrapper, btnDown, btnBottom);

            const parentContainer = targetNode.closest('footer');
            if (parentContainer && parentContainer.parentNode) {
                parentContainer.parentNode.insertBefore(navContainer, parentContainer);
                this.attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer);
                this.updateCounterDisplay();
            }
        },

        createButton(id, ariaLabel, pathData) {
            const button = document.createElement('button');
            button.id = id;
            button.className = 'chat-nav-button';
            button.setAttribute('aria-label', ariaLabel);
            const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            svg.setAttribute('height', '24px');
            svg.setAttribute('viewBox', '0 0 24 24');
            svg.setAttribute('width', '24px');
            svg.setAttribute('fill', 'currentColor');
            const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
            path.setAttribute('d', pathData);
            svg.appendChild(path);
            button.appendChild(svg);
            return button;
        },

        setupHoldableButton(button, action) {
            const HOLD_DELAY = 300;
            const HOLD_INTERVAL = 100;

            let isHolding = false;

            const stopHold = () => {
                clearTimeout(this.holdTimeout);
                clearInterval(this.holdInterval);
                this.holdInterval = null;
                isHolding = false;
            };

            button.addEventListener('click', (e) => {
                if (!isHolding) {
                    action();
                }
                stopHold();
            });

            button.addEventListener('mousedown', (e) => {
                if (e.button !== 0) return;
                isHolding = true;
                this.holdTimeout = setTimeout(() => {
                    if (isHolding) {
                        this.holdInterval = setInterval(action, HOLD_INTERVAL);
                    }
                }, HOLD_DELAY);
            });

            button.addEventListener('mouseup', stopHold);
            button.addEventListener('mouseleave', stopHold);
        },

        attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer) {
            const lastIndex = () => this.allTurns.length - 1;

            this.setupHoldableButton(btnTop, () => this.scrollToAbsoluteTop());

            this.setupHoldableButton(btnUp, async () => {
                this.synchronizeCurrentIndexFromView();
                const last = lastIndex();
                const scrollContainer = document.querySelector('ms-autoscroll-container');

                if (scrollContainer && this.currentIndex === last && last > -1) {
                    const currentTurn = this.allTurns[this.currentIndex];
                    const targetScrollTop = currentTurn.offsetTop - (scrollContainer.clientHeight * 0.3);
                    const isAtFocalPoint = Math.abs(scrollContainer.scrollTop - targetScrollTop) < 5;

                    if (!isAtFocalPoint) {
                        await this.navigateToIndex(last, 'center');
                        return;
                    }
                }

                await this.navigateToIndex(this.currentIndex - 1, 'center');
            });

            this.setupHoldableButton(btnDown, async () => {
                this.synchronizeCurrentIndexFromView();
                const last = lastIndex();
                if (this.currentIndex < last) {
                    await this.navigateToIndex(this.currentIndex + 1, 'center');
                } else {
                    const scrollContainer = document.querySelector('ms-autoscroll-container');
                    if (!scrollContainer) return;
                    if (this.isDownButtonAtEndToggle) {
                        await this.navigateToIndex(last, 'center');
                        this.isDownButtonAtEndToggle = false;
                    } else {
                        this.isScrollingProgrammatically = true;
                        scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
                        setTimeout(() => { this.isScrollingProgrammatically = false; }, 800);
                        this.isDownButtonAtEndToggle = true;
                    }
                }
            });

            let ignoreNextBottomClick = false;

            const bottomNavAction = async () => {
                this.synchronizeCurrentIndexFromView();
                const last = lastIndex();
                if (this.currentIndex === last) {
                    const scrollContainer = document.querySelector('ms-autoscroll-container');
                    if (scrollContainer) {
                        this.isScrollingProgrammatically = true;
                        scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: this.isBottomHoldActive ? 'auto' : 'smooth' });
                        setTimeout(() => { this.isScrollingProgrammatically = false; }, this.isBottomHoldActive ? 50 : 800);
                    }
                } else {
                    await this.navigateToIndex(last, 'center');
                }
            };

            btnBottom.addEventListener('mousedown', () => {
                if (this.isBottomHoldActive) return;

                this.bottomHoldTimeout = setTimeout(() => {
                    this.isBottomHoldActive = true;
                    btnBottom.classList.add('auto-click-active');
                    ignoreNextBottomClick = true;
                    bottomNavAction();
                    this.bottomHoldInterval = setInterval(bottomNavAction, 150);
                }, 1000);
            });

            const stopBottomHoldDetector = () => {
                clearTimeout(this.bottomHoldTimeout);
            };

            btnBottom.addEventListener('mouseup', stopBottomHoldDetector);
            btnBottom.addEventListener('mouseleave', stopBottomHoldDetector);

            btnBottom.addEventListener('click', () => {
                if (ignoreNextBottomClick) {
                    ignoreNextBottomClick = false;
                    return;
                }

                if (this.isBottomHoldActive) {
                    this.isBottomHoldActive = false;
                    clearInterval(this.bottomHoldInterval);
                    this.bottomHoldInterval = null;
                    btnBottom.classList.remove('auto-click-active');
                } else {
                    bottomNavAction();
                }
            });

            counter.addEventListener('click', (e) => { e.stopPropagation(); this.toggleNavMenu(); });
            counter.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.toggleNavMenu(); } });
            menuContainer.addEventListener('keydown', (e) => {
                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                if (!items.length) return;
                let newIndex = this.menuFocusedIndex;
                let shouldUpdateFocus = true;
                switch (e.key) {
                    case 'ArrowDown': e.preventDefault(); newIndex = (this.menuFocusedIndex + 1) % items.length; break;
                    case 'ArrowUp': e.preventDefault(); newIndex = (this.menuFocusedIndex - 1 + items.length) % items.length; break;
                    case 'PageDown': e.preventDefault(); newIndex = Math.min(items.length - 1, this.menuFocusedIndex + this.JUMP_DISTANCE); break;
                    case 'PageUp': e.preventDefault(); newIndex = Math.max(0, this.menuFocusedIndex - this.JUMP_DISTANCE); break;
                    case 'Home': e.preventDefault(); newIndex = 0; break;
                    case 'End': e.preventDefault(); newIndex = items.length - 1; break;
                    case 'Enter': e.preventDefault(); if (this.menuFocusedIndex !== -1) { items[this.menuFocusedIndex].click(); } shouldUpdateFocus = false; break;
                    case 'Escape': e.preventDefault(); this.toggleNavMenu(); shouldUpdateFocus = false; break;
                    default: shouldUpdateFocus = false; break;
                }
                if (shouldUpdateFocus && newIndex !== this.menuFocusedIndex) { this.updateMenuFocus(items, newIndex); }
            });
        },

        scrollToAbsoluteTop() {
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer) return;

            this.isScrollingProgrammatically = true;

            scrollContainer.scrollTo({
                top: 0,
                behavior: this.holdInterval ? 'auto' : 'smooth'
            });

            if (this.allTurns.length > 0 && this.currentIndex !== 0) {
                this.updateHighlight(this.currentIndex, 0);
                this.currentIndex = 0;
                this.updateCounterDisplay();
            }

            setTimeout(() => {
                this.isScrollingProgrammatically = false;
            }, this.holdInterval ? 50 : 800);
        },

        waitForTurnToStabilize(turnElement, timeout = 1000) {
            return new Promise((resolve, reject) => {
                let lastRect = turnElement.getBoundingClientRect();
                let stableChecks = 0;
                const STABLE_CHECKS_REQUIRED = 3;
                const CHECK_INTERVAL = 100;

                const intervalId = setInterval(() => {
                    if (!turnElement || !turnElement.isConnected) {
                        clearInterval(intervalId);
                        clearTimeout(timeoutId);
                        return reject(new Error('Target element was removed from DOM.'));
                    }
                    const currentRect = turnElement.getBoundingClientRect();

                    if (currentRect.top !== lastRect.top || currentRect.height !== lastRect.height) {
                        lastRect = currentRect;
                        stableChecks = 0;
                    } else {
                        stableChecks++;
                    }

                    if (stableChecks >= STABLE_CHECKS_REQUIRED) {
                        clearInterval(intervalId);
                        clearTimeout(timeoutId);
                        resolve();
                    }
                }, CHECK_INTERVAL);

                const timeoutId = setTimeout(() => {
                    clearInterval(intervalId);
                    reject(new Error(`Element stabilization timed out.`));
                }, timeout);
            });
        },

        async scrollToTurn(index, blockPosition = 'center') {
            const targetTurn = this.allTurns[index];
            if (!targetTurn) {
                this.isScrollingProgrammatically = false;
                return;
            }

            this.isScrollingProgrammatically = true;
            const isSmooth = !this.isQueueProcessing && !this.holdInterval;
            const scrollContainer = document.querySelector('ms-autoscroll-container');

            if (!scrollContainer) {
                this.isScrollingProgrammatically = false;
                return;
            }

            targetTurn.scrollIntoView({ behavior: 'auto', block: 'center' });

            try {
                await this.waitForTurnToStabilize(targetTurn, 2000);

                let targetScrollTop;
                if (blockPosition === 'start') {
                    targetScrollTop = targetTurn.offsetTop;
                } else if (blockPosition === 'end') {
                    targetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight - targetTurn.offsetHeight);
                } else { // 'center' or default
                    targetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight * 0.3);
                }

                targetScrollTop = Math.max(0, targetScrollTop);
                targetScrollTop = Math.min(targetScrollTop, scrollContainer.scrollHeight - scrollContainer.clientHeight);

                scrollContainer.scrollTo({ top: targetScrollTop, behavior: isSmooth ? 'smooth' : 'auto' });

                const timeoutDuration = isSmooth ? 800 : 50;
                await new Promise(resolve => setTimeout(resolve, timeoutDuration));

            } catch (error) {
                console.warn('QuickNav:', error.message, 'Proceeding with final position.');
            } finally {
                this.isScrollingProgrammatically = false;
            }
        },

        async navigateToIndex(newIndex, blockPosition = 'center') {
            if (newIndex < 0 || newIndex >= this.allTurns.length) return;

            const oldIndex = this.currentIndex;

            if (newIndex < this.allTurns.length - 1) this.isDownButtonAtEndToggle = false;

            this.currentIndex = newIndex;
            this.updateHighlight(oldIndex, newIndex);
            this.updateCounterDisplay();
            await this.scrollToTurn(newIndex, blockPosition);
            this.updateScrollPercentage();
        },

        updateScrollPercentage() {
            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;
            if (this.currentIndex < 0) {
                floater.style.visibility = 'hidden';
                return;
            }
            const currentTurn = this.allTurns[this.currentIndex];
            if (!currentTurn) {
                floater.style.visibility = 'hidden';
                return;
            }

            const rect = currentTurn.getBoundingClientRect();
            const turnHeight = rect.height;
            const isVisible = rect.bottom > 8 && rect.top < window.innerHeight;

            if (!isVisible || turnHeight <= 0) {
                floater.style.visibility = 'hidden';
                return;
            }

            floater.style.visibility = 'visible';
            const floaterHeight = floater.offsetHeight || 40;
            const idealTop = (window.innerHeight / 2) - (floaterHeight / 2);

            const upperBound = Math.max(8, rect.top);
            const lowerBound = Math.min(window.innerHeight, rect.bottom) - floaterHeight - 4;

            let finalTop = idealTop;
            finalTop = Math.max(finalTop, upperBound);
            finalTop = Math.min(finalTop, lowerBound);

            floater.style.top = `${finalTop}px`;
            floater.style.left = `${rect.right - (floater.offsetWidth / 2)}px`;

            const percentageBadge = document.getElementById('quicknav-badge-percentage');
            if (!percentageBadge) return;

            const viewportCenterY = window.innerHeight / 2;
            const distanceScrolled = viewportCenterY - rect.top;

            const percentage = (distanceScrolled / turnHeight) * 100;
            const clampedPercentage = Math.max(0, Math.min(percentage, 100));

            percentageBadge.textContent = `${Math.round(clampedPercentage)}%`;
        },

        synchronizeCurrentIndexFromView() {
            if (this.isUnstickingFromBottom || this.isScrollingProgrammatically || this.allTurns.length === 0) {
                if (this.allTurns.length === 0) {
                    this.updateHighlight(this.currentIndex, -1);
                    this.currentIndex = -1;
                }
                this.updateCounterDisplay();
                return;
            }

            if (this.currentIndex > -1 && this.currentIndex < this.allTurns.length) {
                const currentTurn = this.allTurns[this.currentIndex];
                const rect = currentTurn.getBoundingClientRect();
                if (rect.top < window.innerHeight && rect.bottom > 0) {
                    this.updateScrollPercentage();
                    return;
                }
            }

            const focusPointY = window.innerHeight * 0.35;
            let bestMatch = { index: -1, delta: Infinity };
            this.allTurns.forEach((turn, index) => {
                const rect = turn.getBoundingClientRect();
                if (rect.top < window.innerHeight && rect.bottom > 0) {
                    const delta = Math.abs(rect.top - focusPointY);
                    if (delta < bestMatch.delta) {
                        bestMatch = { index, delta };
                    }
                }
            });

            if (bestMatch.index !== -1 && this.currentIndex !== bestMatch.index) {
                this.updateHighlight(this.currentIndex, bestMatch.index);
                this.currentIndex = bestMatch.index;
                this.updateCounterDisplay();
                this.updateScrollPercentage();
            }
        },

        updateHighlight(oldIndex, newIndex) {
            if (oldIndex > -1 && oldIndex < this.allTurns.length) {
                this.allTurns[oldIndex].classList.remove('prompt-turn-highlight', 'response-turn-highlight');
            }

            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;

            if (newIndex > -1 && newIndex < this.allTurns.length) {
                const newTurn = this.allTurns[newIndex];
                const isPrompt = this.getTurnType(newTurn) === 'user_prompt';
                newTurn.classList.add(isPrompt ? 'prompt-turn-highlight' : 'response-turn-highlight');

                const badgeIndex = document.getElementById('quicknav-badge-index');
                const badgeClass = isPrompt ? 'prompt-badge-bg' : 'response-badge-bg';

                if (badgeIndex) {
                    badgeIndex.textContent = newIndex + 1;
                }
                floater.className = badgeClass;
                floater.style.visibility = 'visible';
                this.updateScrollPercentage();
            } else {
                floater.style.visibility = 'hidden';
            }
        },

        updateCounterDisplay() {
            let currentNumSpan = document.getElementById('chat-nav-current-num');
            let totalNumSpan = document.getElementById('chat-nav-total-num');
            const counterContainer = document.getElementById('chat-nav-counter');

            if (!currentNumSpan || !totalNumSpan || !counterContainer) {
                const counter = document.getElementById('chat-nav-counter');
                if (counter) {
                    const current = this.currentIndex > -1 ? this.currentIndex + 1 : '-';
                    counter.textContent = `${current} / ${this.allTurns.length}`;
                }
                return;
            }

            const current = this.currentIndex > -1 ? this.currentIndex + 1 : '-';
            const total = this.allTurns.length;

            currentNumSpan.textContent = current;
            totalNumSpan.textContent = total;

            currentNumSpan.classList.remove('chat-nav-current-grey', 'chat-nav-current-blue');
            if (this.currentIndex === total - 1 && total > 0) {
                currentNumSpan.classList.add('chat-nav-current-blue');
            } else {
                currentNumSpan.classList.add('chat-nav-current-grey');
            }
        },

        toggleNavMenu() {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const counter = document.getElementById('chat-nav-counter');
            if (!menuContainer || !counter) return;

            const isVisible = menuContainer.style.display === 'flex';
            if (isVisible) {
                menuContainer.style.display = 'none';
                counter.setAttribute('aria-expanded', 'false');
                document.removeEventListener('click', this.closeNavMenu, true);
                this.stopDynamicMenuLoading();
                counter.focus();
            } else {
                this.populateNavMenu();
                const chatContainer = document.querySelector('ms-chunk-editor');
                if (chatContainer) {
                    const chatWidth = chatContainer.clientWidth;
                    const finalWidth = Math.max(300, Math.min(chatWidth, 800));
                    menuContainer.style.width = `${finalWidth}px`;
                }
                const counterRect = counter.getBoundingClientRect();
                menuContainer.style.bottom = `${window.innerHeight - counterRect.top + 8}px`;
                menuContainer.style.left = `${counterRect.left + (counterRect.width / 2)}px`;
                menuContainer.style.transform = 'translateX(-50%)';

                menuContainer.style.display = 'flex';
                counter.setAttribute('aria-expanded', 'true');
                menuContainer.focus();

                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                const initialFocusIndex = this.currentIndex > -1 ? this.currentIndex : 0;
                this.updateMenuFocus(items, initialFocusIndex, false);

                requestAnimationFrame(() => {
                    const availableSpace = counterRect.top - 18;
                    menuContainer.style.maxHeight = `${availableSpace}px`;
                    if (this.menuFocusedIndex > -1 && this.menuFocusedIndex < items.length) {
                        const menuList = document.getElementById('chat-nav-menu');
                        const focusedItem = items[this.menuFocusedIndex];
                        if (menuList && focusedItem) {
                            menuList.scrollTop = focusedItem.offsetTop - menuList.offsetTop - (menuList.clientHeight / 2) + (focusedItem.clientHeight / 2);
                        }
                    }
                });
                setTimeout(() => document.addEventListener('click', this.closeNavMenu, true), 0);
            }
        },

        closeNavMenu(e) {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const counter = document.getElementById('chat-nav-counter');
            if (menuContainer && counter && !menuContainer.contains(e.target) && !counter.contains(e.target) && menuContainer.style.display === 'flex') {
                this.toggleNavMenu();
            }
        },

        updateMenuFocus(items, newIndex, shouldScroll = true) {
            if (!items || items.length === 0 || newIndex < 0 || newIndex >= items.length) return;
            if (this.menuFocusedIndex > -1 && this.menuFocusedIndex < items.length) {
                items[this.menuFocusedIndex].classList.remove('menu-item-focused');
            }
            items[newIndex].classList.add('menu-item-focused');
            if (shouldScroll) {
                const menuList = document.getElementById('chat-nav-menu');
                const focusedItem = items[newIndex];
                if (menuList && focusedItem) {
                    const itemRect = focusedItem.getBoundingClientRect();
                    const menuRect = menuList.getBoundingClientRect();
                    if (itemRect.bottom > menuRect.bottom) {
                        menuList.scrollTop += itemRect.bottom - menuRect.bottom;
                    } else if (itemRect.top < menuRect.top) {
                        menuList.scrollTop -= menuRect.top - itemRect.top;
                    }
                }
            }
            this.menuFocusedIndex = newIndex;
        },

        getTextFromTurn(turn, fromDOMOnly = false) {
            turn.isFallbackContent = false;
            if (!fromDOMOnly) {
                const turnId = turn.id;
                if (turnId) {
                    const scrollbarButton = document.getElementById(`scrollbar-item-${turnId.replace('turn-', '')}`);
                    if (scrollbarButton && scrollbarButton.getAttribute('aria-label')) {
                        const labelText = scrollbarButton.getAttribute('aria-label');
                        return { display: labelText, full: labelText, source: 'scrollbar' };
                    }
                }
            }
            const contentContainer = turn.querySelector('.turn-content');
            if (contentContainer) {
                const clonedContainer = contentContainer.cloneNode(true);
                clonedContainer.querySelectorAll('ms-code-block').forEach(codeBlockElement => {
                    const codeContent = codeBlockElement.querySelector('pre code');
                    if (codeContent) {
                        const pre = document.createElement('pre');
                        pre.textContent = ` ${codeContent.textContent} `;
                        codeBlockElement.parentNode.replaceChild(pre, codeBlockElement);
                    } else { codeBlockElement.remove(); }
                });
                clonedContainer.querySelectorAll('.author-label, .turn-separator, ms-thought-chunk').forEach(el => el.remove());
                const text = clonedContainer.textContent?.trim().replace(/\s+/g, ' ');
                if (text) return { display: text, full: text, source: 'dom' };
            }
            turn.isFallbackContent = true;
            return { display: '...', full: 'Could not extract content.', source: 'fallback' };
        },

        populateNavMenu() {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            if (!menuContainer) return;
            while (menuContainer.firstChild) { menuContainer.removeChild(menuContainer.firstChild); }
            const header = document.createElement('div');
            header.className = 'chat-nav-menu-header';
            const menuList = document.createElement('ul');
            menuList.id = 'chat-nav-menu';
            this.allTurns.forEach((turn, index) => {
                let displayContent;
                if (turn.cachedContent && !turn.isFallbackContent) {
                    displayContent = turn.cachedContent;
                } else {
                    displayContent = this.getTextFromTurn(turn);
                }
                const { display, full } = displayContent;
                const truncatedText = (display.length > 200) ? display.substring(0, 197) + '...' : display;
                const item = document.createElement('li');
                item.className = 'chat-nav-menu-item';
                item.setAttribute('role', 'menuitem');
                const isPrompt = this.getTurnType(turn) === 'user_prompt';
                item.classList.add(isPrompt ? 'prompt-item-bg' : 'response-item-bg');
                const numberSpan = document.createElement('span');
                numberSpan.className = `menu-item-number ${isPrompt ? 'prompt-number-color' : 'response-number-color'}`;
                numberSpan.textContent = `${index + 1}.`;
                const textSpan = document.createElement('span');
                textSpan.className = 'menu-item-text';
                textSpan.textContent = truncatedText;
                item.append(numberSpan, textSpan);
                item.title = full.replace(/\s+/g, ' ');
                item.addEventListener('click', () => {
                    this.toggleNavMenu();
                    this.navigateToIndex(index);
                });
                menuList.appendChild(item);
            });
            const leftContainer = document.createElement('div');
            leftContainer.className = 'header-controls left';
            const loadButton = document.createElement('button');
            loadButton.id = 'chat-nav-load-button';
            loadButton.className = 'header-button';
            loadButton.textContent = 'Load All';
            loadButton.title = 'Load full text for all messages';
            loadButton.addEventListener('click', (e) => {
                e.stopPropagation();
                this.startDynamicMenuLoading();
            });
            const statusIndicator = document.createElement('span');
            statusIndicator.id = 'chat-nav-loader-status';
            leftContainer.append(loadButton, statusIndicator);
            const titleElement = document.createElement('div');
            titleElement.className = 'quicknav-title';
            titleElement.textContent = 'QuickNav for Google AI Studio';
            const rightContainer = document.createElement('div');
            rightContainer.className = 'header-controls right';
            const donateButton = document.createElement('a');
            donateButton.href = 'https://nowpayments.io/embeds/donation-widget?api_key=0fe4e67c-64aa-4a74-b2d2-a91608b1ccc6';
            donateButton.target = '_blank';
            donateButton.rel = 'noopener noreferrer';
            donateButton.className = 'header-button';
            donateButton.textContent = 'Donate ☕';
            donateButton.title = 'Support the developer';
            donateButton.addEventListener('click', e => e.stopPropagation());
            rightContainer.appendChild(donateButton);
            header.append(leftContainer, titleElement, rightContainer);
            menuContainer.appendChild(header);
            menuContainer.appendChild(menuList);
        },

        async forceScrollToTop(scrollContainer) {
            return new Promise(resolve => {
                this.isScrollingProgrammatically = true;
                const firstTurn = this.allTurns[0];
                if (!firstTurn) {
                    this.isScrollingProgrammatically = false;
                    return resolve();
                }
                let attempts = 0;
                const maxAttempts = 15;
                const attemptScroll = () => {
                    attempts++;
                    scrollContainer.scrollTop = 0;
                    setTimeout(() => {
                        if (scrollContainer.scrollTop < 50 || attempts >= maxAttempts) {
                            firstTurn.scrollIntoView({ behavior: 'auto', block: 'start' });
                            setTimeout(() => { this.isScrollingProgrammatically = false; resolve(); }, 100);
                        } else {
                            attemptScroll();
                        }
                    }, 150);
                };
                attemptScroll();
            });
        },

        async startDynamicMenuLoading() {
            if (this.isQueueProcessing) return;
            const loadButton = document.getElementById('chat-nav-load-button');
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer || !loadButton) return;

            this.originalCurrentIndex = this.currentIndex;
            this.originalScrollTop = scrollContainer.scrollTop;

            const menuItems = document.querySelectorAll('#chat-nav-menu .chat-nav-menu-item');
            this.loadingQueue = this.allTurns
                .map((turn, index) => ({ turn, index, menuItem: menuItems[index] }))
                .filter(item => {
                    const text = item.menuItem.title;
                    return text.endsWith('...') || text === 'Could not extract content.' || !item.turn.cachedContent || item.turn.isFallbackContent;
                });

            if (this.loadingQueue.length > 0) {
                this.totalToLoad = this.loadingQueue.length;
                loadButton.disabled = true;
                this.isQueueProcessing = true;
                await this.forceScrollToTop(scrollContainer);
                this.processLoadingQueue();
            } else {
                const statusIndicator = document.getElementById('chat-nav-loader-status');
                if (statusIndicator) statusIndicator.textContent = 'All loaded.';
            }
        },

        pollForContent(turn) {
            return new Promise((resolve, reject) => {
                const maxAttempts = 50;
                let attempts = 0;
                const interval = setInterval(() => {
                    if (!this.isQueueProcessing) {
                        clearInterval(interval);
                        return reject(new Error('Loading stopped by user.'));
                    }
                    const content = this.getTextFromTurn(turn, true);
                    if (content.source === 'dom') {
                        clearInterval(interval);
                        resolve(content);
                    } else if (++attempts >= maxAttempts) {
                        clearInterval(interval);
                        reject(new Error('Content polling timed out.'));
                    }
                }, 100);
            });
        },

        async processLoadingQueue() {
            const statusIndicator = document.getElementById('chat-nav-loader-status');
            const menuItems = document.querySelectorAll('#chat-nav-menu .chat-nav-menu-item');
            while (this.loadingQueue.length > 0 && this.isQueueProcessing) {
                const itemsProcessed = this.totalToLoad - this.loadingQueue.length;
                if (statusIndicator) statusIndicator.textContent = `Loading ${itemsProcessed + 1} of ${this.totalToLoad}...`;
                const itemToLoad = this.loadingQueue.shift();
                const { turn, index, menuItem } = itemToLoad;
                const textSpan = menuItem.querySelector('.menu-item-text');
                if (!turn || !textSpan) continue;
                this.updateMenuFocus(menuItems, index, true);
                try {
                    await this.scrollToTurn(index, 'center');
                    const newContent = await this.pollForContent(turn);
                    turn.cachedContent = newContent;
                    turn.isFallbackContent = false;
                    const truncatedText = (newContent.display.length > 200) ? newContent.display.substring(0, 197) + '...' : newContent.display;
                    textSpan.textContent = truncatedText;
                    menuItem.title = newContent.full.replace(/\s+/g, ' ');
                } catch (error) {
                    console.error(`Failed to load item ${index + 1}:`, error.message);
                    textSpan.textContent = '[Error]';
                }
            }

            this.isQueueProcessing = false;
            const scrollContainer = document.querySelector('ms-autoscroll-container');

            if (this.originalCurrentIndex > -1 && this.originalCurrentIndex < this.allTurns.length) {
                await this.navigateToIndex(this.originalCurrentIndex, 'center');
            } else if (scrollContainer) {
                this.isScrollingProgrammatically = true;
                scrollContainer.scrollTo({ top: this.originalScrollTop, behavior: 'smooth' });
                await new Promise(resolve => setTimeout(() => { this.isScrollingProgrammatically = false; resolve(); }, 800));
            }

            const menuContainer = document.getElementById('chat-nav-menu-container');
            const items = menuContainer ? menuContainer.querySelectorAll('.chat-nav-menu-item') : [];
            if (menuContainer && menuContainer.style.display === 'flex' && this.currentIndex > -1 && items.length > 0) {
                this.updateMenuFocus(items, this.currentIndex, true);
            }

            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) loadButton.disabled = false;
            if (statusIndicator) statusIndicator.textContent = this.loadingQueue.length > 0 ? 'Stopped.' : 'Done.';
        },

        stopDynamicMenuLoading() {
            if (!this.isQueueProcessing) return;
            this.isQueueProcessing = false;
            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) loadButton.disabled = false;
            const statusIndicator = document.getElementById('chat-nav-loader-status');
            if (statusIndicator) statusIndicator.textContent = 'Stopped.';
        }
    };

    ChatNavigator.closeNavMenu = ChatNavigator.closeNavMenu.bind(ChatNavigator);
    ChatNavigator.init();
})();