Skip to main content

PromptInput

Where the user talks to the model. The box holds an auto-growing textarea (capped at maxRows, then it scrolls) and a toolbar row with the send button. The root is a real <form>; the send button submits it. On phones the textarea renders at 16px, so iOS does not zoom on focus, and the toolbar buttons have 44px targets on a coarse pointer.

Enter sends and Shift+Enter makes a newline (IME composition never sends); set submitKey="mod-enter" to keep Enter as a newline and send on ⌘/Ctrl+Enter — for prompts that are prose first. onSubmit receives the trimmed text and is never called with an empty string.

import PromptInput, {usePromptInput} from '@zuilib/ai/prompt-input'

Basic

Loading example

Uncontrolled, the box clears itself after a send. Controlled (value + onValueChange), clearing after a send is yours — keep the text on failure so nothing is lost.

Extension points

Three slots, all inside the box, top to bottom:

SlotWhereFor
headerAbove the textareaAttachment tray, context chips, a reply-to preview
toolbarUnder the text, before the send buttonModel picker, attach button, tool toggles, dropdowns
actionsEnd of the toolbar row, replacing the built-in send / stopA custom send button, a send-with-options split button

Anything rendered in a slot can reach the composer through usePromptInput(): read the text, insert at the caret, trigger the same guarded send as Enter. PromptInput.ToolbarButton is the matching icon button for toolbar actions.

Toolbar: custom buttons and dropdowns

The toolbar is a flex row that wraps on narrow panels. Put any control in it — the send button keeps itself at the end (ms-auto). Use PromptInput.ToolbarButton for icon actions (ghost, muted, the sm control square, type="button" so it never submits the form — give it an aria-label), and any ZUI Menu / NativeSelect / Select for dropdowns.

Loading example

Anchored Headless UI panels (Menu.Items, Select and Combobox options) portal to <body> — React context still reaches them, so usePromptInput() works inside a menu, but a subtree theme override does not; pass anchor={false} to keep the panel inside a themed subtree (the same trade-off as everywhere else in ZUI).

Custom controls: usePromptInput()

Any component inside the box can call usePromptInput() — this is how a template picker fills the textarea, a slash-command menu rewrites it, or a voice button appends a transcription:

Loading example

What the hook returns:

PropTypeDescription
valuestringThe current text
setValue(value: string) => voidReplaces the text. Uncontrolled, the box updates itself; controlled, it reaches the parent through onValueChange, which must apply it
insert(snippet: string) => voidInserts at the caret (replacing a selection), then restores focus there
submit() => voidThe same guarded send as Enter: trims, no-ops when empty / responding / disabled, clears an uncontrolled box
focus() => voidFocuses the textarea
respondingbooleanA run is in flight
disabledboolean
emptybooleanNothing but whitespace — the send button's disabled state, for a custom send

The hook throws outside <PromptInput>; it works from header, toolbar and actions, including inside portalled panels opened from them.

Header: attachments and context chips

header renders above the textarea, inside the border — the attachment tray of a chat, or chips naming what the model can see:

Loading example

The slot renders only when header is given, so an empty tray adds no blank row — pass null while there are no attachments.

Replacing the send button: actions

When the built-in icon send is not enough — a labeled button, a send-with-options split — pass actions. It replaces the send / stop entirely; wire your own controls to usePromptInput() so they stay guarded the same way:

Loading example

A type="submit" button submits the form, which routes through the same guarded send — you rarely need to call composer.submit() yourself here. With actions given, showing a Stop while responding is also yours.

While a run is in flight

Loading example

While responding, sends are blocked. With onStop the send button becomes a Stop button that aborts the run — pass the same abort the AssistantDock tasks use.

Props

PropTypeDefaultDescription
onSubmit(text: string) => voidThe send; receives the trimmed text, never empty
value / defaultValuestringControlled / uncontrolled text
onValueChange(value: string) => void
submitKey'enter' | 'mod-enter''enter'Enter sends, or ⌘/Ctrl+Enter sends
respondingbooleanA run is in flight; sends are blocked
onStop() => voidShown as the Stop button while responding
minRows / maxRowsnumber1 / 8Autogrow floor and cap
headerReactNodeAbove the textarea: attachments, context chips
toolbarReactNodeThe row before the send button
actionsReactNodeReplaces the built-in send / stop button
labelstring'Message'The textarea's accessible name
submitLabel / stopLabelstring'Send message' / 'Stop'
className / textareaClassNamestringMerged last onto the box / the textarea

Everything else a <textarea> takes (maxLength, autoFocus, name, onPaste…) passes through to the textarea; ref reaches it too. PromptInput.ToolbarButton takes every ZUI Button prop.