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);
})();