Unsluggish ChatGPT Input

In a long chat typing is very sluggish. This script adds an alternative input area. (De)activate it via Ctrl+Alt+i

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Unsluggish ChatGPT Input
// @namespace    http://tampermonkey.net/
// @version      2.3.0
// @description  In a long chat typing is very sluggish. This script adds an alternative input area. (De)activate it via Ctrl+Alt+i
// @author       evermind-zz
// @homepage     https://github.com/evermind-zz/chatgpt-no-sluggish-input
// @license      GPL-3.0-or-later
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // ======== DEFAULT SETTINGS ========
    const defaultSettings = {
        overlayButton: true,
        enterCanStop: true
    };

    // Load persistent settings
    const savedSettings = JSON.parse(localStorage.getItem('overlay_settings') || '{}');
    window.OVERLAY_BUTTON = savedSettings.overlayButton ?? defaultSettings.overlayButton;
    window.OVERLAY_ENTER_CAN_STOP = savedSettings.enterCanStop ?? defaultSettings.enterCanStop;

    // ======== GLOBAL VARIABLES ========
    let wrapper = null;
    let overlay = null;
    let overlayBtn = null;
    let settingsIcon = null;
    let settingsPopup = null;
    let pollingInterval = null;
    window.FAST_INPUT_DEBUG = true;
    const MAX_HEIGHT = 300;

    // ======== ENUM STATES ========
    const ButtonState = {
        IDLE: 'idle',
        STOP: 'stop',
        SEND: 'send'
    };
    Object.freeze(ButtonState);

    function logDebug(msg, ...args) {
        if (window.FAST_INPUT_DEBUG) console.log(`[FastInputOverlay] ${msg}`, ...args);
    }

    // ======== HELPERS ========
    function getTextContainer() {
        const container = document.querySelector('#prompt-textarea');
        logDebug('getTextContainer:', container);
        return container;
    }

    function adjustHeight(el) {
        el.style.height = 'auto';
        el.style.height = Math.min(el.scrollHeight, MAX_HEIGHT) + 'px';
        el.style.overflowY = el.scrollHeight > MAX_HEIGHT ? 'auto' : 'hidden';
        logDebug('adjustHeight:', el.style.height);
    }

    function waitForSendButton(maxTime = 2000, intervalTime = 50) {
        return new Promise((resolve, reject) => {
            const start = Date.now();
            const interval = setInterval(() => {
                const btn = detectSendButton();
                if (btn) {
                    updateOverlayButton(ButtonState.SEND);
                    clearInterval(interval);
                    logDebug('Send button found');
                    resolve(btn);
                } else if (Date.now() - start > maxTime) {
                    clearInterval(interval);
                    logDebug('Send button not found within timeout');
                    reject('Send button not found');
                }
            }, intervalTime);
        });
    }

    function detectSendButton() {
        return document.querySelector('[data-testid="send-button"]');
    }

    function detectStopButton() {
        return document.querySelector('[data-testid="stop-button"]');
    }

    function getButtonState() {
        if (detectStopButton()) return ButtonState.STOP;
        if (detectSendButton()) return ButtonState.SEND;
        return ButtonState.IDLE;
    }

    function checkWhichButtonShown() {
        if (pollingInterval) return; // only one interval
        pollingInterval = setInterval(() => {
            if (!overlayBtn) return;
            const state = getButtonState();

            updateOverlayButton(state);

            switch (state) {
                case ButtonState.IDLE:
                    clearInterval(pollingInterval);
                    pollingInterval = null;
                    break;
                case ButtonState.STOP:
                case ButtonState.SEND:
                    break;
            }
        }, 200);
    }

    function updateOverlayButton(state) {
        if (!overlayBtn) return;

        switch (state) {
            case ButtonState.IDLE:
                overlayBtn.innerText = '✉️'; // Send icon
                overlayBtn.style.background = '#d4f8d4'; // light green
                overlayBtn.style.color = '#006400'; // dark green
                break;

            case ButtonState.SEND:
                overlayBtn.innerText = '...'; // briefly when sending
                overlayBtn.style.background = '#f0f0f0'; // neutral
                overlayBtn.style.color = '#000';
                break;

            case ButtonState.STOP:
                overlayBtn.innerText = '⏹'; // Stop icon
                overlayBtn.style.background = '#f8d4d4'; // light red
                overlayBtn.style.color = '#8b0000'; // dark red
                break;
        }

        logDebug('Overlay button updated →', state);
    }

    function copyTextToChatGPT(text) {
        const container = getTextContainer();
        if (!container) {
            logDebug('Text container not found');
            return;
        }

        const lines = text.split('\n');
        const html = lines.map(line => `<p>${line || '&nbsp;'}</p>`).join('');
        container.innerHTML = html;
        logDebug('Overlay text copied:', lines);
    }

    // ======== SETTINGS POPUP ========
    function createSettingsPopup() {
        settingsPopup = document.createElement('div');
        settingsPopup.style.position = 'absolute';
        settingsPopup.style.bottom = '100%';
        settingsPopup.style.left = '0';
        settingsPopup.style.background = '#fff';
        settingsPopup.style.border = '1px solid #ccc';
        settingsPopup.style.borderRadius = '6px';
        settingsPopup.style.padding = '10px';
        settingsPopup.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
        settingsPopup.style.display = 'none';
        settingsPopup.style.zIndex = '10000';
        settingsPopup.style.fontSize = '14px';

        // Overlay button toggle
        const btnCheckbox = document.createElement('input');
        btnCheckbox.type = 'checkbox';
        btnCheckbox.checked = window.OVERLAY_BUTTON;
        btnCheckbox.id = 'overlayBtnCheckbox';
        const btnLabel = document.createElement('label');
        btnLabel.innerText = 'Show Overlay Button';
        btnLabel.htmlFor = 'overlayBtnCheckbox';
        btnLabel.style.marginLeft = '4px';
        btnLabel.style.marginRight = '10px';
        btnCheckbox.addEventListener('change', () => {
            window.OVERLAY_BUTTON = btnCheckbox.checked;
            overlayBtn.style.display = window.OVERLAY_BUTTON ? 'inline-block' : 'none';
            saveSettings();
        });

        // Enter can stop toggle
        const enterCheckbox = document.createElement('input');
        enterCheckbox.type = 'checkbox';
        enterCheckbox.checked = window.OVERLAY_ENTER_CAN_STOP;
        enterCheckbox.id = 'enterCanStopCheckbox';
        const enterLabel = document.createElement('label');
        enterLabel.innerText = 'Enter can Stop';
        enterLabel.htmlFor = 'enterCanStopCheckbox';
        enterCheckbox.addEventListener('change', () => {
            window.OVERLAY_ENTER_CAN_STOP = enterCheckbox.checked;
            saveSettings();
        });

        settingsPopup.appendChild(btnCheckbox);
        settingsPopup.appendChild(btnLabel);
        settingsPopup.appendChild(enterCheckbox);
        settingsPopup.appendChild(enterLabel);

        settingsIcon.appendChild(settingsPopup);
    }

    function toggleSettingsPopup() {
        if (!settingsPopup) return;
        settingsPopup.style.display = settingsPopup.style.display === 'none' ? 'block' : 'none';
    }

    function saveSettings() {
        localStorage.setItem('overlay_settings', JSON.stringify({
            overlayButton: window.OVERLAY_BUTTON,
            enterCanStop: window.OVERLAY_ENTER_CAN_STOP
        }));
    }

    // ======== OVERLAY CREATION ========
    function createOverlay() {
        if (wrapper) return;

        wrapper = document.createElement('div');
        wrapper.style.position = 'fixed';
        wrapper.style.bottom = '20px';
        wrapper.style.left = '50%';
        wrapper.style.transform = 'translateX(-50%)';
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'flex-end';
        wrapper.style.gap = '8px';
        wrapper.style.width = '60%';
        wrapper.style.zIndex = '9999';

        // Settings icon
        settingsIcon = document.createElement('div');
        settingsIcon.innerText = '⚙️';
        settingsIcon.style.cursor = 'pointer';
        settingsIcon.style.fontSize = '18px';
        settingsIcon.addEventListener('click', toggleSettingsPopup);
        wrapper.appendChild(settingsIcon);

        // Overlay textarea
        overlay = document.createElement('textarea');
        overlay.style.width = '100%';
        overlay.style.height = '80px';
        overlay.style.fontSize = '16px';
        overlay.style.padding = '10px';
        overlay.style.border = '2px solid #0078D7';
        overlay.style.borderRadius = '8px';
        overlay.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
        overlay.style.resize = 'none';
        overlay.style.overflowY = 'hidden';
        overlay.placeholder = 'Type here and press Enter (Shift+Enter for newline)...';
        overlay.addEventListener('input', () => adjustHeight(overlay));

        overlay.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                handleOverlayAction();
                adjustHeight(overlay);
                overlay.focus();
            }
        });

        wrapper.appendChild(overlay);

        // Optional Overlay button
        if (window.OVERLAY_BUTTON) {
            overlayBtn = document.createElement('button');
            overlayBtn.style.padding = '10px';
            overlayBtn.style.borderRadius = '6px';
            overlayBtn.style.border = '1px solid #0078D7';
            overlayBtn.style.background = '#f0f0f0';
            overlayBtn.style.cursor = 'pointer';
            overlayBtn.onclick = handleOverlayAction;
            updateOverlayButton(ButtonState.IDLE);
            wrapper.appendChild(overlayBtn);
        }

        document.body.appendChild(wrapper);
        createSettingsPopup();
    }

    // ======== STOP BUTTON HANDLING ========
    function clickStopButton(maxTime = 1000, intervalTime = 50) {
        return new Promise((resolve, reject) => {
            const start = Date.now();
            const poll = setInterval(() => {
                const stopBtn = detectStopButton();
                if (stopBtn) {
                    clearInterval(poll);
                    logDebug('Stop button found → clicking');
                    stopBtn.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
                    resolve(true);
                } else if (Date.now() - start > maxTime) {
                    clearInterval(poll);
                    logDebug('Stop button not found within timeout');
                    resolve(false);
                }
            }, intervalTime);
        });
    }

    // ======== HANDLE ACTION ========
    function handleOverlayAction() {
        const text = overlay.value.trim();
        const state = getButtonState();

        logDebug('handleOverlayAction → state:', state);

        updateOverlayButton(state);

        switch (state) {
            case ButtonState.IDLE: // at that time the chatgpt button is still idle
                if (text) {
                    copyTextToChatGPT(text);
                    waitForSendButton()
                        .then(btn => {
                            btn.click();
                            overlay.value = '';
                            checkWhichButtonShown();
                        })
                        .catch(console.error);
                } else {
                    logDebug('IDLE state but no text → nothing to send');
                }
                break;

            case ButtonState.STOP:
                if (window.OVERLAY_ENTER_CAN_STOP) {
                    clickStopButton().then(clicked => {
                        if (!clicked) logDebug('No stop button to click');
                        logDebug('STOP state and Enter-stop executed');
                    });
                } else {
                    logDebug('STOP state but Enter-stop disabled');
                }
                break;

            case ButtonState.SEND:
            default:
                logDebug('Send already underway → do nothing');
                break;
        }
    }

    // ======== TOGGLE OVERLAY ========
    function toggleOverlay() {
        if (!wrapper) createOverlay();
        if (wrapper.style.display === 'none' || wrapper.style.display === '') {
            wrapper.style.display = 'flex';
            overlay.focus();
            adjustHeight(overlay);
            logDebug('Overlay shown');
        } else {
            wrapper.style.display = 'none';
            logDebug('Overlay hidden');
        }
    }

    // ======== HOTKEY ========
    document.addEventListener('keydown', (e) => {
        if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'i') {
            e.preventDefault();
            e.stopPropagation();
            toggleOverlay();
        }
    }, true);

    window.addEventListener('load', () => {
        setTimeout(createOverlay, 1000);
    });
})();