Blocks
AI chat
A complete assistant surface in the shape people already know — a conversation sidebar, a threaded chat with attachments and a rich composer, and a side panel that opens generated documents beside the conversation. Assembled from the Orbit library: Button, Tooltip, Input, Menu, Avatar, Badge, the markdown-editor's renderer, and the file-dropzone helpers.
npx shadcn@latest add @orbit/ai-chatOverview
Live — send a message (Enter), click the Sprint 24 Retro card to open the artifact panel, attach or drag in a file, switch chats, or start a new one. It ships with a seeded conversation and canned replies so it demos on its own.
Empty state & greeting
A conversation with no messages renders the greeting headline (override it with the greeting prop) and suggestion chips that pre-fill the composer. Try one that sounds like drafting — “Draft a project brief” — and the demo reply mints an artifact.
Capabilities
Every affordance people expect from an assistant, on your tokens and ready to drop into a product.
User bubbles and assistant replies with lightweight markdown — bold, code, and bullet lists — plus copy, rate and retry actions and a typing indicator.
Attach files from the composer or drag them in — images preview as thumbnails, everything else as type-colored cards, removable before you send.
Replies can produce documents that open in a side panel with a version pill, copy and download — the conversation and its output live side by side.
A date-grouped, searchable chat sidebar with new-chat and switching, plus a composer model picker — collapse either rail to focus.
Built from the library
The block is a composition — these Orbit components do the heavy lifting, so it inherits their theming, focus rings, and accessibility.
How it's assembled
Three columns — sidebar, chat pane, artifact panel — inside one bordered frame.
<AIChat> ├─ Sidebar · aside, collapsible │ ├─ Button · primary lg — "New chat" │ ├─ Input · search chats, clearable │ ├─ Conversation list · date-grouped, filtered rows │ └─ Avatar + Button(ghost) · account row, settings ├─ Chat pane │ ├─ Top bar · Button(ghost) × 3 + Tooltip │ ├─ Thread · role="log", autoscrolls │ │ ├─ renderMarkdown() · assistant + user bodies │ │ ├─ Attachment chips · file-dropzone helpers │ │ ├─ Artifact card · opens the panel │ │ ├─ Copy / rate / retry · Button(ghost sm) + Tooltip │ │ └─ Typing dots · while a reply is pending │ └─ Composer │ ├─ Attach + drag-and-drop · Button(secondary) + drop overlay │ ├─ Menu · model picker (radio group) │ └─ Button · send, disabled when empty └─ Artifact panel · aside, Esc closes ├─ Badge · version pill ├─ Copy / download / close · Button(ghost) + Tooltip └─ renderMarkdown() · document body
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| showSidebar | boolean | true | Show the conversation history sidebar on load. The top-bar toggle flips it either way. |
| startWithArtifact | boolean | true | Open the artifact panel on load (the first artifact in the map). |
| greeting | string | “Good to see you, {first name}” | Empty-state greeting headline. |
| userName | string | "Sam Chen" | Account in the sidebar footer — drives the Avatar and the default greeting. |
| conversations | AIChatConversation[] | seeded demo | Initial conversations. The first one is active. |
| artifacts | Record<string, AIChatArtifact> | seeded demo | Artifact map keyed by AIChatMessage.artifactId. Bodies are Markdown. |
| models | AIChatModel[] | 3 Orbit models | Options in the composer's model picker. |
| defaultModel | string | second model | Initially selected model id. |
| suggestions | AIChatSuggestion[] | 4 chips | Empty-state suggestion chips — { label, icon? }. |
| demoReplies | boolean | true | Built-in canned-reply mode: typing dots, then a reply (an artifact for drafting-shaped prompts). Turn off with a real backend. |
| onSend | (text, attachments, conversationId) => void | — | A message was submitted — wire your model call here. |
| onModelChange | (modelId: string) => void | — | The picker's model changed. |
| onConversationChange | (conversationId: string) => void | — | A different conversation was selected in the sidebar. |
| onNewChat | (conversationId: string) => void | — | “New chat” was clicked, with the fresh conversation's id. |
| Prop | Type | Default | Description |
|---|---|---|---|
| AIChatConversation | { id, title, group, messages } | — | group is the sidebar section label — “Today”, “Yesterday”, “Previous 7 days”. |
| AIChatMessage | { id, role, text, attachments?, artifactId? } | — | role is "user" | "assistant"; text is Markdown; artifactId renders an open-artifact card. |
| AIChatAttachment | { id, name, size, kind, url? } | — | kind "image" with a url renders an 88px thumbnail; anything else a type-colored chip. |
| AIChatArtifact | { title, kind, version, body } | — | body is Markdown, rendered by the markdown-editor's renderMarkdown(). |
| AIChatModel | { id, name, desc } | — | A picker option — name plus a one-line positioning blurb. |
Usage
The block demos on its own — wire the composer's submit to a real model to make it live.
Stream responses
Swap the canned reply for a real token stream and keep the typing indicator until the first token — the perceived speed is most of the experience.
Trap output in the chat
Anything long or reusable — a doc, a table, code — belongs in the artifact panel, not buried in the scrollback where it's lost on the next turn.
Keep the composer reachable
Enter sends, Shift+Enter adds a line, and the send button disables when empty. The field stays pinned so it's always one keystroke away.
Hide provenance
Show which model answered and let people switch. A silent model swap erodes trust in the answers people are acting on.
Keyboard & accessibility
Every action is a labeled button with the library's focus ring. The thread is a polite live region (role=log) so replies are announced; the model picker is a Base UI menu with radio semantics and full keyboard navigation; Esc closes the artifact panel, and the drag-and-drop overlay is presentation-only — the attach button reaches the same picker.