Where you ask: send, stop, queue, dictate, and keep the draft.
Every shadcn style · one file
v2.0 · Updated Oct 13pnpm dlx shadcn@latest add https://notchset.dev/r/prompt-input.jsonpnpm dlx shadcn@latest add https://notchset.dev/r/prompt-input.jsonFree, no account or key. One command for every shadcn style. With the @notchset registry added, @notchset/<name> works too.
Works with React 18.3 or 19 on Tailwind CSS v4, in any shadcn project. What's tested
pnpm add lucide-react@^1The CLI adds the styles to the CSS file named in components.json (tailwind.css). Check that it points at your global stylesheet, that the project uses Tailwind CSS v4, and that the file is imported in your root layout. Installed by hand: paste styles.css into that file.
Import it and pass your content. The props that matter most are listed next.
"use client";
import * as React from "react";
import { PromptInput, type Dictation } from "@/components/ui/notchset/prompt-input";
/* the sample "hears" these words; the template's voice adapter listens for real */
const HEARD = ["Draft", "a short", "note to", "the beta list", "about Tuesday"];
export default function PromptInputDemo() {
const [value, setValue] = React.useState("");
const [sent, setSent] = React.useState<string[]>([]);
const [running, setRunning] = React.useState(false);
const [queued, setQueued] = React.useState<string | null>(null);
const [dict, setDict] = React.useState<{ on: boolean; heard: number }>({ on: false, heard: 0 });
/* a sample reply runs for three seconds, then a waiting message goes */
React.useEffect(() => {
if (!running) return;
const t = setTimeout(() => {
setRunning(false);
if (queued) {
setSent((s) => [...s, queued]);
setQueued(null);
setRunning(true);
}
}, 3000);
return () => clearTimeout(t);
}, [running, queued]);
React.useEffect(() => {
if (!dict.on) return;
const t = setTimeout(() => {
if (dict.heard < HEARD.length) return setDict({ on: true, heard: dict.heard + 1 });
/* the phrase ends: the words land in the box, nothing is sent */
setValue((v) => `${v}${v ? " " : ""}${HEARD.join(" ")}.`);
setDict({ on: false, heard: 0 });
}, 420);
return () => clearTimeout(t);
}, [dict]);
const dictation: Dictation = { state: dict.on ? "listening" : "off", interim: dict.heard ? HEARD.slice(0, dict.heard).join(" ") : undefined, onToggle: () => setDict((d) => ({ on: !d.on, heard: 0 })) };
return (
<div className="flex max-w-[640px] flex-col gap-3">
{sent.length > 0 && (
<ul className="m-0 flex list-none flex-col gap-1 p-0 text-[length:var(--ns-text-small)] text-[var(--ns-ink-2)]">
{sent.map((s, i) => (
<li key={i}>Sent: {s}</li>
))}
</ul>
)}
<PromptInput
value={value}
onValueChange={setValue}
onSubmit={(t) => {
if (running) setQueued(t);
else {
setSent((s) => [...s, t]);
setRunning(true);
}
setValue("");
}}
running={running}
onStop={() => setRunning(false)}
queued={queued ? { text: queued, onSendNow: () => (setRunning(false), setSent((s) => [...s, queued]), setQueued(null), setRunning(true)), onCancel: () => (setValue(queued), setQueued(null)) } : undefined}
dictation={dictation}
attach={{ onFiles: () => {} }}
placeholder="Ask anything"
/>
</div>
);
}components/ui/notchset/prompt-input.tsx
value–
stringRequired.
onValueChange–
(value: string) => voidRequired.
onSubmit–
(text: string) => voidsend (or, while a reply runs, queue) the text
runningfalse
booleana reply is running: the button is Stop
onStop–
() => voidqueued–
{ text: string; onSendNow: () => void; onCancel: () => void }a message waiting for the reply to finish
dictation–
Dictationattach–
{ onFiles: (files: File[]) => void; refused?: string | undefined; count?: number | undefined; busy?: boolean | undefined; /** the file picker's types ("image/*,application/pdf") */ accept?: string | undefined }add files (the paperclip, paste, drop); with `refused` the button stays and says why it can't ("This model can't read files"). `count`: files ready, so Send works without words; `busy`: one is still reading, so Send waits.
attachments–
React.ReactNodethe attached files' chips, above the words
tools–
React.ReactNodea tools menu, after the paperclip (Search the web, Deep research)
limit–
{ line: string; onSendWhenReset: () => void }at the plan's limit: what it says and the way on
offlinefalse
booleanthe connection is gone: the draft stays and says it will wait
onEditLast–
() => void↑ in an empty box edits your last message
model–
React.ReactNodethe model selector
voice–
React.ReactNodethe voice button
footer–
React.ReactNodethe usage line, under the box
placeholder"Ask anything"
stringlabel"Message"
stringGive your coding assistant the Markdown guide instead of screenshots.
https://notchset.dev/api/mcp (setup)v2.0.0 · 2026-10-13
Released with the Notchset system.
The same file, at work in a finished app. Open it live and find it on its pages.
Agent UI. An assistant people trust: ask, pick a model, watch the answer stream in with its receipt, and approve what it does.
4 free · 6 ProSee the Agent UI kit