ChatGPT & Gemini Paste & Send

Adds a draggable floating button to ChatGPT and Gemini that pastes your clipboard and auto-sends the message. Auto-detects browser language.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ChatGPT & Gemini Paste & Send
// @namespace    https://greatest.deepsurf.us/users/1602450-gonzalo-uma%C3%B1a
// @version      1.0.0
// @description  Adds a draggable floating button to ChatGPT and Gemini that pastes your clipboard and auto-sends the message. Auto-detects browser language.
// @author       Gonzalo Umaña
// @match        *://chatgpt.com/*
// @match        *://gemini.google.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== I18N =====
    const TRANSLATIONS = {
        en: {
            paste: '📋 PASTE',
            sent: '✅ SENT',
            empty: '⚠️ EMPTY',
            noInput: '⚠️ NO INPUT',
            notSent: '⚠️ NOT SENT',
            error: '❌ ERROR',
            handleTitle: 'Drag to move · Double-click to reset position',
            logClipboard: 'Clipboard read failed:',
            logEmpty: 'Clipboard is empty.',
            logNoInput: 'Chat input field not found.',
            logNotSent: 'Send button not found.'
        },
        es: {
            paste: '📋 PEGAR',
            sent: '✅ ENVIADO',
            empty: '⚠️ VACÍO',
            noInput: '⚠️ SIN CAMPO',
            notSent: '⚠️ NO ENVIÓ',
            error: '❌ ERROR',
            handleTitle: 'Arrastrar para mover · Doble click para resetear',
            logClipboard: 'Error al leer el portapapeles:',
            logEmpty: 'Portapapeles vacío.',
            logNoInput: 'Campo de entrada no encontrado.',
            logNotSent: 'Botón de envío no encontrado.'
        },
        pt: {
            paste: '📋 COLAR',
            sent: '✅ ENVIADO',
            empty: '⚠️ VAZIO',
            noInput: '⚠️ SEM CAMPO',
            notSent: '⚠️ NÃO ENVIADO',
            error: '❌ ERRO',
            handleTitle: 'Arrastar para mover · Duplo clique para redefinir',
            logClipboard: 'Falha ao ler a área de transferência:',
            logEmpty: 'Área de transferência vazia.',
            logNoInput: 'Campo de entrada não encontrado.',
            logNotSent: 'Botão de envio não encontrado.'
        },
        fr: {
            paste: '📋 COLLER',
            sent: '✅ ENVOYÉ',
            empty: '⚠️ VIDE',
            noInput: '⚠️ SANS CHAMP',
            notSent: '⚠️ NON ENVOYÉ',
            error: '❌ ERREUR',
            handleTitle: 'Glisser pour déplacer · Double-cliquer pour réinitialiser',
            logClipboard: 'Échec de lecture du presse-papiers :',
            logEmpty: 'Presse-papiers vide.',
            logNoInput: 'Champ de saisie introuvable.',
            logNotSent: 'Bouton d\'envoi introuvable.'
        },
        de: {
            paste: '📋 EINFÜGEN',
            sent: '✅ GESENDET',
            empty: '⚠️ LEER',
            noInput: '⚠️ KEIN FELD',
            notSent: '⚠️ NICHT GESENDET',
            error: '❌ FEHLER',
            handleTitle: 'Zum Verschieben ziehen · Doppelklick zum Zurücksetzen',
            logClipboard: 'Zwischenablage konnte nicht gelesen werden:',
            logEmpty: 'Zwischenablage ist leer.',
            logNoInput: 'Eingabefeld nicht gefunden.',
            logNotSent: 'Senden-Button nicht gefunden.'
        },
        it: {
            paste: '📋 INCOLLA',
            sent: '✅ INVIATO',
            empty: '⚠️ VUOTO',
            noInput: '⚠️ NESSUN CAMPO',
            notSent: '⚠️ NON INVIATO',
            error: '❌ ERRORE',
            handleTitle: 'Trascina per spostare · Doppio clic per reimpostare',
            logClipboard: 'Lettura degli appunti non riuscita:',
            logEmpty: 'Appunti vuoti.',
            logNoInput: 'Campo di input non trovato.',
            logNotSent: 'Pulsante di invio non trovato.'
        }
    };

    function detectLanguage() {
        const browserLang = (navigator.language || 'en').slice(0, 2).toLowerCase();
        return TRANSLATIONS[browserLang] ? browserLang : 'en';
    }

    const T = TRANSLATIONS[detectLanguage()];

    // ===== CONFIG =====
    const HOST = location.hostname;
    const IS_GEMINI = HOST.includes('gemini.google.com');
    const DEFAULT_BOTTOM = 85;
    const DEFAULT_LEFT = 20;
    const BTN_ID = 'paste-send-btn';
    const HANDLE_ID = 'paste-send-handle';

    function createWidget() {
        if (document.getElementById(BTN_ID)) return;
        if (!document.body) return;

        const posBottom = parseFloat(localStorage.getItem('paste-send-bottom')) || DEFAULT_BOTTOM;
        const posLeft = parseFloat(localStorage.getItem('paste-send-left')) || DEFAULT_LEFT;

        // ===== MAIN BUTTON =====
        const btn = document.createElement('button');
        btn.id = BTN_ID;
        btn.textContent = T.paste;
        btn.style.cssText = `
            position: fixed !important;
            bottom: ${posBottom}px !important;
            left: ${posLeft}px !important;
            z-index: 2147483647 !important;
            padding: 6px 12px !important;
            background-color: #007bff !important;
            color: white !important;
            border: 1px solid #0056b3 !important;
            border-radius: 0 0 4px 4px !important;
            cursor: pointer !important;
            font-size: 11px !important;
            font-weight: bold !important;
            box-shadow: 0 2px 6px rgba(0,0,0,0.2) !important;
            font-family: sans-serif !important;
            user-select: none !important;
            white-space: nowrap !important;
            min-width: 130px !important;
            margin: 0 !important;
            box-sizing: border-box !important;
            line-height: normal !important;
        `;

        // ===== DRAG HANDLE =====
        const handle = document.createElement('div');
        handle.id = HANDLE_ID;
        handle.textContent = '⋮⋮';
        handle.title = T.handleTitle;
        handle.style.cssText = `
            position: fixed !important;
            bottom: ${posBottom + 24}px !important;
            left: ${posLeft}px !important;
            z-index: 2147483647 !important;
            height: 12px !important;
            background-color: #dee2e6 !important;
            color: #6c757d !important;
            font-size: 8px !important;
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;
            cursor: grab !important;
            border-radius: 4px 4px 0 0 !important;
            border: 1px solid #ced4da !important;
            border-bottom: none !important;
            user-select: none !important;
            box-sizing: border-box !important;
            margin: 0 !important;
        `;

        document.body.appendChild(btn);
        document.body.appendChild(handle);

        function syncHandle() {
            const rect = btn.getBoundingClientRect();
            handle.style.setProperty('width', rect.width + 'px', 'important');
            handle.style.setProperty('left', rect.left + 'px', 'important');
            handle.style.setProperty('bottom', (window.innerHeight - rect.top) + 'px', 'important');
        }
        syncHandle();
        setTimeout(syncHandle, 100);
        setTimeout(syncHandle, 500);

        // ===== HOVER =====
        btn.onmouseover = () => btn.style.setProperty('background-color', '#0056b3', 'important');
        btn.onmouseout = () => btn.style.setProperty('background-color', '#007bff', 'important');

        // ===== UNIFIED FEEDBACK =====
        let feedbackTimer = null;
        function showFeedback(text, bgColor, borderColor, duration = 1200) {
            if (feedbackTimer) clearTimeout(feedbackTimer);
            btn.textContent = text;
            btn.style.setProperty('background-color', bgColor, 'important');
            btn.style.setProperty('border-color', borderColor, 'important');
            feedbackTimer = setTimeout(() => {
                btn.textContent = T.paste;
                btn.style.setProperty('background-color', '#007bff', 'important');
                btn.style.setProperty('border-color', '#0056b3', 'important');
                feedbackTimer = null;
            }, duration);
        }

        // ===== PASTE + SEND LOGIC =====
        btn.onclick = async function(e) {
            e.preventDefault();

            // 1. Read clipboard
            let text = '';
            try {
                text = await navigator.clipboard.readText();
            } catch (err) {
                console.error('[Paste & Send]', T.logClipboard, err);
                showFeedback(T.error, '#dc3545', '#a71d2a', 1500);
                return;
            }

            if (!text || !text.trim()) {
                console.warn('[Paste & Send]', T.logEmpty);
                showFeedback(T.empty, '#fd7e14', '#c2570c', 1500);
                return;
            }

            // 2. Find chat input
            let input = null;
            if (HOST.includes('chatgpt.com')) {
                input = document.querySelector('#prompt-textarea')
                    || document.querySelector('div[contenteditable="true"][data-virtualkeyboard]')
                    || document.querySelector('main div[contenteditable="true"]');
            } else if (IS_GEMINI) {
                input = document.querySelector('rich-textarea div[contenteditable="true"]')
                    || document.querySelector('div.ql-editor[contenteditable="true"]')
                    || document.querySelector('div[contenteditable="true"]');
            }

            if (!input) {
                console.error('[Paste & Send]', T.logNoInput);
                showFeedback(T.noInput, '#fd7e14', '#c2570c', 1500);
                return;
            }

            // 3. Paste
            input.focus();

            if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
                input.select();
            } else {
                const range = document.createRange();
                range.selectNodeContents(input);
                const sel = window.getSelection();
                sel.removeAllRanges();
                sel.addRange(range);
            }

            // execCommand: required for Gemini to register the full text
            let inserted = false;
            try {
                inserted = document.execCommand('insertText', false, text);
            } catch (_) {
                inserted = false;
            }
            if (!inserted) {
                if (input.tagName === 'TEXTAREA' || input.tagName === 'INPUT') {
                    input.value = text;
                } else {
                    input.textContent = text;
                }
            }

            input.dispatchEvent(new InputEvent('input', {
                bubbles: true,
                cancelable: true,
                inputType: 'insertFromPaste',
                data: text
            }));

            // 4. Success feedback
            showFeedback(T.sent, '#28a745', '#1e7e34');

            // 5. Auto-send after 500ms
            setTimeout(() => {
                const selectors = [
                    'button[data-testid="send-button"]',
                    'button#composer-submit-button',
                    'button.send-button',
                    'button[aria-label="Enviar mensaje"]',
                    'button[aria-label="Send message"]',
                    'button[aria-label*="Enviar"]',
                    'button[aria-label*="Send"]'
                ];
                for (const sel of selectors) {
                    const sendBtn = document.querySelector(sel);
                    if (sendBtn && !sendBtn.disabled && sendBtn.offsetParent !== null) {
                        sendBtn.click();
                        return;
                    }
                }
                console.warn('[Paste & Send]', T.logNotSent);
                showFeedback(T.notSent, '#fd7e14', '#c2570c', 1800);
            }, 500);
        };

        // ===== DRAGGING =====
        let isDragging = false;
        let startMouseX, startMouseY, startBtnLeft, startBtnBottom;

        handle.onmousedown = function(e) {
            isDragging = true;
            handle.style.setProperty('cursor', 'grabbing', 'important');
            startMouseX = e.clientX;
            startMouseY = e.clientY;
            startBtnLeft = parseFloat(btn.style.left) || posLeft;
            startBtnBottom = parseFloat(btn.style.bottom) || posBottom;
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onRelease);
            e.preventDefault();
        };

        function onMove(e) {
            if (!isDragging) return;
            const deltaX = e.clientX - startMouseX;
            const deltaY = e.clientY - startMouseY;
            const rect = btn.getBoundingClientRect();
            const handleH = 12;

            const maxLeft = window.innerWidth - rect.width;
            const maxBottom = window.innerHeight - rect.height - handleH;

            const newLeft = Math.max(0, Math.min(maxLeft, startBtnLeft + deltaX));
            const newBottom = Math.max(0, Math.min(maxBottom, startBtnBottom - deltaY));

            btn.style.setProperty('left', newLeft + 'px', 'important');
            btn.style.setProperty('bottom', newBottom + 'px', 'important');
            syncHandle();
        }

        function onRelease() {
            if (!isDragging) return;
            isDragging = false;
            handle.style.setProperty('cursor', 'grab', 'important');
            const finalLeft = parseFloat(btn.style.left);
            const finalBottom = parseFloat(btn.style.bottom);
            localStorage.setItem('paste-send-left', finalLeft);
            localStorage.setItem('paste-send-bottom', finalBottom);
            document.removeEventListener('mousemove', onMove);
            document.removeEventListener('mouseup', onRelease);
        }

        handle.ondblclick = function(e) {
            e.preventDefault();
            btn.style.setProperty('left', DEFAULT_LEFT + 'px', 'important');
            btn.style.setProperty('bottom', DEFAULT_BOTTOM + 'px', 'important');
            syncHandle();
            localStorage.removeItem('paste-send-left');
            localStorage.removeItem('paste-send-bottom');
        };
    }

    setInterval(createWidget, 1500);
})();