ChatGPT Web Optimizer

A user script to optimize ChatGPT's DOM structure, effectively preventing lag during long conversations.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         ChatGPT Web Optimizer
// @name:zh-TW   ChatGPT Web 優化器
// @name:zh-CN   ChatGPT Web 优化器
// @name:ja      ChatGPT Web オプティマイザ
// @namespace    https://github.com/April-15/tampermonkey-scripts/blob/main/ChatGPT_Web_Optimizer.js
// @version      0.1.0
// @description  A user script to optimize ChatGPT's DOM structure, effectively preventing lag during long conversations.
// @description:zh-TW  優化 ChatGPT 網頁效能,自動隔離過往聊天對話,解決長對話造成的網頁卡頓問題。
// @description:zh-CN  优化 ChatGPT 网页性能,自动隔离过往聊天对话,解决长对话造成的网页卡顿问题。
// @description:ja  ChatGPTの長文チャットによるラグを防ぐため、過去のチャットを自動的に分離してウェブページのパフォーマンスを最適化するスクリプトです。
// @author       April 15th
// @match        https://chatgpt.com/*
// @icon         https://chatgpt.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ============================================
    // i18n / 多语言设置
    // ============================================
    const i18n = {
        en: {
            title: "ChatGPT Web Optimizer",
            enable: "Enable Optimization",
            bufferDistance: "Buffer Margin px",
            language: "Language/语言",
            save: "Save & Reload",
            info: "Info: Scrolled out nodes are hidden via content-visibility to eliminate reflow lag without breaking React. Save & Reload to apply.",
            optimizedCount: "Hidden Nodes: ",
            panelToggle: "DOM Optimizer",
            hideBtn: "Hide Floating Button (Open via Menu)"
        },
        zh_CN: {
            title: "ChatGPT Web 优化",
            enable: "启用优化",
            bufferDistance: "缓冲距离 px",
            language: "语言/Language",
            save: "保存并刷新",
            info: "当对话节点滚出视口+设定缓冲距离后,自动通过 CSS 隔离计算开销,彻底消除打字卡顿。更改设置后请保存刷新生效。",
            optimizedCount: "当前隐藏节点: ",
            panelToggle: "DOM优化",
            hideBtn: "隐藏悬浮按钮 (仅从菜单打开)"
        },
        zh_TW: {
            title: "ChatGPT Web 優化器",
            enable: "啟用優化",
            bufferDistance: "緩衝距離 px",
            language: "語言/Language",
            save: "保存並刷新",
            info: "當對話節點滾出視口+設定緩衝距離後,自動通過 CSS 隔離計算開銷,徹底消除打字卡頓。更改設定後請保存刷新生效。",
            optimizedCount: "當前隱藏節點: ",
            panelToggle: "DOM優化",
            hideBtn: "隱藏懸浮按鈕 (僅從菜單打開)"
        },
        ja: {
            title: "ChatGPT Web オプティマイザ",
            enable: "最適化を有効化",
            bufferDistance: "バッファマージン px",
            language: "言語/Language",
            save: "保存して再読み込み",
            info: "情報: ビューポート外のノードは content-visibility によって非表示になり、React を壊さずにリフローラグを排除します。適用するには保存して再読み込みしてください。",
            optimizedCount: "非表示ノード数: ",
            panelToggle: "DOM最適化",
            hideBtn: "フローティングボタンを非表示 (メニューから開く)"
        }
    };

    // ============================================
    // 配置与状态保存
    // ============================================
    let settings = {
        enabled: GM_getValue('enabled', true),
        bufferDistance: parseInt(GM_getValue('bufferDistance', 2500)),
        lang: GM_getValue('lang', 'en'),
        hideBtn: GM_getValue('hideBtn', false)
    };

    const t = i18n[settings.lang] || i18n['en'];

    // ============================================
    // 核心逻辑: CSS-based Virtual DOM Manager
    // ============================================
    const hiddenSections = new Set();
    let statsElement = null;

    function updateOptimizedCount() {
        if (statsElement) {
            statsElement.textContent = t.optimizedCount + hiddenSections.size;
        }
    }

    const domObserver = new IntersectionObserver((entries) => {
        if (!settings.enabled) return;

        entries.forEach(entry => {
            const section = entry.target;
            const turnId = section.getAttribute('data-turn-id') || section.getAttribute('data-testid');
            if (!turnId) return;

            if (!entry.isIntersecting) {
                // Out of view -> Hide internal nodes
                if (!hiddenSections.has(turnId)) {
                    const rect = section.getBoundingClientRect();
                    if (rect.height > 10) {
                        // Lock exact height
                        section.style.boxSizing = 'border-box';
                        section.style.height = rect.height + 'px';

                        // Strict CSS containment stops reflow propagation
                        section.style.contain = 'strict';
                        section.style.contentVisibility = 'hidden';

                        // Apply display: none to children as a fallback for absolute isolation
                        Array.from(section.children).forEach(child => {
                            child.dataset.optiOrigDisplay = child.style.display || '';
                            child.style.display = 'none';
                        });

                        hiddenSections.add(turnId);
                        updateOptimizedCount();
                    }
                }
            } else {
                // In view -> Restore
                if (hiddenSections.has(turnId)) {
                    // Remove locks
                    section.style.height = '';
                    section.style.boxSizing = '';
                    section.style.contain = '';
                    section.style.contentVisibility = '';

                    // Recover children
                    Array.from(section.children).forEach(child => {
                        if (child.hasAttribute('data-opti-orig-display')) {
                            child.style.display = child.dataset.optiOrigDisplay;
                            child.removeAttribute('data-opti-orig-display');
                        }
                    });

                    hiddenSections.delete(turnId);
                    updateOptimizedCount();
                }
            }
        });
    }, {
        // Expand root margin so elements render well before they reach screen
        rootMargin: `${settings.bufferDistance}px 0px`
    });

    function observeNewNodes() {
        const sections = document.querySelectorAll('section[data-testid^="conversation-turn-"]');
        sections.forEach(sec => domObserver.observe(sec));
    }

    // Used to detect dynamically appended React messages
    const mutationObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            mutation.addedNodes.forEach((node) => {
                if (node.nodeType === 1) { // Node.ELEMENT_NODE
                    if (node.tagName && node.tagName.toLowerCase() === 'section' && node.matches('[data-testid^="conversation-turn-"]')) {
                        domObserver.observe(node);
                    } else if (node.querySelectorAll) {
                        const sections = node.querySelectorAll('section[data-testid^="conversation-turn-"]');
                        if (sections.length > 0) {
                            sections.forEach(sec => domObserver.observe(sec));
                        }
                    }
                }
            });
        });
    });

    // ============================================
    // 初始化 & UI 组件挂载 (安全隔离 Shadow DOM)
    // ============================================
    function renderUIPanel() {
        const host = document.createElement('div');
        host.id = 'opti-cgpt-host';
        host.style.position = 'fixed';
        host.style.bottom = '20px';
        host.style.right = '20px';
        host.style.zIndex = '2147483647'; // Maximum z-index

        // Attach Shadow DOM so ChatGPTs CSS does not hide our settings panel
        const shadow = host.attachShadow({ mode: 'open' });

        const style = document.createElement('style');
        style.textContent = `
            * { box-sizing: border-box; }
            #opti-panel {
                width: 260px;
                background-color: #202123;
                color: #ececf1;
                border: 1px solid #4d4d4f;
                padding: 16px;
                border-radius: 12px;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
                box-shadow: 0 4px 20px rgba(0,0,0,0.6);
                display: none;
                flex-direction: column;
                gap: 12px;
                position: absolute;
                bottom: 45px;
                right: 0;
            }
            #opti-panel.show {
                display: flex;
            }
            .opti-header {
                font-weight: 600;
                font-size: 15px;
                cursor: default;
                user-select: none;
                padding-bottom: 6px;
                border-bottom: 1px solid #4d4d4f;
            }
            .opti-row {
                display: flex;
                justify-content: space-between;
                align-items: center;
                font-size: 13px;
            }
            select, input {
                background: #343541;
                color: white;
                border: 1px solid #565869;
                border-radius: 6px;
                padding: 6px;
                outline: none;
            }
            select:focus, input:focus { border-color: #10a37f; }
            input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; }
            button.opti-save-btn {
                background-color: #10a37f;
                color: white;
                border: none;
                padding: 10px;
                border-radius: 6px;
                cursor: pointer;
                font-size: 14px;
                font-weight: bold;
                margin-top: 5px;
                transition: background 0.2s;
            }
            button.opti-save-btn:hover { background-color: #0b8c6c; }
            .opti-info {
                font-size: 11px;
                color: #8e8ea0;
                line-height: המח 1.4;
            }
            #opti-stats {
                font-size: 12px;
                color: #10a37f;
                font-weight: 600;
            }
            .opti-toggle-btn {
                background: #343541;
                color: #ececf1;
                border: 1px solid #565869;
                border-radius: 8px;
                padding: 8px 16px;
                cursor: pointer;
                font-size: 13px;
                font-weight: 500;
                box-shadow: 0 2px 10px rgba(0,0,0,0.5);
                transition: opacity 0.2s;
                opacity: 0.6;
            }
            .opti-toggle-btn:hover { opacity: 1; }
        `;
        shadow.appendChild(style);

        // Toggle Button
        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'opti-toggle-btn';
        toggleBtn.textContent = '⚙️ ' + t.panelToggle;
        if (settings.hideBtn) {
            toggleBtn.style.display = 'none';
        }
        shadow.appendChild(toggleBtn);

        // Panel Container
        const panel = document.createElement('div');
        panel.id = 'opti-panel';

        panel.innerHTML = `
            <div class="opti-header">🎯 ${t.title}</div>

            <div class="opti-row">
                <label for="opti-enable">${t.enable}</label>
                <input type="checkbox" id="opti-enable" ${settings.enabled ? 'checked' : ''}>
            </div>

            <div class="opti-row">
                <label for="opti-lang">${t.language}</label>
                <select id="opti-lang">
                    <option value="zh_CN" ${settings.lang === 'zh_CN' ? 'selected' : ''}>简体中文 (zh-CN)</option>
                    <option value="zh_TW" ${settings.lang === 'zh_TW' ? 'selected' : ''}>繁體中文 (zh-TW)</option>
                    <option value="en" ${settings.lang === 'en' ? 'selected' : ''}>English (en)</option>
                    <option value="ja" ${settings.lang === 'ja' ? 'selected' : ''}>日本語 (ja)</option>
                </select>
            </div>

            <div class="opti-row">
                <label for="opti-buffer">${t.bufferDistance}</label>
                <input type="number" id="opti-buffer" style="width: 70px;" step="500" min="500" value="${settings.bufferDistance}">
            </div>

            <div class="opti-row">
                <label for="opti-hideBtn">${t.hideBtn}</label>
                <input type="checkbox" id="opti-hideBtn" ${settings.hideBtn ? 'checked' : ''}>
            </div>

            <div class="opti-info">${t.info}</div>

            <div id="opti-stats">${t.optimizedCount}0</div>

            <button class="opti-save-btn" id="opti-save">${t.save}</button>
        `;
        shadow.appendChild(panel);

        document.body.appendChild(host);

        statsElement = shadow.getElementById('opti-stats');

        // Toggle Behavior
        toggleBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            panel.classList.toggle('show');
        });

        // Click outside to close
        document.addEventListener('click', (e) => {
            if (e.composedPath().includes(host)) return;
            panel.classList.remove('show');
        });

        // Save Function
        shadow.getElementById('opti-save').addEventListener('click', () => {
            GM_setValue('enabled', shadow.getElementById('opti-enable').checked);
            GM_setValue('lang', shadow.getElementById('opti-lang').value);
            GM_setValue('bufferDistance', shadow.getElementById('opti-buffer').value);
            GM_setValue('hideBtn', shadow.getElementById('opti-hideBtn').checked);
            window.location.reload();
        });

        GM_registerMenuCommand(t.panelToggle, () => {
            panel.classList.add('show');
        });
    }

    // ============================================
    // 防崩溃启动程序
    // ============================================
    function bootstrap() {
        if (!document.body) {
            // Wait for body to be available
            setTimeout(bootstrap, 200);
            return;
        }

        try {
            renderUIPanel();

            if (settings.enabled) {
                // Initialize observer
                mutationObserver.observe(document.body, { childList: true, subtree: true });
                // Grab any already loaded elements
                observeNewNodes();
            }
        } catch (e) {
            console.error('[OptiCGPTWeb] Failed to initialize:', e);
        }
    }

    bootstrap();

})();