<main class="page">
<section class="workspace" aria-labelledby="workspace-title">
<h1 class="sr-only" id="workspace-title">AI Prompt Composer</h1>
<div class="log" id="log" role="log" aria-label="Sent prompts" tabindex="0" hidden></div>
<form class="composer" id="composer">
<div class="menu-wrap" id="menu-wrap" hidden>
<div class="menu-head">
<span class="menu-title" id="menu-title">Commands</span>
<span class="menu-kbd">Up / Down · Enter</span>
</div>
<ul class="menu" id="menu" role="listbox" aria-labelledby="menu-title"></ul>
</div>
<ul class="chips" id="chips" aria-label="Attached context"></ul>
<label class="sr-only" for="prompt">Prompt</label>
<textarea id="prompt" rows="2" aria-autocomplete="list" aria-controls="menu" placeholder="Ask anything, type / for commands or @ to attach a file"></textarea>
<div class="composer-bar">
<button type="button" class="trigger-btn" id="btn-slash" aria-label="Insert slash command" aria-expanded="false" aria-controls="menu">
<span class="trigger-key">/</span>
<span>Commands</span>
</button>
<button type="button" class="trigger-btn" id="btn-mention" aria-label="Mention a file" aria-expanded="false" aria-controls="menu">
<span class="trigger-key">@</span>
<span>Files</span>
</button>
<span class="hint">Enter to send</span>
<button type="submit" class="send-btn" id="send" aria-label="Send prompt">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>
</button>
</div>
</form>
<p class="sr-only" id="status" role="status"></p>
</section>
</main>*, *::before, *::after {
box-sizing: border-box;
}
:root {
--page: #f1f5f9;
--card: #ffffff;
--text: #0f172a;
--muted: #64748b;
--soft: #f1f5f9;
--hover: #e2e8f0;
--accent: #2563eb;
--accent-soft: #eff6ff;
--accent-text: #1d4ed8;
--ring: rgba(15, 23, 42, 0.06);
--shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 16px 36px -12px rgba(15, 23, 42, 0.14);
--menu-shadow: 0 2px 6px rgba(15, 23, 42, 0.05), 0 24px 48px -12px rgba(15, 23, 42, 0.18);
}
body {
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
background: var(--page);
color: var(--text);
}
.page {
display: grid;
grid-template-columns: minmax(0, 580px);
justify-content: center;
align-content: end;
min-height: 100vh;
padding: 20px 16px 36px;
}
.workspace {
display: flex;
flex-direction: column;
gap: 12px;
}
.log {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 110px;
padding: 4px 2px;
overflow-y: auto;
}
.log[hidden] {
display: none;
}
.log:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 10px;
}
.sent {
align-self: flex-end;
max-width: 88%;
margin: 0;
padding: 10px 14px;
border-radius: 18px 18px 4px 18px;
background: var(--card);
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
font-size: 0.875rem;
line-height: 1.45;
overflow-wrap: anywhere;
}
.sent-files {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 4px;
}
.sent-file {
padding: 2px 8px;
border-radius: 6px;
background: var(--accent-soft);
color: var(--accent-text);
font-family: ui-monospace, Consolas, monospace;
font-size: 0.75rem;
font-weight: 600;
}
.composer {
position: relative;
padding: 14px 14px 12px;
border-radius: 22px;
background: var(--card);
box-shadow: 0 0 0 1px var(--ring), var(--shadow);
transition: box-shadow 0.15s ease;
}
.composer:focus-within {
box-shadow: 0 0 0 2px var(--accent), var(--shadow);
}
.menu-wrap {
position: absolute;
right: 0;
bottom: calc(100% + 10px);
left: 0;
z-index: 10;
padding: 6px;
border-radius: 18px;
background: var(--card);
box-shadow: 0 0 0 1px var(--ring), var(--menu-shadow);
}
.menu-wrap[hidden] {
display: none;
}
.menu-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px 8px;
}
.menu-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
}
.menu-kbd {
font-family: ui-monospace, Consolas, monospace;
font-size: 0.6875rem;
color: var(--muted);
}
.menu {
max-height: 224px;
margin: 0;
padding: 0;
overflow-y: auto;
list-style: none;
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 9px 10px;
border-radius: 12px;
cursor: pointer;
}
.menu-item[aria-selected="true"] {
background: var(--accent-soft);
}
.item-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.item-ico {
display: grid;
place-items: center;
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 8px;
background: var(--soft);
color: var(--muted);
}
.item-ico svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.menu-item[aria-selected="true"] .item-ico {
background: var(--accent);
color: #ffffff;
}
.item-key {
font-family: ui-monospace, Consolas, monospace;
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
}
.menu-item[aria-selected="true"] .item-key {
color: var(--accent-text);
}
.item-meta {
overflow: hidden;
font-size: 0.8125rem;
color: var(--muted);
text-overflow: ellipsis;
white-space: nowrap;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0;
padding: 0 2px 10px;
list-style: none;
}
.chips:empty {
display: none;
}
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 4px 6px 4px 8px;
border-radius: 10px;
background: var(--soft);
font-size: 0.8125rem;
}
.chip-ico {
display: grid;
place-items: center;
color: var(--accent);
}
.chip-ico svg {
width: 15px;
height: 15px;
fill: none;
stroke: currentColor;
stroke-width: 1.9;
stroke-linecap: round;
stroke-linejoin: round;
}
.chip-name {
font-family: ui-monospace, Consolas, monospace;
font-weight: 600;
}
.chip-meta {
color: var(--muted);
font-size: 0.75rem;
}
.chip-remove {
display: grid;
place-items: center;
width: 22px;
height: 22px;
padding: 0;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
cursor: pointer;
}
.chip-remove svg {
width: 13px;
height: 13px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
}
.chip-remove:hover {
background: var(--hover);
color: var(--text);
}
textarea {
display: block;
width: 100%;
padding: 2px 4px 8px;
border: 0;
outline: 0;
resize: none;
background: transparent;
color: var(--text);
font: inherit;
font-size: 0.9375rem;
line-height: 1.5;
}
textarea::placeholder {
color: var(--muted);
}
.composer-bar {
display: flex;
align-items: center;
gap: 6px;
padding-top: 4px;
}
.trigger-btn {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 32px;
padding: 4px 11px 4px 8px;
border: 0;
border-radius: 999px;
background: var(--soft);
color: var(--text);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.trigger-btn:hover {
background: var(--hover);
}
.trigger-btn[aria-expanded="true"] {
background: var(--accent-soft);
color: var(--accent-text);
}
.trigger-key {
display: grid;
place-items: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--card);
color: var(--muted);
font-family: ui-monospace, Consolas, monospace;
font-size: 0.75rem;
font-weight: 700;
}
.trigger-btn[aria-expanded="true"] .trigger-key {
background: var(--accent);
color: #ffffff;
}
.hint {
margin-left: auto;
padding-right: 4px;
font-size: 0.75rem;
color: var(--muted);
}
.send-btn {
display: grid;
place-items: center;
flex-shrink: 0;
width: 34px;
height: 34px;
padding: 0;
border: 0;
border-radius: 50%;
background: var(--text);
color: var(--card);
cursor: pointer;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.send-btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.send-btn:disabled {
background: var(--soft);
color: var(--muted);
cursor: default;
}
.send-btn svg {
width: 17px;
height: 17px;
fill: none;
stroke: currentColor;
stroke-width: 2.1;
stroke-linecap: round;
stroke-linejoin: round;
}
.trigger-btn:focus-visible,
.send-btn:focus-visible,
.chip-remove:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@media (max-width: 480px) {
.hint {
display: none;
}
.send-btn {
margin-left: auto;
}
}
@media (pointer: coarse) {
.trigger-btn,
.menu-item,
textarea {
min-height: 44px;
}
.send-btn,
.chip-remove {
width: 44px;
height: 44px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--page: #0b1120;
--card: #151e30;
--text: #f1f5f9;
--muted: #94a3b8;
--soft: #1e293b;
--hover: #293548;
--accent: #3b82f6;
--accent-soft: #172554;
--accent-text: #93c5fd;
--ring: rgba(255, 255, 255, 0.08);
--shadow: 0 18px 40px -14px rgba(0, 0, 0, 0.6);
--menu-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.75);
}
}
@media (forced-colors: active) {
.composer,
.menu-wrap {
border: 1px solid CanvasText;
}
.menu-item[aria-selected="true"] {
outline: 2px solid Highlight;
}
}const COMMANDS = [
{ key: "/explain", desc: "Explain how the selected code works", icon: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3M12 17h.01M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0Z" },
{ key: "/summarize", desc: "Summarize key points and decisions", icon: "M4 6h16M4 12h16M4 18h10" },
{ key: "/refactor", desc: "Clean up structure without changing behavior", icon: "M16 18l6-6-6-6M8 6l-6 6 6 6" },
{ key: "/tests", desc: "Write unit tests for edge cases", icon: "M9 12l2 2 4-4M22 12a10 10 0 1 1-20 0 10 10 0 0 1 20 0Z" }
];
const FILES = [
{ name: "schema.sql", meta: "42 lines", icon: "M12 8c4.42 0 8-1.34 8-3s-3.58-3-8-3-8 1.34-8 3 3.58 3 8 3ZM4 5v14c0 1.66 3.58 3 8 3s8-1.34 8-3V5M4 12c0 1.66 3.58 3 8 3s8-1.34 8-3" },
{ name: "router.js", meta: "118 lines", icon: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6ZM14 2v6h6M10 13l-2 2 2 2M14 13l2 2-2 2" },
{ name: "auth.js", meta: "84 lines", icon: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" },
{ name: "README.md", meta: "64 lines", icon: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6ZM14 2v6h6M8 13h8M8 17h5" }
];
const composer = document.getElementById("composer");
const prompt = document.getElementById("prompt");
const menuWrap = document.getElementById("menu-wrap");
const menuTitle = document.getElementById("menu-title");
const menu = document.getElementById("menu");
const chips = document.getElementById("chips");
const log = document.getElementById("log");
const sendBtn = document.getElementById("send");
const slashBtn = document.getElementById("btn-slash");
const mentionBtn = document.getElementById("btn-mention");
const status = document.getElementById("status");
let attached = [];
let activeTrigger = null;
let matches = [];
let activeIndex = 0;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text) node.textContent = text;
return node;
}
function makeIcon(pathData) {
const ns = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(ns, "svg");
const path = document.createElementNS(ns, "path");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("aria-hidden", "true");
path.setAttribute("d", pathData);
svg.append(path);
return svg;
}
// Match a / or @ token right before the caret
function readTrigger() {
const caret = prompt.selectionStart;
const before = prompt.value.slice(0, caret);
const hit = before.match(/(?:^|\s)([/@])([^\s/@]*)$/);
if (!hit) return null;
const symbol = hit[1];
const query = hit[2].toLowerCase();
const start = caret - query.length - 1;
return { symbol: symbol, query: query, start: start, end: caret };
}
function closeMenu() {
activeTrigger = null;
matches = [];
activeIndex = 0;
menuWrap.hidden = true;
menu.textContent = "";
slashBtn.setAttribute("aria-expanded", "false");
mentionBtn.setAttribute("aria-expanded", "false");
prompt.removeAttribute("aria-activedescendant");
}
function syncActiveOption() {
const items = menu.querySelectorAll(".menu-item");
items.forEach(function(item, i) {
const selected = i === activeIndex;
item.setAttribute("aria-selected", String(selected));
if (selected) {
prompt.setAttribute("aria-activedescendant", item.id);
item.scrollIntoView({ block: "nearest" });
}
});
}
function renderMenu() {
menu.textContent = "";
const isCommand = activeTrigger.symbol === "/";
menuTitle.textContent = isCommand ? "Commands" : "Workspace files";
matches.forEach(function(item, i) {
const li = el("li", "menu-item");
const left = el("div", "item-left");
const ico = el("span", "item-ico");
ico.append(makeIcon(item.icon));
const key = el("span", "item-key", isCommand ? item.key : "@" + item.name);
const meta = el("span", "item-meta", isCommand ? item.desc : item.meta);
li.id = "menu-opt-" + i;
li.setAttribute("role", "option");
li.setAttribute("aria-selected", String(i === activeIndex));
left.append(ico, key);
li.append(left, meta);
li.addEventListener("mousedown", function(e) {
e.preventDefault();
pickItem(i);
});
menu.append(li);
});
menuWrap.hidden = false;
slashBtn.setAttribute("aria-expanded", String(isCommand));
mentionBtn.setAttribute("aria-expanded", String(!isCommand));
syncActiveOption();
status.textContent = matches.length + (matches.length === 1 ? " option" : " options") + " available";
}
function updateMenu() {
const trigger = readTrigger();
if (!trigger) {
closeMenu();
return;
}
const list = trigger.symbol === "/"
? COMMANDS.filter(function(c) { return c.key.slice(1).indexOf(trigger.query) === 0; })
: FILES.filter(function(f) { return f.name.toLowerCase().indexOf(trigger.query) !== -1; });
if (!list.length) {
closeMenu();
return;
}
activeTrigger = trigger;
matches = list;
if (activeIndex >= matches.length) activeIndex = 0;
renderMenu();
}
function renderChips() {
chips.textContent = "";
attached.forEach(function(file) {
const li = el("li", "chip");
const ico = el("span", "chip-ico");
ico.append(makeIcon(file.icon));
const name = el("span", "chip-name", file.name);
const meta = el("span", "chip-meta", file.meta);
const btn = el("button", "chip-remove");
btn.type = "button";
btn.setAttribute("aria-label", "Remove " + file.name);
btn.append(makeIcon("M18 6L6 18M6 6l12 12"));
btn.addEventListener("click", function() {
attached = attached.filter(function(f) { return f.name !== file.name; });
renderChips();
updateSend();
status.textContent = "Removed " + file.name;
prompt.focus();
});
li.append(ico, name, meta, btn);
chips.append(li);
});
}
function pickItem(index) {
const item = matches[index];
if (!item || !activeTrigger) return;
if (activeTrigger.symbol === "/") {
const replacement = item.key + " ";
prompt.setRangeText(replacement, activeTrigger.start, activeTrigger.end, "end");
status.textContent = "Inserted " + item.key;
} else {
prompt.setRangeText("", activeTrigger.start, activeTrigger.end, "end");
if (!attached.some(function(f) { return f.name === item.name; })) {
attached.push(item);
renderChips();
}
status.textContent = "Attached " + item.name;
}
closeMenu();
updateSend();
prompt.focus();
}
function updateSend() {
sendBtn.disabled = !prompt.value.trim() && !attached.length;
}
function insertTrigger(symbol) {
prompt.focus();
const pos = prompt.selectionStart;
const before = prompt.value.slice(0, pos);
const pad = before.length && !/\s$/.test(before) ? " " : "";
prompt.setRangeText(pad + symbol, pos, prompt.selectionEnd, "end");
updateMenu();
updateSend();
}
function send() {
const text = prompt.value.trim();
if (!text && !attached.length) return;
log.hidden = false;
const card = el("div", "sent");
if (attached.length) {
const filesRow = el("div", "sent-files");
attached.forEach(function(f) {
filesRow.append(el("span", "sent-file", "@" + f.name));
});
card.append(filesRow);
}
if (text) card.append(el("div", "", text));
log.append(card);
log.scrollTop = log.scrollHeight;
status.textContent = "Sent prompt";
prompt.value = "";
attached = [];
renderChips();
closeMenu();
updateSend();
}
prompt.addEventListener("keydown", function(e) {
if (e.isComposing || e.keyCode === 229) return;
if (!menuWrap.hidden) {
if (e.key === "ArrowDown") {
e.preventDefault();
activeIndex = (activeIndex + 1) % matches.length;
syncActiveOption();
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
activeIndex = (activeIndex - 1 + matches.length) % matches.length;
syncActiveOption();
return;
}
if (e.key === "Enter" || e.key === "Tab") {
e.preventDefault();
pickItem(activeIndex);
return;
}
if (e.key === "Escape") {
e.preventDefault();
closeMenu();
return;
}
}
if (e.key === "Backspace" && !prompt.value && attached.length) {
const removed = attached.pop();
renderChips();
updateSend();
status.textContent = "Removed " + removed.name;
return;
}
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
});
prompt.addEventListener("input", function() {
updateMenu();
updateSend();
});
prompt.addEventListener("click", updateMenu);
prompt.addEventListener("keyup", function(e) {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") updateMenu();
});
slashBtn.addEventListener("click", function() {
insertTrigger("/");
});
mentionBtn.addEventListener("click", function() {
insertTrigger("@");
});
composer.addEventListener("submit", function(e) {
e.preventDefault();
send();
});
document.addEventListener("pointerdown", function(e) {
if (!composer.contains(e.target)) closeMenu();
});
// Start with a file attached and the command menu open
attached = [FILES[0]];
renderChips();
prompt.value = "/";
prompt.setSelectionRange(1, 1);
updateMenu();
updateSend();Click any element to find its code Esc
Check this code before you run it
It came from a shared link. It runs with fewer permissions, so it cannot open pop-ups, submit forms or start downloads.
element.animate() call shows up here as it plays.