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

目前為 2025-10-01 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    });
})();