Skip to content

Instantly share code, notes, and snippets.

@l-portet
Created August 5, 2026 09:36
Show Gist options
  • Select an option

  • Save l-portet/c86424f0f1cb30658de973ae20b606db to your computer and use it in GitHub Desktop.

Select an option

Save l-portet/c86424f0f1cb30658de973ae20b606db to your computer and use it in GitHub Desktop.
// ==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