Getting Started
Building AI surfaces
A small kit for chat and assistant UIs. The components only render state: you own the request, the stream and the message list. They work with any model or SDK.
The kit
| Component | What it does |
|---|---|
Conversation | Scrolling message list with a sticky composer. Follows new content while the reader is at the bottom. |
PromptInput | The composer. Send turns into Stop while a reply streams. |
MessageBubble | One message; MessageActions adds copy, feedback and regenerate. |
ThinkingIndicator | Fills the gap between sending and the first token. |
StreamingText | Renders text as it arrives without reflowing what is already there. |
Citation, SourceList | Numbered source markers and the source row under a reply. |
A reply moves through three states, and each has one component. Drive them from a single status value and pass it to PromptInput as well:
<Conversation composer={<PromptInput status={status} onSubmit={send} onStop={stop} />}>
{messages.map((m) => (
<MessageBubble key={m.id} from={m.from}>
{status === 'submitting' && m.id === lastId
? <ThinkingIndicator />
: <StreamingText text={m.text} streaming={status === 'streaming' && m.id === lastId} />}
</MessageBubble>
))}
</Conversation>The Conversation page has a working version with Stop and a citation.
Motion
- Motion only shows state. The thinking shape morphs because the assistant is working; new text fades in because it just arrived; the caret blinks because more is coming. Nothing moves for decoration.
- Text already on screen never moves. StreamingText animates only the newest chunks, and Conversation follows the bottom only while the reader is there.
- Reduced motion turns it all off: the thinking shape and shimmer are static, chunks appear without fading, the caret stops blinking, and Jump to latest scrolls instantly. The state is still visible.
Accessibility
- Don’t make streaming text a live region. Screen readers re-read the whole region on every token. StreamingText and Conversation both default to off; keep it that way while tokens arrive.
- Announce completion once. When a reply finishes, put a short message such as “Reply ready” in a polite status region, or turn on Conversation’s
announceif you add whole messages at once instead of streaming. - Keep focus in the composer. PromptInput leaves focus in the field after sending; don’t move it to the new message. Users read back with their screen reader’s article navigation, since each MessageBubble is an
article. - Stop with Escape. While
status="streaming", Escape in the field callsonStop, the same as the Stop button. - ThinkingIndicator is a polite status: it is announced once when it appears. Remove it when tokens start.
Security
- Citations only link
http:andhttps:URLs. Anything else, includingjavascript:anddata:, renders as plain text. Use the exportedsafeUrl()for any other link built from model output. - Never render model output as HTML. StreamingText renders text only. If you want Markdown, parse it to React elements with raw HTML disabled; don’t pass it to
dangerouslySetInnerHTML. - Source links open in a new tab with
rel="noopener noreferrer", and favicons load withreferrerPolicy="no-referrer".
Message keys
The AI components read these strings from NexubProvider. Each component also takes a messages prop for local overrides.
| Key | Default | Used by |
|---|---|---|
promptLabel | 'Message' | PromptInput |
sendMessage | 'Send message' | PromptInput |
stopGenerating | 'Stop generating' | PromptInput |
promptHint | 'Press Enter to send, Shift+Enter for a new line' | PromptInput |
thinking | 'Thinking' | ThinkingIndicator |
youSaid | 'You said' | MessageBubble |
assistantSaid | 'Assistant said' | MessageBubble |
failedToSend | 'Failed to send' | MessageBubble |
retry | 'Retry' | MessageBubble |
copy | 'Copy' | MessageActions |
copied | 'Copied' | MessageActions |
goodResponse | 'Good response' | MessageActions |
badResponse | 'Bad response' | MessageActions |
regenerate | 'Regenerate' | MessageActions |
sources | 'Sources' | SourceList |
source | (n) => `Source ${n}` | Citation |
jumpToLatest | 'Jump to latest' | Conversation |
conversation | 'Conversation' | Conversation |