Chat
The AI conversation thread, the second piece of the AI module. The assistant answers as a document, the Notion and ChatGPT style; the user turn stays a bubble. Streaming and reasoning included.
Installation#
Anatomy#
Every part is a named export, never dot-notation, so each one crosses the server and client boundary cleanly. Conversation is the scrollable thread; each Message carries its role to its parts through context, so one prop drives the side, tint, and meta alignment. Compose only the parts a turn needs.
<Conversation>
<Message role="assistant">
<MessageAvatar name="Koala" />
<MessageBody>
<MessageHeader>
<MessageName>Koala</MessageName>
<MessageTime>2:34 PM</MessageTime>
</MessageHeader>
<MessageReasoning>
<MessageReasoningTrigger>Thought for 6s</MessageReasoningTrigger>
<MessageReasoningContent>…</MessageReasoningContent>
</MessageReasoning>
<MessageContent>…</MessageContent>
<MessageActions>
<MessageAction aria-label="Copy">…</MessageAction>
</MessageActions>
</MessageBody>
</Message>
</Conversation>Usage#
import {
Conversation,
Message,
MessageAvatar,
MessageBody,
MessageContent,
} from "@/components/ui/chat"
export function Thread() {
return (
<Conversation className="max-h-[32rem]">
<Message role="user">
<MessageBody>
<MessageContent>Where is the theme applied on load?</MessageContent>
</MessageBody>
</Message>
<Message role="assistant">
<MessageAvatar name="Koala" />
<MessageBody>
<MessageContent>In app/layout.tsx, via a blocking script before paint.</MessageContent>
</MessageBody>
</Message>
</Conversation>
)
}Roles#
A message's role is the only switch. assistant aligns left with an avatar and a muted bubble; user aligns right with a solid blue bubble. Both use a soft, uniform radius on all four corners.
With avatar (bubble)#
The default is the document style above. For a chat with an identity, the “robotized” look, set variant="bubble" on the Conversation: every turn gets a bubble, and you pair the assistant with a MessageAvatar, name, time, and actions. A per-message variant overrides the conversation default.
Rich content#
MessageContent is markup-agnostic. Drop in plain text or your own rendered markdown; the bubble styles the common elements (paragraphs, headings, lists, inline code, pre blocks, and links) through descendant selectors, tinted to read on either bubble.
Actions#
MessageActions holds a row of MessageAction buttons for copy, regenerate, and feedback. Each wraps our Button, so it inherits the press scale, the focus ring, and a tooltip drawn from its aria-label.
Typing indicator#
While a response streams, drop a MessageTyping inside a MessageContent. Three dots ripple left to right, and hold still under reduced-motion.
Reasoning#
The collapsible “thinking” block, the way Claude and ChatGPT show it. While streaming the icon pulses and the trigger reads “Investigating…”; once done, it settles to a summary like “Investigated 14 sources” that anyone can expand. For a simple answer, pass plain text; for an agentic run, the block becomes a log of what it did. Group each phase in a MessageReasoningStep (an optional label captions it, a dot pins onto the rail so a column reads as a timeline, and status="active" pulses the in-progress one). Show the searches it ran with MessageReasoningQuery and the URLs it consulted with MessageReasoningSource, as compact favicon and domain chips by default or a bordered title list via layout="list". The domain and favicon are derived from the href. Built on Radix Collapsible, so keyboard and ARIA come for free. Press Ask again to watch it stream through the phases.
API reference#
Conversation
The scrollable thread viewport (role="log", a polite live region, so appended turns are announced). A flex column with a gap; set a height or max-height to make it scroll. variant ("plain" default, "bubble") sets the style for every turn inside. stickToBottom (default true) follows new turns, pinning the viewport to the newest message as the thread grows, and only while the reader is already at the bottom, so scrolling up to reread history is never yanked back down. Turn it off for a static transcript that should open at the top. Forwards all native div props.
Message
One turn. role?: "assistant" | "user" (default "assistant") drives alignment and bubble tint, and flows to every part through context. variant overrides the conversation default for this turn.
MessageAvatar
Wraps our Avatar. Takes src or name (initials fallback) plus any Avatar prop (e.g. size); children override the fallback, for example with an icon.
MessageBody · MessageHeader · MessageName · MessageTime · MessageContent
MessageBody is the vertical column that aligns to the message side; MessageContent is the bubble (markup-agnostic). Header, Name, and Time are the optional meta row above the bubble.
MessageActions · MessageAction · MessageTyping
MessageAction is a ghost icon Button (all Button props apply, and the aria-label drives both the name and a tooltip); MessageTyping is the three-dot streaming indicator.
MessageReasoning · MessageReasoningTrigger · MessageReasoningContent
The collapsible thinking block (Radix Collapsible). MessageReasoning takes streaming (pulses the icon, defaults the label to “Thinking…”) plus Collapsible's open / defaultOpen / onOpenChange. The trigger's children are the label; the content holds the chain of thought behind a left rail.
MessageReasoningStep · MessageReasoningQueries · MessageReasoningQuery
MessageReasoningStep is one phase of an agentic reasoning log. label captions it, status?: "active" | "done" (default "done") turns the rail dot into a brand pulse, and marker={false} drops the dot. MessageReasoningQuery is a monospace search pill; wrap several in a MessageReasoningQueries row.
MessageReasoningSources · MessageReasoningSource
MessageReasoningSources groups the consulted URLs. layout?: "chips" | "list" (default "chips") switches between compact domain pills and a bordered title list, and flows to each source through context. MessageReasoningSource takes a required href (the domain and favicon derive from it), an optional title for the list layout, and an optional favicon override (it falls back to a globe glyph). It renders an anchor that opens in a new tab.