Skip to content

AI interfaces

Components for chat, tool use and approvals, built around one message shape and for how assistive technology handles text that streams in.

A normal screen is a button and a result. An AI screen is a conversation: the assistant looks something up, uses a tool, asks permission, and produces a table or a chart. These components show that, and they leave the model, the server and the data to you. They take messages in one shape and tell you what the person did.

The message shape

A message has a role (user, assistant or system), a status, and an ordered list of parts. Each part has a type, and each type has its own component.

PartWhat it isShown by
textMarkdown, with a streaming flagResponse
reasoningA short account of how the answer was reachedMessage (closed by default)
toolOne tool the assistant used, and where it isToolCall, grouped by AgentActivity
citationA source for a statementCitation markers and Sources
artifactA table or a chart the assistant madeGeneratedTable and GeneratedChart
fileAn attachmentMessage

Tool states

A tool is pending, running, awaiting-approval, approved, denied, done or failed. States only move forward: a tool that needs permission goes from running to awaiting-approval, then to approved (and runs on) or denied. denied, done and failed are final. A late or repeated update can never take a finished tool back, so a screen reader hears "Searching, done" once and never a flicker.

Wire it up

The helpers return new messages and never change the one you pass in, so they fit any state library. This is the whole loop for a streamed reply:

import { Chat, appendText, finishMessage, updateTool, type ChatMessage } from "@rdloom/react";

const [messages, setMessages] = useState<ChatMessage[]>([]);
const [status, setStatus] = useState<"ready" | "submitted" | "streaming">("ready");

// Text arrives: add it to the last assistant message.
setMessages((m) => [...m.slice(0, -1), appendText(m[m.length - 1], chunk)]);

// A tool moved on (it can't go backwards).
setMessages((m) => [...m.slice(0, -1), updateTool(m[m.length - 1], "q1", { state: "done" })]);

// All done.
setMessages((m) => [...m.slice(0, -1), finishMessage(m[m.length - 1])]);

<Chat
  messages={messages}
  status={status}
  onSend={send}          // call your model or API here
  onStop={stop}
  onApprove={(toolId) => approve(toolId)}
  onDeny={(toolId) => deny(toolId)}
/>

Nothing here talks to a model. If you use an AI SDK, map its messages into this shape once, in a small function, and keep the rest of your app unchanged.

What is different about accessibility

  • Streaming is not read token by token. The message list is not a live region. One polite status line says "Assistant is responding" and "Response complete", and the reply is marked busy while it grows.
  • Tools announce themselves. Each tool says its own state changes, once, and a tool that arrives already finished is not read out again.
  • Approvals are never hidden or accidental. The question is announced when it appears (assertively for high risk), focus goes to the box and not to a button, risk is said in words as well as drawn, and a group with a waiting question can't be collapsed. After an answer, focus returns to the tool.
  • Charts always have words. A written summary sits beside every chart and View as table shows the same numbers as a table. Series differ by shape, not just color.
  • Following the reply is polite. The list follows new text only while you are at the bottom; scroll up and it stops, with a Jump to latest button.
  • Model text is never trusted. Replies are built as elements, never injected as HTML, and unsafe links show as plain text.

The components

Chat puts it all together. Use the parts on their own for your own layout: Message, PromptInput, Response, ToolCall, AgentActivity, ApprovalBox, Citation, Sources, GeneratedTable and GeneratedChart.