Prompt editor
The Tiptap editor for long prompts: variables, a slash menu, AI rewrites with inline diffs and change tracking.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
import { Button } from "@oration/canon/components/button";import { PromptEditor, type PromptVariable } from "@oration/canon/components/prompt-editor/prompt-editor";import { toast } from "@oration/canon/components/toast";import { PhoneCallIcon } from "lucide-react";import * as React from "react";export function Hero() { const [variables, setVariables] = React.useState<PromptVariable[]>([ { name: "supplier_name", description: "The supplier's name from the vendor record.", example: "Northwind Freight", }, { name: "invoice_number", description: "The invoice the caller is asking about.", example: "INV-20931", }, { name: "payment_date", description: "When that invoice is scheduled to be paid.", example: "Friday, October 2", }, { name: "agent_name", kind: "system", description: "The name the agent introduces itself with.", example: "Nora", }, { name: "caller_number", kind: "system", description: "The caller's phone number in E.164.", example: "+13125550148", }, ]); const baseline = [ "# Who you are", "You are {{agent_name}}, the supplier support agent for Cedarline's accounts payable team. Suppliers call you about invoices, payments and remittances.", "", "# How you sound", "Friendly and clear. Keep answers short.", "", "# What you do", "1. Greet {{supplier_name}} by name.", "2. Ask for the invoice number and read it back: {{invoice_number}}.", "3. Look up the payment and tell them it's scheduled for {{payment_date}}.", "", "# Guardrails", "- Never read out full bank account or routing numbers.", ].join("\n"); const [value, setValue] = React.useState( [ "# Who you are", "You are {{agent_name}}, the supplier support agent for Cedarline's accounts payable team. Suppliers call you about invoices, payments and remittances.", "", "# How you sound", "Warm, unhurried and exact. Read amounts and dates back slowly, and never guess.", "", "# What you do", "1. Greet {{supplier_name}} by name and confirm you're speaking with their accounts receivable contact.", "2. Ask for the invoice number and read it back: {{invoice_number}}.", "3. Look up the payment with `get_payment_status` and tell them it's scheduled for {{payment_date}}.", "4. If they ask for remittance details, offer to email them to the address on file.", "", "# Guardrails", "- Never read out full bank account or routing numbers.", "- Don't promise a payment date that isn't in the system.", "- If the caller disputes an amount, take a note and transfer them to Priya Raman's team.", ].join("\n"), ); return ( <PromptEditor value={value} onChange={setValue} baseline={baseline} baselineLabel="published version 4" variables={variables} onCreateVariable={(name) => { setVariables((current) => [...current, { name }]); toast.add({ title: `Added {{${name}}}`, description: "Describe it so the model knows how to use the value.", }); }} toolbarActions={ <Button type="button" variant="ghost" size="sm" onClick={() => toast.add({ title: "Calling you as Nora", description: "Using the draft prompt, not published version 4.", }) } > <PhoneCallIcon data-icon="inline-start" aria-hidden="true" /> Test call </Button> } maxHeight="26rem" aria-label="Nora prompt" className="w-full" /> );}Usage#
Prompt editor is where people write the long instructions a voice agent follows, such as the system prompt for Nora, Cedarline's supplier support agent. It is a Tiptap editor that stores plain text with light Markdown: {{variables}} become chips, / opens blocks and AI actions, selecting text offers AI rewrites as inline diffs, and the footer counts changes against the published version. The thing people get wrong is hard-coding per-call facts like a supplier's name into the prose; anything that changes per call belongs in a variable that is defined and described.
When to use
- For an agent's system prompt, a procedure's instructions or a skill's body: long text the model reads.
- When the text references call-time values such as
{{supplier_name}},{{invoice_number}}or{{payment_date}}. - When edits are drafted against a published version and people need to see what changed before publishing.
- When AI help (rewrite, shorten, add guardrails, translate) should arrive as a reviewable suggestion, not a silent overwrite.
- With
toolbar={false}for a shorter templated message, like the first line the agent says.
When not to use
- For a short single-line value, such as a variable's example. Use Input
- For a plain note or description without variables or formatting. Use Textarea
- For a chat message someone sends to an assistant. Use Prompt bar
- For showing code or a JSON payload that people read but don't write. Use Code block
- For comparing two finished versions side by side. Use Diff
Suggestions, not overwrites
The Label-Beside-Color Rule
The Tint Well Rule
bg-warning/10), not a bordered alert box.Anatomy#
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
- Frame. A card surface with
rounded-xlandshadow-border. While the text has focus it gains a 3px Focus Indigo ring at 20%. - Text style menu. Text or Heading 1 to 3, in a 108px ghost trigger at the start of the toolbar.
- Formatting tools. 28px ghost icon buttons with tooltips and shortcuts: bold, italic, inline code, lists, quote, code block, undo and redo. Pressed tools fill with
--accent. - Variable button. Inserts
{{at the caret, which opens the variable menu. - Toolbar actions.
toolbarActions, after a divider at the end of the toolbar. - Writing area. 14px text on 24px lines (16px on small screens), 16px by 12px padding. Grows from
minHeightand scrolls pastmaxHeight. - Variable chip. An inline atom that serializes to
{{name}}. Gray for custom, teal for system, a warning tint for undefined names. - Change status. A 6px dot (indigo when changed) beside how many lines and words moved from
baseline. Hidden without a baseline. - View changes. Opens a line diff against the baseline. Only shown when something changed.
- Word and token count. Live word count and a rough token estimate, at the end of the footer.
Examples#
With the variables panel
extractVariables(value) feeds the panel's usage counts and its warning for names nobody defined. The panel inserts at the caret through the editor's ref.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Dynamic variables
1 variable is used but not defined. It will be blank on calls.
- remittance_email (undefined variable)
- supplier_nameUsed once
The supplier's name from the vendor record.
Example: Northwind Freight
- invoice_numberUsed once
The invoice the caller is asking about.
Example: INV-20931
- payment_dateUsed once
When that invoice is scheduled to be paid.
Example: Friday, October 2
System
- agent_name (system variable)Not used
The name the agent introduces itself with.
Example: Nora
import { extractVariables, PromptEditor, type PromptEditorHandle, type PromptVariable, VariablesPanel,} from "@oration/canon/components/prompt-editor/prompt-editor";import { toast } from "@oration/canon/components/toast";import * as React from "react";export function WithVariablesPanel() { const editorRef = React.useRef<PromptEditorHandle>(null); const [variables, setVariables] = React.useState<PromptVariable[]>([ { name: "supplier_name", description: "The supplier's name from the vendor record.", example: "Northwind Freight", }, { name: "invoice_number", description: "The invoice the caller is asking about.", example: "INV-20931", }, { name: "payment_date", description: "When that invoice is scheduled to be paid.", example: "Friday, October 2", }, { name: "agent_name", kind: "system", description: "The name the agent introduces itself with.", example: "Nora", }, ]); const [value, setValue] = React.useState( [ "Greet {{supplier_name}} and confirm the invoice number, {{invoice_number}}.", "", "Tell them it's scheduled for {{payment_date}}. If they ask for remittance details, offer to email them to {{remittance_email}}.", ].join("\n"), ); return ( <div className="grid w-full gap-4 md:grid-cols-[minmax(0,1fr)_16rem]"> <PromptEditor ref={editorRef} value={value} onChange={setValue} variables={variables} onCreateVariable={(name) => setVariables((current) => [...current, { name }]) } minHeight={200} aria-label="Nora prompt" /> <div className="self-start rounded-xl bg-card p-3 shadow-border"> <VariablesPanel title="Dynamic variables" variables={variables} usedNames={extractVariables(value)} onInsert={(name) => editorRef.current?.insertVariable(name)} onCreate={(name) => { if (!name) { editorRef.current?.openVariableMenu(); return; } setVariables((current) => [ ...current, { name, description: "Where to send remittance advice.", example: "ar@northwindfreight.com", }, ]); toast.add({ type: "success", title: `Defined {{${name}}}`, description: "It will be filled in on every call.", }); }} onEdit={(variable) => toast.add({ title: `Edit {{${variable.name}}}`, description: variable.description, }) } /> </div> </div> );}AI actions
onAIAction gets the action, the selection and the whole prompt, and resolves with replacement text. The answer streams in as a suggestion people accept or reject.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Select the sentence under How you sound and choose Rewrite, or type / on the empty last line and choose Add guardrails.
import { PromptEditor } from "@oration/canon/components/prompt-editor/prompt-editor";import * as React from "react";export function AIActions() { const [value, setValue] = React.useState( [ "# How you sound", "Be nice to callers and try to help them with whatever they need about their payments.", "", "# What you do", "Look up the invoice and tell them when it gets paid.", "", ].join("\n"), ); return ( <div className="flex w-full flex-col gap-2"> <PromptEditor value={value} onChange={setValue} variables={[]} minHeight={200} aria-label="Nora prompt" onAIAction={async (request) => { // Call your model here. The editor streams the answer in for review. await new Promise((resolve) => window.setTimeout(resolve, 500), ); if (request.kind === "guardrails") return [ "## Guardrails", "- Never read out full bank account or routing numbers.", "- If the caller disputes an amount, transfer them to Priya Raman's team.", ].join("\n"); if (request.selection?.includes("Be nice")) return "Stay warm, unhurried and exact with every caller. Read amounts and dates back slowly, and never guess."; return request.selection ?? request.document; }} /> <p className="text-13 text-pretty text-muted-foreground"> Select the sentence under How you sound and choose Rewrite, or type / on the empty last line and choose Add guardrails. </p> </div> );}Without the toolbar
toolbar={false} and a small minHeight for a short templated message. Name it with aria-labelledby pointing at the visible label.
First message
What Nora says when the supplier picks up.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
import { PromptEditor } from "@oration/canon/components/prompt-editor/prompt-editor";import * as React from "react";export function FirstMessage() { const labelId = React.useId(); const [value, setValue] = React.useState( "Hi, this is {{agent_name}} from Cedarline supplier support. Am I speaking with someone from {{supplier_name}}?", ); return ( <div className="flex w-full max-w-xl flex-col gap-2"> <div className="flex flex-col gap-0.5"> <p id={labelId} className="text-13 font-medium text-foreground"> First message </p> <p className="text-13 text-muted-foreground"> What Nora says when the supplier picks up. </p> </div> <PromptEditor value={value} onChange={setValue} variables={[ { name: "supplier_name", description: "The supplier's name from the vendor record.", example: "Northwind Freight", }, { name: "agent_name", kind: "system", description: "The name the agent introduces itself with.", example: "Nora", }, ]} toolbar={false} minHeight={72} placeholder="What should the agent say first?" aria-labelledby={labelId} /> </div> );}Plain text in and out
The value is the text the model reads: light Markdown with {{name}} for each chip. Edit on the left and watch the stored value.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Confirm **{{invoice_number}}** with {{supplier_name}}, then say it's paid on {{payment_date}}.
- Never read out bank details.
- Repeat {{invoice_number}} back before you look it up.import { CodeBlock } from "@oration/canon/components/code-block";import { extractVariables, PromptEditor } from "@oration/canon/components/prompt-editor/prompt-editor";import { VariableChip } from "@oration/canon/components/variable-chip";import * as React from "react";export function PlainText() { const [value, setValue] = React.useState( [ "Confirm **{{invoice_number}}** with {{supplier_name}}, then say it's paid on {{payment_date}}.", "- Never read out bank details.", "- Repeat {{invoice_number}} back before you look it up.", ].join("\n"), ); const names = [...new Set(extractVariables(value))]; return ( <div className="grid w-full gap-4 md:grid-cols-2"> <PromptEditor value={value} onChange={setValue} variables={[ { name: "supplier_name", example: "Northwind Freight" }, { name: "invoice_number", example: "INV-20931" }, { name: "payment_date", example: "Friday, October 2" }, ]} toolbar={false} minHeight={120} aria-label="Payment status instructions" /> <div className="flex min-w-0 flex-col gap-3"> <CodeBlock code={value} title="value" /> <div className="flex flex-wrap items-center gap-1.5"> <span className="font-mono text-xs text-muted-foreground"> extractVariables(value) </span> {names.map((name) => ( <VariableChip key={name} name={name} size="sm" /> ))} </div> </div> </div> );}Loading
PromptEditorSkeleton holds the editor's frame, toolbar and footer while the prompt loads. Match its minHeight to the editor's.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
import { Button } from "@oration/canon/components/button";import { PromptEditor, PromptEditorSkeleton } from "@oration/canon/components/prompt-editor/prompt-editor";import { SkeletonReveal } from "@oration/canon/components/skeleton-reveal";import { RotateCcwIcon } from "lucide-react";import * as React from "react";export function Loading() { const [loading, setLoading] = React.useState(false); const [value, setValue] = React.useState( "# Guardrails\n- Never read out full bank account or routing numbers.\n- Don't promise a payment date that isn't in the system.", ); React.useEffect(() => { if (!loading) return; const id = window.setTimeout(() => setLoading(false), 1200); return () => window.clearTimeout(id); }, [loading]); return ( <div className="flex w-full flex-col items-start gap-3"> <Button type="button" variant="outline" size="sm" disabled={loading} onClick={() => setLoading(true)} > <RotateCcwIcon data-icon="inline-start" aria-hidden="true" /> Reload prompt </Button> <SkeletonReveal loading={loading} label="Loading prompt" className="w-full" skeleton={ <PromptEditorSkeleton minHeight={140} className="w-full" /> } > <PromptEditor value={value} onChange={setValue} variables={[]} minHeight={140} aria-label="Nora guardrails" className="w-full" /> </SkeletonReveal> </div> );}Disabled
For people who can read the prompt but not change it. Say who can, beside it.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Only admins can edit Nora's prompt. Ask Maya Okafor for access.
import { PromptEditor } from "@oration/canon/components/prompt-editor/prompt-editor";import * as React from "react";export function ReadOnly() { const [value, setValue] = React.useState( "Greet {{supplier_name}} by name, confirm {{invoice_number}} and tell them it's scheduled for {{payment_date}}.", ); return ( <div className="flex w-full flex-col gap-2"> <PromptEditor value={value} onChange={setValue} baseline={value} baselineLabel="published version 4" variables={[ { name: "supplier_name" }, { name: "invoice_number" }, { name: "payment_date" }, ]} minHeight={96} disabled aria-label="Nora prompt" /> <p className="text-13 text-muted-foreground"> Only admins can edit Nora's prompt. Ask Maya Okafor for access. </p> </div> );}States#
| State | Treatment |
|---|---|
| Loading | Before Tiptap mounts, the frame shows four skeleton lines at minHeight. Use PromptEditorSkeleton while the prompt itself loads. |
| Empty | The placeholder shows in muted text. Empty lines in a focused editor hint: Type / for commands or {{ for a variable. |
| Focused | A 3px ring at 20% around the whole frame. |
| Slash menu open | A popover of Blocks and AI actions under the caret, filtered as you type. The highlight follows the arrow keys. |
| Variable menu open | Custom variables, then system variables, then Create name when the typed name is new and valid. |
| Text selected | The AI bubble appears above the selection after 120ms: Ask AI, Rewrite, Shorten, Expand, Translate and Change tone. |
| Suggestion streaming | The editor is locked. New text streams in green beside the struck original with a pulsing caret, and a Stop button. |
| Suggestion ready | An inline word diff with Accept (focused), Reject and Try again. Whole-document actions replace the blocks with the diff. |
| Suggestion failed or empty | Couldn't get a suggestion. with Try again and Dismiss, or No changes suggested. with Dismiss. |
| Changed | Indigo dot and 3 lines and 12 words changed, with View changes. Unchanged reads Matches the published version. |
| Undefined variable | The chip takes the warning tint, an icon and a tooltip saying it will be blank on calls. |
| Disabled | The whole frame drops to 60% opacity, the text isn't editable and every toolbar tool is disabled. |
Behavior#
valueandonChangeare plain text. Chips serialize to{{name}}, and the Markdown subset (#headings,-and1.lists with two-space nesting,>quotes, fenced code,---,**bold**,_italic_, `code`) round-trips without escaping, so the model sees exactly what was written.- It is controlled. A new
valuefrom outside replaces the document without callingonChange; values the editor just emitted are ignored. - Typing
{{opens the variable menu, matching names and descriptions. Picking one inserts the chip and a space. A new snake_case name offers Create, which inserts the chip and callsonCreateVariable. - Typing a full
{{name}}or pasting text that contains one turns it into a chip. /after a space or at the start of a line opens Blocks (Heading, Bulleted list, Numbered list, Quote, Divider, Variable) and AI (Write a section, Improve writing, Add guardrails, Summarize, Translate).- Menus keep focus in the editor: arrows move the highlight, Enter or Tab picks, Escape closes. They enter with a fast spring.
- AI actions call
onAIActionwith the action, the selection and the whole document as text. The full answer is awaited, then streamed in a few words at a time. Without a handler, built-in simulated answers run, which is what these demos use unless noted. - While a suggestion is open the editor is read-only. Accept inserts it as one undoable transaction and returns focus to the text; Reject restores focus without changes.
- The footer diff uses a deferred value, so typing stays smooth on long prompts. View changes folds unchanged lines, keeping two lines of context around each change.
- The toolbar is one tab stop and sticks to the top of the frame's scroll container. The
refhandle offersfocus(),insertVariable(name)andopenVariableMenu(), which a variables panel uses to insert at the caret.
Do and don't#
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Type slash for blocks and AI actions, or two opening braces to insert a variable. Select text for AI rewrites, then press Control J or Command J to reach them.
Variables
- supplier_nameUsed once
The supplier's name from the vendor record.
Example: Northwind Freight
- invoice_numberUsed 2 times
The invoice the caller is asking about.
Example: INV-20931
Variables
2 variables are used but not defined. They will be blank on calls.
- invoice_number (undefined variable)
- remittance_email (undefined variable)
- supplier_nameUsed once
Content#
- Write the prompt to the agent in the second person: You are Nora, the supplier support agent for Cedarline.
- Use
#headings for sections (Who you are, How you sound, What you do, Guardrails) and numbered lists for steps in order. - Variable names are lowercase snake_case nouns:
supplier_name,invoice_number,payment_date. - Describe each variable in one sentence that says where the value comes from, and give a realistic example such as INV-20931.
- The placeholder says what to write, not how the editor works: the default is Describe who the agent is, what it should do and how it should sound…
- Set
baselineLabelto the version people know: published version 4 reads as Matches the published version 4.
Accessibility#
- The writing area is
role="textbox"witharia-multiline. Name it witharia-label(default Prompt) or pointaria-labelledbyat a visible heading. - A visually hidden hint, linked by
aria-describedby, explains/,{{, AI rewrites and the ⌘J or Ctrl J shortcut. Your ownaria-describedbyis appended to it. - While a menu is open the editor carries
aria-expanded,aria-controls,aria-autocomplete="list"andaria-activedescendantpointing at the highlighted option, and a live region announces the count. - The AI bubble is a
role="toolbar"with arrow-key movement. Suggestions are a labelled group; added and removed words carry visually hidden Added and Removed prefixes, and a live region says when a suggestion is ready. - Undefined variables carry an icon and a tooltip, not only the warning tint. The footer dot is
aria-hidden; its sentence carries the meaning. - The streaming caret pulses only when motion is allowed.
| Keys | Action |
|---|---|
| / | Opens blocks and AI actions after a space or at the start of a line. |
| {{ | Opens the variable menu. |
| ↑↓ | Move the highlight in an open menu. |
| Enter | Picks the highlighted item. Tab does the same. |
| Esc | Closes a menu, leaves the AI bubble, or rejects an open suggestion. |
| ⌘J | With text selected, moves focus into the AI bubble. Ctrl J on Windows. |
| ←→ | Move between toolbar tools or AI bubble actions. Home and End jump in the toolbar. |
| ⌘Enter | Accepts a ready suggestion. |
| ⌘B | Bold. ⌘I italic, ⌘E inline code, ⌘⇧8 bulleted list, ⌘⇧7 numbered list. |
| ⌘Z | Undo. ⌘⇧Z redo. |
Design tokens#
| Token | Used for |
|---|---|
--card | Frame and sticky toolbar background |
shadow-border | Frame edge |
--ring | Focus ring at 20% on the frame |
--border | Toolbar and footer hairlines, quote rule, dividers |
--accent | Pressed tools and the highlighted menu item |
--popover | AI bubble and suggestion menus, with shadow-popover |
--primary | Changed dot in the footer |
--success | Inserted words in suggestions and the changes diff |
--destructive | Removed words and the error message |
--warning | Undefined variable chips and the panel's warning well |
--tag-gray | Custom variable chips |
--tag-teal | System variable chips |
--muted | Inline code and code block fill |
--subtle-foreground | List markers and the empty-line hint |
API reference#
PromptEditor
The editor, toolbar, menus, AI bubble and change footer.
Other props spread onto Doesn't spread props; renders <div data-slot="prompt-editor">.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | No default | Prompt text with light Markdown and {{variables}}. |
onChangeRequired | (text: string) => void | No default | Called with plain text on every edit. |
variablesRequired | PromptVariable[] | No default | Known variables, for the menu, chip states and tooltips. |
baseline | string | No default | The published text. Enables the change count and View changes. |
baselineLabel | string | "published version" | Names the baseline in the footer and the diff dialog. |
onCreateVariable | (name: string) => void | No default | Called when someone creates a variable from the {{ menu. The chip is inserted either way. |
onAIAction | (request: AIActionRequest) => Promise<string> | No default | Runs an AI action and resolves with the replacement text. Without it, simulated answers run. |
placeholder | string | "Describe who the agent is, what it should do and how it should sound…" | Shown while the document is empty. |
toolbar | boolean | true | Shows the formatting toolbar. |
toolbarActions | ReactNode | No default | Extra controls at the end of the toolbar. |
minHeight | number | string | 280 | Height of the writing area, like 240 or "18rem". |
maxHeight | number | string | No default | Past this height the writing area scrolls. |
id | string | No default | Set on the writing area, for a <label htmlFor>. |
aria-label | string | "Prompt" | Name of the writing area. |
aria-labelledby | string | No default | Id of a visible heading that names the writing area. |
aria-describedby | string | No default | Added after the built-in keyboard hint. |
disabled | boolean | false | Read-only text, disabled toolbar, 60% opacity. |
ref | Ref<PromptEditorHandle> | No default | Imperative handle: focus, insertVariable, openVariableMenu. |
className | string | No default | Merged onto the frame. |
PromptEditorHandle
What the ref exposes.
| Prop | Type | Default | Description |
|---|---|---|---|
focus | () => void | No default | Focuses the writing area. |
insertVariable | (name: string) => void | No default | Inserts a chip at the caret. |
openVariableMenu | () => void | No default | Types {{ at the caret, which opens the variable menu. |
PromptVariable
One variable the prompt can reference.
| Prop | Type | Default | Description |
|---|---|---|---|
nameRequired | string | No default | snake_case name, without braces. |
description | string | No default | Shown in the menu, the chip tooltip and the panel. |
example | string | No default | A realistic value, shown beside the description. |
kind | "custom" | "system" | "custom" | System variables are filled in at call time and drawn teal. |
AIActionRequest
What onAIAction receives.
| Prop | Type | Default | Description |
|---|---|---|---|
kindRequired | "write" | "improve" | "guardrails" | "summarize" | "translate" | "rewrite" | "shorten" | "expand" | "tone" | "custom" | No default | Which action ran. |
instruction | string | No default | Free text for write and custom, the language for translate, the tone for tone. |
selection | string | No default | The selected text, when the action runs on a selection. |
documentRequired | string | No default | The whole prompt as text, with variables as {{name}}. |
VariablesPanel
The variables a prompt can reference, with usage counts and a warning for names used but not defined. Pair it with extractVariables(value).
Other props spread onto Doesn't spread props; renders <section aria-labelledby>.
| Prop | Type | Default | Description |
|---|---|---|---|
variablesRequired | PromptVariable[] | No default | Defined variables, custom and system. |
usedNamesRequired | string[] | No default | Every {{name}} in the prompt, repeats included. |
onInsert | (name: string) => void | No default | Shows an insert button on each row. |
onCreate | (name?: string) => void | No default | Shows New variable, and Define beside each undefined name (called with that name). |
onEdit | (variable: PromptVariable) => void | No default | Shows an edit button on custom rows. |
title | string | "Variables" | The panel heading. |
className | string | No default | Merged onto the section. |
PromptEditorSkeleton
The editor's frame with placeholder lines, for while the prompt loads.
| Prop | Type | Default | Description |
|---|---|---|---|
toolbar | boolean | true | Draws a toolbar row. |
minHeight | number | 280 | Match the editor's minHeight. |
className | string | No default | Merged onto the frame. |
extractVariables
(text: string) => string[]. Every {{name}} in order, repeats included.
| Prop | Type | Default | Description |
|---|---|---|---|
textRequired | string | No default | Prompt text. |
docToText
(doc: JSONContent) => string. Tiptap JSON to prompt text.
| Prop | Type | Default | Description |
|---|---|---|---|
docRequired | JSONContent | No default | A Tiptap document. |
textToDoc
(text: string) => JSONContent. Prompt text to Tiptap JSON.
| Prop | Type | Default | Description |
|---|---|---|---|
textRequired | string | No default | Prompt text. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The AI bubble's buttons don't set type="button". The bubble is appended inside the editor, so inside a <form> choosing Rewrite or Shorten submits the form.
The AI bubble's buttons and the toolbar's Variable button pass text-13 next to a text color through Button's cn, so the 13px size is dropped and they render at 14px.
onAIAction gets no AbortSignal. Stop ends the local stream, but the request keeps running.
The suggestion hint reads ⌘↵ to accept, Esc to reject on every platform; the toolbar detects Windows and shows Ctrl, the hint doesn't.
Try again in a ready suggestion is an icon button named with title, not a tooltip.
disabled dims the whole frame to 60%, text included. There is no read-only mode at full contrast for people who can read but not edit.
The menus' entrance spring and the AI bubble have no reduced-motion check of their own. The app-wide MotionConfig drops their transforms, but their fades still play.
The token count is characters divided by four, a rough estimate rather than the model's tokenizer.
PromptEditorSkeleton takes a number for minHeight while PromptEditor also accepts strings.