Adds a draggable floating button to ChatGPT and Gemini that pastes your clipboard and auto-sends the message. Auto-detects browser language.
// ==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);
})();