Xzero UI
Pro library

AI composerPro

The prompt box at the bottom of an AI app: files upload in place, / opens commands, @ inserts mentions, a model and effort menu sits in the toolbar, and the send button turns into a stop control ringed by the reply's progress.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/ai-composer
Live · keyboard ready

Pass onSend(message, { signal }) and return an async iterable of text deltas; the block handles the fast streamed reveal, Stop (it aborts the signal), Retry and the progress ring. The message carries the text, mention and command tokens, attachments with their File, model, effort, webSearch and thinking. Without onSend it plays simulated replies; greeting, suggestions, models, placeholder and maxLength customize it.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/ai-composer

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { AiComposer, type ComposerMessage } from "@/components/xzero/blocks/ai-composer/ai-composer";

async function* streamReply(message: ComposerMessage, signal: AbortSignal) {
  const response = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ text: message.text, model: message.model, effort: message.effort, webSearch: message.webSearch, thinking: message.thinking }),
    signal,
  });
  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader();
  for (;;) {
    const { value, done } = await reader.read();
    if (done) return;
    yield value;
  }
}

export function Assistant() {
  return <AiComposer greeting="How can I help with checkout?" onSend={(message, { signal }) => streamReply(message, signal)} />;
}

API reference

AiComposer

The prompt box at the bottom of an AI app, shown on a minimal chat stage. One rounded surface holds an autosizing textarea and a toolbar: a plus menu to upload files or add them from Google Drive, GitHub or a screenshot, Web search and Extended thinking toggles, a model chip with the model's mark that opens five current models and an effort row, a dictation mic, and the send button. The send button is muted until there is something to send, shows an arrow when ready, and morphs into a stop square ringed by the reply's progress while it streams. Attachments upload in place with a progress ring that resolves into a check, long pastes become a pasted text chip, / opens commands and @ inserts a mention token. The empty state shows a greeting and three suggestions; sending lifts the message into a bubble and streams the reply below it.

PropTypeDefaultDescription
onSend(message: ComposerMessage, options: { signal: AbortSignal }) => void | Promise<void> | AsyncIterable<string>–Called with every sent message: { text, tokens, attachments, files, model, effort, webSearch, thinking }. Return an async iterable of text deltas to stream a real reply into the thread; Stop aborts signal. Return nothing and the preview plays a simulated reply and says so in its status region.
greetingstring"What should we work on?"Display heading of the new chat state, set in Geist.
suggestionsSuggestion[]sampleSuggestionsUp to three starters under the composer, each { id, label, prompt, icon? } with icon "summary", "draft" or "bug". Choosing one fills the composer with its prompt; known commands and mentions in the prompt become tokens.
modelsComposerModel[]sampleModelsModels in the model menu, each { id, name, short, note, mark, defaultEffort } with mark "claude", "openai" or "gemini". The chip shows short; the menu shows name and note.
defaultModelstring–Id of the model selected at first. Defaults to the first model, Claude Opus 5.5 in the sample.
placeholderstring"Ask anything, / for commands, @ to add context"Placeholder of the empty textarea.
maxLengthnumber4000Character limit. A ring counter appears at 80 percent, turns to the warning color at 95 percent, and sending stops above the limit.
classNamestring–Class on the root element.

Keyboard interactions

KeysAction
EnterSends the message. With the command or mention list open, inserts the highlighted option instead.
Shift + EnterAdds a new line. The field grows to about eight lines, then scrolls.
/At the start of a line, opens the command list: /summarize, /translate, /explain and /fix.
@After a space or at the start, opens the files and docs list. The choice is inserted as one atomic token.
Arrow up / Arrow downMoves through the command or mention list while it is open; the textarea keeps focus.
TabInserts the highlighted command or mention while the list is open; otherwise moves through the toolbar controls.
EscapeCloses the command or mention list, cancels dictation, or stops a running reply, in that order.
Backspace / DeleteNext to a token, removes the whole token at once. Arrow keys hop over a token instead of entering it.
Arrow left / Arrow rightIn the model menu, moves along the effort row. Up and down move through the models.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components