Getting Started
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.
| 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.
announce if you add whole messages at once instead of streaming.article.status="streaming", Escape in the field calls onStop, the same as the Stop button.http: and https: URLs. Anything else, including javascript: and data:, renders as plain text. Use the exported safeUrl() for any other link built from model output.dangerouslySetInnerHTML.rel="noopener noreferrer", and favicons load with referrerPolicy="no-referrer".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 |