Created
August 5, 2026 09:36
-
-
Save l-portet/c86424f0f1cb30658de973ae20b606db to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name ChatGPT Shorthands | |
| // @namespace local.chatgpt.shorthands | |
| // @version 1.2.0 | |
| // @description Adds prompt chips and keyboard shortcuts to new ChatGPT conversations. | |
| // @match https://chatgpt.com/* | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (() => { | |
| "use strict"; | |
| const CHIP_CONTAINER_ID = "chatgpt-shorthands"; | |
| const STYLE_ID = "chatgpt-shorthands-styles"; | |
| /* | |
| * Customize your templates here. | |
| * | |
| * key: keyboard shortcut used when no input is focused | |
| * title: text displayed on the chip | |
| * prompt: text inserted into the ChatGPT composer | |
| */ | |
| const PROMPTS = [ | |
| { | |
| key: "v", | |
| title: "Viral tweet", | |
| prompt: "Help me write a viral tweet\n\n--\n\n", | |
| }, | |
| { | |
| key: "g", | |
| title: "Writing good?", | |
| prompt: "Is this writing good?\n\n--\n\n", | |
| }, | |
| { | |
| key: "t", | |
| title: "Translate to French", | |
| prompt: "Translate this to French\n\n--\n\n", | |
| }, | |
| ]; | |
| function addStyles() { | |
| if (document.getElementById(STYLE_ID)) { | |
| return; | |
| } | |
| const style = document.createElement("style"); | |
| style.id = STYLE_ID; | |
| style.textContent = ` | |
| #${CHIP_CONTAINER_ID} { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| width: 100%; | |
| margin-top: 10px; | |
| padding: 0 4px; | |
| box-sizing: border-box; | |
| } | |
| #${CHIP_CONTAINER_ID} .chatgpt-shorthand-chip { | |
| appearance: none; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| border: 1px solid var(--border-light, rgba(0, 0, 0, 0.14)); | |
| border-radius: 9999px; | |
| background: var(--main-surface-secondary, rgba(0, 0, 0, 0.04)); | |
| color: var(--text-primary, inherit); | |
| padding: 7px 11px; | |
| font: inherit; | |
| font-size: 13px; | |
| line-height: 18px; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| transition: | |
| background-color 120ms ease, | |
| border-color 120ms ease, | |
| transform 120ms ease; | |
| } | |
| #${CHIP_CONTAINER_ID} .chatgpt-shorthand-chip:hover { | |
| background: var(--main-surface-tertiary, rgba(0, 0, 0, 0.08)); | |
| border-color: var(--border-medium, rgba(0, 0, 0, 0.24)); | |
| } | |
| #${CHIP_CONTAINER_ID} .chatgpt-shorthand-chip:active { | |
| transform: scale(0.97); | |
| } | |
| #${CHIP_CONTAINER_ID} .chatgpt-shorthand-chip:focus-visible { | |
| outline: 2px solid currentColor; | |
| outline-offset: 2px; | |
| } | |
| #${CHIP_CONTAINER_ID} .chatgpt-shorthand-key { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 20px; | |
| height: 20px; | |
| padding: 0 5px; | |
| border: 1px solid var(--border-light, rgba(0, 0, 0, 0.16)); | |
| border-radius: 6px; | |
| background: var(--main-surface-primary, rgba(255, 255, 255, 0.5)); | |
| color: var(--text-secondary, inherit); | |
| font-family: | |
| ui-monospace, | |
| SFMono-Regular, | |
| Menlo, | |
| Monaco, | |
| Consolas, | |
| "Liberation Mono", | |
| monospace; | |
| font-size: 11px; | |
| line-height: 1; | |
| text-transform: uppercase; | |
| box-sizing: border-box; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function getComposerForm() { | |
| return ( | |
| document.querySelector('form[data-type="unified-composer"]') || | |
| document.querySelector("form.group\\/composer") | |
| ); | |
| } | |
| function getPromptEditor(form = document) { | |
| return ( | |
| form.querySelector( | |
| '[contenteditable="true"][role="textbox"][aria-label="Chat with ChatGPT"]' | |
| ) || | |
| form.querySelector( | |
| '[contenteditable="true"]#prompt-textarea' | |
| ) || | |
| form.querySelector( | |
| '[contenteditable="true"][role="textbox"]' | |
| ) | |
| ); | |
| } | |
| function hasConversationStarted() { | |
| return Boolean( | |
| document.querySelector( | |
| [ | |
| '[data-message-author-role="user"]', | |
| '[data-message-author-role="assistant"]', | |
| 'article[data-testid^="conversation-turn-"]', | |
| ].join(",") | |
| ) | |
| ); | |
| } | |
| function shouldShowChips() { | |
| return !hasConversationStarted(); | |
| } | |
| function textToFragment(text) { | |
| const fragment = document.createDocumentFragment(); | |
| const lines = text.split("\n"); | |
| lines.forEach((line) => { | |
| const paragraph = document.createElement("p"); | |
| if (line.length > 0) { | |
| paragraph.textContent = line; | |
| } else { | |
| paragraph.appendChild(document.createElement("br")); | |
| } | |
| fragment.appendChild(paragraph); | |
| }); | |
| return fragment; | |
| } | |
| function placeCaretAtEnd(editor) { | |
| const selection = window.getSelection(); | |
| const range = document.createRange(); | |
| range.selectNodeContents(editor); | |
| range.collapse(false); | |
| selection.removeAllRanges(); | |
| selection.addRange(range); | |
| } | |
| function dispatchEditorEvents(editor, prompt) { | |
| try { | |
| editor.dispatchEvent( | |
| new InputEvent("input", { | |
| bubbles: true, | |
| inputType: "insertText", | |
| data: prompt, | |
| }) | |
| ); | |
| } catch { | |
| editor.dispatchEvent( | |
| new Event("input", { | |
| bubbles: true, | |
| }) | |
| ); | |
| } | |
| editor.dispatchEvent( | |
| new Event("change", { | |
| bubbles: true, | |
| }) | |
| ); | |
| } | |
| function insertPrompt(prompt) { | |
| const form = getComposerForm(); | |
| const editor = form && getPromptEditor(form); | |
| if (!editor) { | |
| console.warn("ChatGPT Shorthands: prompt editor not found."); | |
| return; | |
| } | |
| editor.focus(); | |
| editor.replaceChildren(textToFragment(prompt)); | |
| placeCaretAtEnd(editor); | |
| dispatchEditorEvents(editor, prompt); | |
| } | |
| function createChip(template) { | |
| const button = document.createElement("button"); | |
| const title = document.createElement("span"); | |
| const key = document.createElement("span"); | |
| button.type = "button"; | |
| button.className = "chatgpt-shorthand-chip"; | |
| button.title = `${template.title} (${template.key.toUpperCase()})`; | |
| title.textContent = template.title; | |
| key.className = "chatgpt-shorthand-key"; | |
| key.textContent = template.key; | |
| button.append(title, key); | |
| button.addEventListener("click", () => { | |
| insertPrompt(template.prompt); | |
| }); | |
| return button; | |
| } | |
| function removeChips() { | |
| document.getElementById(CHIP_CONTAINER_ID)?.remove(); | |
| } | |
| function mountChips() { | |
| const form = getComposerForm(); | |
| const existingContainer = | |
| document.getElementById(CHIP_CONTAINER_ID); | |
| if (!form || !shouldShowChips()) { | |
| existingContainer?.remove(); | |
| return; | |
| } | |
| if (existingContainer) { | |
| if (existingContainer.previousElementSibling === form) { | |
| return; | |
| } | |
| existingContainer.remove(); | |
| } | |
| const container = document.createElement("div"); | |
| container.id = CHIP_CONTAINER_ID; | |
| container.setAttribute( | |
| "aria-label", | |
| "Saved prompt templates" | |
| ); | |
| PROMPTS.forEach((template) => { | |
| container.appendChild(createChip(template)); | |
| }); | |
| form.insertAdjacentElement("afterend", container); | |
| } | |
| function isEditableElement(element) { | |
| if (!(element instanceof Element)) { | |
| return false; | |
| } | |
| return Boolean( | |
| element.closest( | |
| [ | |
| "input", | |
| "textarea", | |
| "select", | |
| '[contenteditable="true"]', | |
| '[contenteditable="plaintext-only"]', | |
| '[role="textbox"]', | |
| ].join(",") | |
| ) | |
| ); | |
| } | |
| function isOverlayOpen() { | |
| return Boolean( | |
| document.querySelector( | |
| [ | |
| '[role="dialog"]', | |
| '[role="menu"]', | |
| '[role="listbox"]', | |
| '[data-radix-menu-content]', | |
| '[data-state="open"][role="listbox"]', | |
| ].join(",") | |
| ) | |
| ); | |
| } | |
| function handleKeyboardShortcut(event) { | |
| if (event.defaultPrevented || event.repeat) { | |
| return; | |
| } | |
| if ( | |
| event.ctrlKey || | |
| event.metaKey || | |
| event.altKey | |
| ) { | |
| return; | |
| } | |
| if (isEditableElement(event.target)) { | |
| return; | |
| } | |
| if (isOverlayOpen()) { | |
| return; | |
| } | |
| const pressedKey = event.key.toLowerCase(); | |
| const template = PROMPTS.find( | |
| ({ key }) => key.toLowerCase() === pressedKey | |
| ); | |
| if (!template) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| insertPrompt(template.prompt); | |
| } | |
| function refresh() { | |
| if (hasConversationStarted()) { | |
| removeChips(); | |
| return; | |
| } | |
| mountChips(); | |
| } | |
| function initialize() { | |
| addStyles(); | |
| refresh(); | |
| document.addEventListener( | |
| "keydown", | |
| handleKeyboardShortcut, | |
| true | |
| ); | |
| const observer = new MutationObserver(() => { | |
| refresh(); | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true, | |
| }); | |
| } | |
| initialize(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment