AI Assistant
A chat panel docked beside a page, a conversation with code and feedback, questions about a usage card with the cost of each answer, and a failed reply with retry.
The assistant works with any model and any API. Messages are plain objects with an id, a role of user or assistant, and content text, and status is ready, submitted, streaming or error: the same shapes as the AI SDK's useChat. Replies are formatted safely, with no HTML, for paragraphs, lists, bold, code and fenced code blocks. A new question moves to the top of the log and its reply grows below it, so it reads from the start; a saved conversation opens at its last question. When you're at the bottom the log follows the reply, until you scroll up or select text. Screen readers hear the whole reply once it's done, not every word. The examples stream canned replies; see Usage to connect your own.
It's built on shadcn's Message, Bubble, Marker, Attachment, Code Block and Message Scroller, which install with it, so it matches the rest of your chat in your style. Code Block adds the shiki package, which colours the code in a reply as it streams, and Message Scroller adds @shadcn/react. The typing indicator's shimmer comes from the shadcn package's Tailwind CSS: if it doesn't show, update shadcn and Tailwind CSS.
Side Panel
An assistant docked beside the page, with suggested prompts to start from, the page as context, and new chat and close buttons. On narrow screens it covers the page.
Conversation
A conversation with a list and a SQL query. Copy a reply or its code, rate it, regenerate the last one, or pick a follow-up question.
Ask About Data
Questions about an AI Usage card, with the tokens and cost of each answer.
- Tokens
- 305.5M
- Cost
- $998
- Cache hits
- 30%
- Input 197M
- Cached 83M
- Output 25.5M
Retry
A reply that failed, kept in the conversation with a button to try again.
Usage
The examples run on useChatStream, a small hook in the primitives. Give it onSend, a function that streams the reply to the conversation so far as chunks of text, and it keeps the messages and status, and handles stop, regenerate and new chat. Stop aborts the signal.
async function* onSend(messages: ChatMessageData[], signal: AbortSignal) {
const response = await fetch('/api/chat', {
body: JSON.stringify({ messages }),
method: 'POST',
signal,
})
const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()
while (true) {
const { done, value } = await reader.read()
if (done) return
yield value
}
}
const chat = useChatStream({ onSend })With the AI SDK, use useChat instead and pass its values to the same primitives. Its messages keep text in parts, so join those into content:
const { messages, regenerate, sendMessage, status, stop } = useChat()
const chatMessages = messages.flatMap((message) =>
message.role === 'system'
? []
: [
{
content: message.parts.map((part) => (part.type === 'text' ? part.text : '')).join(''),
id: message.id,
role: message.role,
},
],
)
<ChatMessages latest={chatMessages.at(-1)} status={status}>
{chatMessages.map((message) => (
<ChatMessage key={message.id} message={message} />
))}
</ChatMessages>
<ChatComposer status={status} onSubmit={(text) => sendMessage({ text })} onStop={stop} />Render each ChatMessage inside ChatMessages, and pass ChatMessages the status: the log places each question at the top when the status turns submitted. Put the typing indicator, an error or follow-up suggestions inside it too, after the messages.