|
// ==UserScript== |
|
// @name Chat Cmd+Enter to Submit |
|
// @namespace https://intellectronica.net/ |
|
// @version 1.0.0 |
|
// @description Makes Enter add a newline and Cmd+Enter (or Ctrl+Enter) submit in Claude, ChatGPT, Gemini, and Google AI Mode |
|
// @author Eleanor Berger <[email protected]> |
|
// @license Public Domain |
|
// @match https://claude.ai/* |
|
// @match https://chat.openai.com/* |
|
// @match https://chatgpt.com/* |
|
// @match https://gemini.google.com/* |
|
// @match https://www.google.com/search* |
|
// @grant none |
|
// @run-at document-start |
|
// ==/UserScript== |
|
|
|
(function () { |
|
'use strict'; |
|
|
|
const INPUT_SELECTORS = { |
|
claude: '.ProseMirror[contenteditable="true"], div[data-placeholder][contenteditable="true"]', |
|
chatgpt: '#prompt-textarea, textarea[data-id="root"], div[contenteditable="true"][data-id]', |
|
gemini: '.ql-editor[contenteditable="true"], div[contenteditable="true"][aria-label*="prompt"], rich-textarea [contenteditable="true"]', |
|
googleai: 'textarea[aria-label="Ask anything"], textarea.ITIRGe' |
|
}; |
|
|
|
const SUBMIT_SELECTORS = { |
|
claude: 'button[data-testid="send-button"], button[aria-label*="Send message" i], button[aria-label="Send" i]', |
|
chatgpt: 'button[data-testid="send-button"], button[aria-label*="Send"], form button[type="submit"]', |
|
gemini: 'button[aria-label*="Send"], button.send-button, button[mattooltip*="Send"]', |
|
googleai: 'button[aria-label*="Send" i], button[aria-label*="Submit" i], button[type="submit"]' |
|
}; |
|
|
|
function getCurrentSite() { |
|
const host = location.hostname; |
|
if (host.includes('claude.ai')) return 'claude'; |
|
if (host.includes('openai.com') || host.includes('chatgpt.com')) return 'chatgpt'; |
|
if (host.includes('gemini.google.com')) return 'gemini'; |
|
if (host.includes('google.com') && (location.search.includes('udm=50') || document.querySelector('textarea[aria-label="Ask anything"]'))) { |
|
return 'googleai'; |
|
} |
|
return null; |
|
} |
|
|
|
function getInputElement(element, site) { |
|
if (!element) return null; |
|
const selector = INPUT_SELECTORS[site]; |
|
if (!selector) return null; |
|
if (element.matches(selector)) return element; |
|
return element.closest(selector); |
|
} |
|
|
|
function findSubmitButton(site, inputEl) { |
|
const selector = SUBMIT_SELECTORS[site]; |
|
if (!selector) return null; |
|
|
|
// Try direct selector match |
|
const buttons = document.querySelectorAll(selector); |
|
for (const btn of buttons) { |
|
if (!btn.disabled && btn.offsetParent !== null) { |
|
return btn; |
|
} |
|
} |
|
|
|
// Fallback: walk up from input to find a suitable button |
|
if ((site === 'claude' || site === 'googleai') && inputEl) { |
|
let container = inputEl.parentElement; |
|
for (let i = 0; i < 10 && container; i++) { |
|
const candidates = Array.from(container.querySelectorAll('button')).filter(btn => { |
|
if (btn.disabled || btn.offsetParent === null) return false; |
|
const label = (btn.getAttribute('aria-label') || '').toLowerCase(); |
|
if (label.includes('menu') || label.includes('attachment') || label.includes('voice') || label.includes('microphone')) return false; |
|
if (site === 'claude' && !btn.querySelector('svg')) return false; |
|
return true; |
|
}); |
|
if (candidates.length > 0) { |
|
return candidates[candidates.length - 1]; |
|
} |
|
container = container.parentElement; |
|
} |
|
} |
|
|
|
return buttons[0] || null; |
|
} |
|
|
|
function triggerSubmit(site, inputEl) { |
|
let submitBtn = findSubmitButton(site, inputEl); |
|
|
|
if (submitBtn) { |
|
if (submitBtn.tagName.toLowerCase() !== 'button') { |
|
submitBtn = submitBtn.closest('button'); |
|
} |
|
if (submitBtn) { |
|
submitBtn.click(); |
|
return true; |
|
} |
|
} |
|
|
|
// Fallback: dispatch Enter key event for sites that submit via Enter |
|
if (inputEl) { |
|
inputEl.dispatchEvent(new KeyboardEvent('keydown', { |
|
key: 'Enter', |
|
code: 'Enter', |
|
keyCode: 13, |
|
which: 13, |
|
bubbles: true, |
|
cancelable: true |
|
})); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
function handleKeyDown(event) { |
|
const site = getCurrentSite(); |
|
if (!site) return; |
|
|
|
const inputEl = getInputElement(event.target, site); |
|
if (!inputEl) return; |
|
|
|
if (event.key !== 'Enter') return; |
|
|
|
const hasModifier = event.metaKey || event.ctrlKey; |
|
const hasShift = event.shiftKey; |
|
|
|
if (hasModifier && !hasShift) { |
|
// Cmd+Enter or Ctrl+Enter: Submit |
|
event.preventDefault(); |
|
event.stopPropagation(); |
|
event.stopImmediatePropagation(); |
|
setTimeout(() => triggerSubmit(site, inputEl), 10); |
|
} else if (!hasModifier && !hasShift) { |
|
// Plain Enter: Transform into Shift+Enter to insert newline |
|
Object.defineProperty(event, 'shiftKey', { get: () => true }); |
|
} |
|
// Shift+Enter: Default behavior (newline) |
|
} |
|
|
|
document.addEventListener('keydown', handleKeyDown, true); |
|
})(); |