ChatGPT Character Counter

Adds a live character counter to the ChatGPT message composer.

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

You will need to install an extension such as Tampermonkey or Violentmonkey 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 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();

})();