AI chatPro
A complete AI chat with streamed markdown answers, folding reasoning, tool chips, citations, and a model switcher.
- A full page assistant with conversation history, model choice, and streamed answers.
- Research or support assistants that show tool calls and cite sources inline.
- Products that need regenerate, answer versions, and editable prompts out of the box.
- Use ai-composer when you only need the prompt input.
- Use chat-thread for person to person messaging without model output.
- Use support-widget for a small floating help chat.
Installation
Pro source and install commands unlock with a Pro plan.
Usage
Use this as a starting point and replace the sample data with your own.
"use client"; import { AiChat } from "@/registry/blocks/ai-chat/ai-chat";import type { ChatResponder } from "@/registry/blocks/ai-chat/ai-chat"; const respond: ChatResponder = async function* (request, signal) { const response = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ messages: request.messages, model: request.model }), signal, }); const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader(); while (true) { const { value, done } = await reader.read(); if (done) break; yield { type: "text", text: value }; }}; export function Assistant() { return ( <AiChat respond={respond} notice={null} models={[{ id: "default", name: "Default", description: "Balanced for everyday work" }]} defaultModel="default" defaultConversations={[]} user={{ name: "Ava Mitchell", detail: "Pro plan" }} /> );}API reference
5 parts. The first is the root.
AiChat
A full AI chat surface: a collapsible conversation list, streamed markdown answers, a reasoning panel, tool call chips, citations with source cards, regenerate with versions, editable prompts, a model switcher, image attachments, and a send button that becomes stop. Connect respond to your model. Also the default export.
defaultConversationsChatConversation[]sampleConversationsInitial conversations. The block owns them afterwards.onConversationsChange(conversations: ChatConversation[]) => void–Called after every change, including each streamed chunk. Not called on mount.activeIdstring | null–Controlled open conversation id; null shows a new chat.defaultActiveIdstring | nullnullOpen conversation on mount when uncontrolled.onActiveChange(id: string | null) => void–Called when a conversation is opened or a new chat starts.modelsChatModel[]sampleModelsOptions in the model switcher.modelstring–Controlled model id.defaultModelstring"atlas"Starting model id when uncontrolled.onModelChange(id: string) => void–Called when a model is chosen.sidebarOpenboolean–Controlled sidebar state on wide layouts.defaultSidebarOpenbooleantrueStarting sidebar state on wide layouts.onSidebarOpenChange(open: boolean) => void–Called when the wide sidebar opens or closes.suggestionsChatSuggestion[]sampleSuggestionsPrompts offered on an empty chat.respondChatRespondersampleResponder(request, signal) => AsyncIterable<ChatStreamEvent>. Streams thinking, tool, sources, and text events. The default plays scripted samples.greetingstring"What are we working on?"Heading on an empty chat.noticeReactNode"Sample responses"Small note in the header. Pass null to hide it.user{ name: string; avatar?: string; detail?: string }{ name: "Emma Collins", avatar: "/media/people/emma-collins.jpg", detail: "Team plan" }Account shown at the bottom of the sidebar.classNamestring–Extra class on the root.ChatResponder
Exported type. (request: ChatRequest, signal: AbortSignal) => AsyncIterable<ChatStreamEvent>. ChatRequest is { conversationId, messages, prompt, attachments, model }. Stop aborts the signal.
No props.
ChatStreamEvent
Exported type. { type: "thinking"; text } appends reasoning, { type: "tool"; tool } adds or updates a ChatToolCall by id, { type: "sources"; sources } replaces the citation list, { type: "text"; text } appends answer markdown. The first tool or text event closes the thinking panel.
No props.
sampleResponder
The default scripted responder, exported for demos and tests. It honors the abort signal.
No props.
StreamText
Exported from ai-chat/markdown.tsx. Plain streamed text, such as reasoning: the same per-word fade as answers, without markdown blocks. Newlines become line breaks. The same file exports Markdown, parseBlocks, plainText, and CopyControl.
textRequiredstring–Text so far.streamingRequiredboolean–Animates newly arrived words while true.reducedRequiredboolean–Pass the reduced motion preference; true renders words without the fade.classNamestring–Class on the paragraph.- Enter
- Sends the message. Shift Enter adds a new line; IME composition is respected.
- Escape
- Stops generation from the composer, cancels a prompt edit, closes a source card, or closes the narrow drawer.
- Arrow Down
- On the model pill, opens the model list.
- Arrow UporDown, HomeorEnd, EnterorSpace
- Move through and choose in the model list; Escape closes it and returns focus to the pill.
- Tab to a citation
- Keyboard focus opens the source card; Enter toggles it.
- The sidebar is an aside labelled "Chat history" with a nav of conversations; the current one has aria-current="page".
- Streaming answers set aria-busy. A polite live region announces generation, completion time, stops, and errors.
- The model switcher is a button with aria-haspopup="listbox" and a listbox using aria-activedescendant.
- Citation chips are labelled with source number, title, and domain; the source card is a role="dialog" linked with aria-controls.
- Thinking, tool detail, and source list toggles use aria-expanded and aria-controls. Icon buttons all have labels.
- Streamed words fade and unblur in with a CSS keyframe; code lines fade in as they arrive.
- Answer height follows content on a spring through a ResizeObserver, so streaming grows instead of jumping.
- The model pill grows into its menu as one surface and shrinks back around the new name.
- Switching answer versions slides in the direction of travel. A new conversation title scrambles in.
- The wide sidebar springs its width open and shut; on narrow layouts it slides over the thread as a drawer.
- Reduced motion stops the word and line keyframes and spinners, removes offsets and blur, and uses short fades.
- Below 720px root width the sidebar becomes a drawer over the thread with a scrim, closed by Escape or a tap outside.
- The main column is a size container. At 560px and below the thread, dock, and header padding tighten; at 380px the header notice hides.
- Message hover actions are always visible on devices without hover.
- Every stream event patches state, so each chunk re-renders the active answer. Batch tiny tokens into larger chunks in respond.
- A ResizeObserver keeps the thread pinned to the bottom while you are within 56px of it; scrolling up releases it and shows a jump button.
- Messages are not virtualized. Very long conversations should load older messages on demand.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for an assistant or copilot that streams answers, shows reasoning and tool use, and cites sources.
- Write respond as an async generator that yields ChatStreamEvent objects and passes the signal to fetch. Write citations in the text as [1], [2], matching the sources event.
- Conversations are uncontrolled after mount; persist them from onConversationsChange, ideally debounced since it fires per chunk.
- Markdown supports headings, lists, quotes, tables, and fenced code with basic regex highlighting from markdown.tsx; swap in your own highlighter if you need more languages.
- The reasoning panel renders with StreamText from markdown.tsx; reuse it for any plain streamed text next to the chat.
- Attachments are images only, up to four, via picker, paste, or drop. They arrive as blob: URLs, so upload them inside respond if the model needs them.
The full library index for assistants is at /llms.txt.