Greasy Fork is available in English.

AI Studio Mod - Dynamic Table of Contents + Revert Scrollbar

Adds a dynamic TOC with smart scrolling for long sections to the settings panel.

Version au 01/09/2025. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         AI Studio Mod - Dynamic Table of Contents + Revert Scrollbar
// @namespace    https://greasyfork.org/users/137913
// @description  Adds a dynamic TOC with smart scrolling for long sections to the settings panel.
// @author       TigerYT
// @version      2.3.1
// @match        *://aistudio.google.com/prompts/*
// @icon         https://www.gstatic.com/aistudio/ai_studio_favicon_2_32x32.png
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. APPLY CUSTOM STYLES ---
    function applyCustomStyles() {
        const head = document.head || document.getElementsByTagName('head')[0];
        if (!head) {
            console.error('[TOC MOD] Could not find document head to inject styles.');
            return;
        }

        const style = document.createElement('style');
        style.type = 'text/css';
        style.id = 'ai-studio-mod-styles'; // Give it an ID to prevent duplicates
        style.textContent = `
            /* Revert scrollbar behavior by making the container's overflow visible */
            ms-autoscroll-container:not(#_) {
                overflow: visible;
            }
            /* Hide the custom scrollbar component */
            ms-prompt-scrollbar {
                display: none;
            }
            /* Common style for all jump links */
            .toc-jump-link {
                color: var(--google-blue-600, #1a73e8);
                text-decoration: none;
                font-weight: 500;
                cursor: pointer;
            }
            .toc-jump-link:hover {
                text-decoration: underline;
            }
        `;

        if (!document.getElementById(style.id)) {
            head.append(style);
        }
    }

    // --- 2. THE CORE LOGIC TO BUILD/UPDATE THE TABLE OF CONTENTS ---
    function updateTableOfContents(settingsContainer, chatBox) {

        // A. Clear any previously generated TOC elements to prevent duplication.
        const oldElements = settingsContainer.querySelectorAll('.toc-generated-item');
        oldElements.forEach(el => el.remove());

        // B. Create the static container elements for the TOC.
        const dividerElement = document.createElement('mat-divider');
        dividerElement.setAttribute('_ngcontent-ng-c1976526210', '');
        dividerElement.setAttribute('role', 'separator');
        dividerElement.setAttribute('aria-orientation', 'horizontal');
        dividerElement.className = 'mat-divider mat-divider-horizontal ng-star-inserted toc-generated-item';

        const headingElement = document.createElement('h3');
        headingElement.setAttribute('_ngcontent-ng-c1211128260', '');
        headingElement.className = 'thinking-group-title ng-tns-c1211128260-17 toc-generated-item';
        headingElement.textContent = 'Table of Contents';

        const tocItemsContainer = document.createElement('div');
        tocItemsContainer.setAttribute('_ngcontent-ng-c19765210', '');
        tocItemsContainer.className = 'advanced-settings ng-star-inserted toc-generated-item';

        // C. Find all chat turns and create a link for each one.
        const chatTurns = Array.from(chatBox.firstElementChild.children)
                               .filter((elem) => elem.tagName === 'MS-CHAT-TURN');

        if (chatTurns.length > 0) {
            chatTurns.forEach((elem, index) => {
                let responseName = 'Unknown Turn';

                const responseElem = elem?.firstElementChild?.children?.[1];
                if (responseElem) {
                    switch (responseElem.dataset.turnRole) {
                        case "User":
                            responseName = 'User Input';
                            break;
                        case "Model":
                            responseName = responseElem.querySelector('.author-label') ? 'Model Thinking' : 'Model Output';
                            break;
                    }
                }

                // --- STRUCTURE CREATION ---

                // 1. Create the main container DIV for the row
                const tocItemContainer = document.createElement('div');
                tocItemContainer.className = 'mat-mdc-tooltip-trigger settings-item ng-tns-c1211128260-12 toc-generated-item';
                tocItemContainer.setAttribute('_ngcontent-ng-c1211128260', '');

                // 2. Create the P element for the label
                const labelElement = document.createElement('p');
                labelElement.className = 'v3-font-body ng-tns-c1211128260-12';
                labelElement.setAttribute('_ngcontent-ng-c1211128260', '');
                labelElement.textContent = (index + 1) + '. ' + responseName;

                // 3. Create a span to hold all action links
                const actionsSpan = document.createElement('span');

                // 4. Check if the element is taller than the viewport
                const isTallerThanViewport = elem.offsetHeight > window.innerHeight;

                if (isTallerThanViewport) {
                    // If it's too tall, add the special 'scroll to bottom' link
                    const jumpLinkDown = document.createElement('a');
                    jumpLinkDown.className = 'toc-jump-link';
                    jumpLinkDown.textContent = '↓';
                    jumpLinkDown.title = 'Scroll to the bottom of this turn';
                    // Apply special styling
                    jumpLinkDown.style.color = 'oklch(from var(--color-v3-text-link) l c calc(h - 30))';
                    jumpLinkDown.style.marginRight = '1ch';
                    jumpLinkDown.addEventListener('click', () => {
                        elem.scrollIntoView({ block: 'end', inline: 'nearest', behavior: 'smooth' });
                    });
                    actionsSpan.append(jumpLinkDown);
                }

                // 5. Always add the standard 'Go To' (scroll to top) link
                const jumpLinkGoTo = document.createElement('a');
                jumpLinkGoTo.className = 'toc-jump-link';
                jumpLinkGoTo.textContent = 'Go To';
                jumpLinkGoTo.title = 'Scroll to the top of this turn';
                jumpLinkGoTo.addEventListener('click', () => {
                    elem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth' });
                });
                actionsSpan.append(jumpLinkGoTo);

                // 6. Assemble and append the final element
                tocItemContainer.append(labelElement, actionsSpan);
                tocItemsContainer.append(tocItemContainer);
            });
        }

        // D. Insert the newly created TOC elements into the settings panel.
        if (chatTurns.length > 0) {
            settingsContainer.insertAdjacentElement('beforeend', dividerElement);
            settingsContainer.insertAdjacentElement('beforeend', headingElement);
            settingsContainer.insertAdjacentElement('beforeend', tocItemsContainer);
        }
    }


    // --- 3. OBSERVER SETUP ---
    function initializeAndMonitor() {
        const observer = new MutationObserver((mutations, obs) => {
            const settingsContainer = document.querySelector('ms-prompt-run-settings');
            const chatBox = document.querySelector('ms-autoscroll-container');

            if (settingsContainer && chatBox && chatBox.firstElementChild) {
                obs.disconnect();
                updateTableOfContents(settingsContainer, chatBox);

                let debounceTimer;
                const chatObserver = new MutationObserver(() => {
                    clearTimeout(debounceTimer);
                    debounceTimer = setTimeout(() => {
                        updateTableOfContents(settingsContainer, chatBox);
                    }, 250);
                });

                chatObserver.observe(chatBox.firstElementChild, { childList: true });
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // --- 4. SCRIPT ENTRY POINT ---
    applyCustomStyles();
    initializeAndMonitor();

})();