ChatGPT Character Counter

Adds a live character counter to the ChatGPT message composer.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         ChatGPT Character Counter
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Adds a live character counter to the ChatGPT message composer.
// @author       Emree.el on Instagram
// @match        https://chatgpt.com/*
// @match        https://www.chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const LIMIT = 32732;
    const COUNTER_ID = 'emree-chatgpt-char-counter';

    let editor = null;
    let counter = null;

    function findEditor() {
        // Current ChatGPT editor.
        const proseMirror = document.querySelector(
            '#prompt-textarea.ProseMirror[contenteditable="true"]'
        );

        if (proseMirror) return proseMirror;

        // Fallback if ChatGPT changes the class structure.
        const editable = document.querySelector(
            '#prompt-textarea[contenteditable="true"]'
        );

        if (editable) return editable;

        // Older/fallback textarea.
        return document.querySelector(
            'textarea[name="prompt-textarea"]'
        );
    }

    function getCharacterCount() {
        if (!editor) return 0;

        if (editor instanceof HTMLTextAreaElement) {
            return editor.value.length;
        }

        // ProseMirror / contenteditable editor.
        return (editor.textContent || '').length;
    }

    function updateCounter() {
        if (!editor || !counter || !editor.isConnected) return;

        const count = getCharacterCount();

        counter.textContent =
            `${count.toLocaleString()}/${LIMIT.toLocaleString()}`;

        if (count > LIMIT) {
            counter.style.color = '#ff5555';
            counter.style.textShadow = '0 0 8px rgba(255, 0, 0, 0.7)';
        } else if (count > 0) {
            counter.style.color = '#55ff88';
            counter.style.textShadow = '0 0 8px rgba(0, 255, 100, 0.45)';
        } else {
            counter.style.color = '';
            counter.style.textShadow = 'none';
        }
    }

    function attach(editorElement) {
        // Already attached to this editor.
        if (editor === editorElement && counter?.isConnected) {
            updateCounter();
            return;
        }

        // Remove old counter.
        document.getElementById(COUNTER_ID)?.remove();

        editor = editorElement;

        counter = document.createElement('div');
        counter.id = COUNTER_ID;

        Object.assign(counter.style, {
            fontSize: '12px',
            fontWeight: '600',
            marginTop: '4px',
            paddingRight: '8px',
            textAlign: 'right',
            opacity: '0.8',
            pointerEvents: 'none',
            userSelect: 'none'
        });

        const form = editor.closest('form');

        if (form) {
            form.insertAdjacentElement('afterend', counter);
        } else {
            editor.insertAdjacentElement('afterend', counter);
        }

        // The main event: catches typing, deleting and pasting.
        editor.addEventListener('input', () => {
            updateCounter();

            // Let ProseMirror/React finish any DOM updates.
            requestAnimationFrame(updateCounter);
        });

        // Extra fallback for keyboard input.
        editor.addEventListener('keyup', () => {
            requestAnimationFrame(updateCounter);
        });

        // Extra fallback for paste.
        editor.addEventListener('paste', () => {
            requestAnimationFrame(() => {
                requestAnimationFrame(updateCounter);
            });
        });

        updateCounter();
    }

    function checkEditor() {
        const found = findEditor();

        if (!found) return;

        if (found !== editor || !counter?.isConnected) {
            attach(found);
        }
    }

    // Very lightweight SPA check.
    setInterval(checkEditor, 1000);

    checkEditor();

})();