ChatGPT Web Optimizer

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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();

})();