Conversation
The message area of a chat: one that follows new text down, one with edited prompts that branch, two models answering side by side, a side thread for a follow-up, and an agent transcript with tool calls folded in.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/conversation-default.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/conversation-default.json
Usage & API
Example
import { Conversation } from "@/components/ai/conversation-default"
<Conversation
children={children}
/>Props Conversation
| Prop | Type & description | Required |
|---|---|---|
children | ReactNode | Required |
className | stringHeight of the scroll area. | Optional |
threshold | numberDistance from the bottom, in px, that still counts as "at the bottom". | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Stick to bottom exports Conversation with children, className, default h-[360px], and threshold, default 32. The scroller is role log and tabIndex 0. A ResizeObserver on the inner content scrolls to the bottom when the stick flag is set. Scrolling up past threshold clears that flag and shows Latest, with an arrow, centred on the bottom edge. Clicking it smooth-scrolls to the end and sets the flag again. A fade covers the top 32px of the box so text does not meet the rim. The component does not know what a message is. The preview puts bubbles in children and appends to them.
Example
import { BranchNav } from "@/components/ai/conversation-branches"
<BranchNav
index={1}
count={1}
onChange={() => {}}
/>Props BranchNav
| Prop | Type & description | Required |
|---|---|---|
index | numberZero-based index of the branch on screen. | Required |
count | number | Required |
onChange | (index: number) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Branches exports BranchNav and BranchedMessage. BranchNav takes index, count, and onChange. It renders nothing when count is under 2. Otherwise it is a group, Versions of this message, with Previous version, a live {n}/{count}, and Next version. BranchedMessage takes branches, active, onActiveChange, onEdit, and renderAfter. A branch has a prompt and the turns after it. The pencil button copies the prompt into a textarea labelled Edit message. The note under the field reads Sending starts a new version. The others stay. Cancel and Escape abandon the edit. Enter without Shift calls onEdit with the trimmed text, unless it is empty or unchanged. Copy message writes the prompt. renderAfter receives the active branch and its index, and the preview crossfades that region.
Example
import { ModelCompare } from "@/components/ai/conversation-compare"
<ModelCompare
answers={[]}
onPrefer={() => {}}
/>Props ModelCompare
| Prop | Type & description | Required |
|---|---|---|
answers | CompareAnswer[] | Required |
onPrefer | (id: string) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Side by side exports ModelCompare with answers and onPrefer. A CompareAnswer has id, model, text, done, time, and tokens. From the md breakpoint the answers sit in a two-column grid. Below that, a tablist labelled Responses shows one at a time, and a dot on the tab marks an answer that is not done. Each column header shows the model, time, and tokens, for example 3.4s and a token count, then Prefer this. The button stays disabled until done is true, and it calls onPrefer with the id. An empty text shows three dots labelled Thinking. Paragraphs split on a blank line. The preview header names the pair. The component itself does not pick a winner. It only reports the click.
Example
import { SideThread } from "@/components/ai/conversation-side-thread"
<SideThread
quote="…"
messages={[]}
onSend={() => {}}
onClose={() => {}}
/>Props SideThread
| Prop | Type & description | Required |
|---|---|---|
quote | stringThe paragraph the thread is about. | Required |
source | stringWho wrote the quoted paragraph, shown above it. | Optional |
messages | ThreadMessage[] | Required |
replying | booleanTrue while the assistant is writing a reply in the thread. | Optional |
autoFocus | booleanFocus the reply box when the panel opens. | Optional |
onSend | (text: string) => void | Required |
onClose | () => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Side thread exports SideThread with quote, source, default Assistant, messages, replying, autoFocus, onSend, and onClose. The panel is labelled Thread. A caption above the quote shows source. The note beside the heading, hidden on the smallest width, reads Not added to the main chat. Escape calls onClose. messages are id, role of user or assistant, and text. User text is a bubble. Assistant text is a paragraph. replying shows three dots labelled Writing a reply. The reply box placeholder is Reply in thread, and Send stays disabled until there is text. autoFocus moves into that box when you set it. The panel scrolls to the latest message when the list or replying changes. The preview opens it from a quote button on a paragraph in the main thread.
Example
import { AgentTranscript } from "@/components/ai/conversation-agent"
<AgentTranscript
steps={[]}
onDecide={() => {}}
/>Props AgentTranscript
| Prop | Type & description | Required |
|---|---|---|
steps | AgentStep[] | Required |
streamingId | string | nullId of a text step that is still streaming, to show a caret after it. | Optional |
onDecide | (id: string, allow: boolean) => void | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Agent transcript exports AgentTranscript with steps, streamingId, and onDecide. A step is a user bubble, a text paragraph, a tools group, an edit, or an approval. Text treats backticks as inline code. If streamingId matches a text step, a caret sits after it. A tools step has a label, a status of running, ok, or fail, items of kind read, search, or run, and an optional output. The group stays collapsed and disabled while running, with a spinner. Opened, it lists the items and colours output lines that start with FAIL or PASS. An edit step shows the file, +added, and the removed count, and the diff starts open. An approval with status waiting asks Run {command}? with the reason, the line Only this command, this once, and Deny and Allow. onDecide receives the step id and a boolean. After that, the line reads You allowed or You denied.
Choosing a layout
Use Stick to bottom for an ordinary streaming reply, when the failure mode is the reader being dragged downward while they are reading something higher up. Use Branches when a person can edit a prompt and you need the earlier answers to remain reachable. The control reads 2/3 once there are three versions. Use Side by side when two or more models answer the same prompt and someone has to choose. Use the side thread when a follow-up is about one quoted paragraph and should not join the main transcript. The header says Not added to the main chat. Use the agent transcript when the reply is a run: files read, a command, a diff, and a pause for Allow or Deny.
Do not use the agent transcript as a generic tool-call card with no surrounding conversation. The steps are the conversation: a user step, a text step, a tools step, an edit step, an approval step. A lone approval with no transcript is a different screen. Do not use Side by side to compare two drafts of the same model. It labels each column with answer.model and calls onPrefer with the answer id. Do not put a side thread's messages into the main children and also into the panel. The panel is explicit that those replies stay separate.
Why does the thread jump when I scroll up to read?
Conversation only sticks while the reader is within threshold of the bottom. The default is 32px. If your message list grows by more than that between frames, or if something above the fold changes height, measure again: handleScroll sets the flag from scrollHeight, scrollTop, and clientHeight. While the flag is off, growth does not move scrollTop, and Latest is the way back. There is no prop to force stick. If you need to jump to an older prompt instead, that control is Scroll rail, which calls onSelect with the turn id.
Does editing a message replace the old answer?
No. BranchedMessage calls onEdit with the new text and leaves the branches array to you. The hint in the editor says the other versions stay. BranchNav then moves active among them. Sending the same text, or an empty trim, only closes the editor. The assistant turns live in branch.after, and renderAfter is how you draw them, so each version can have its own reply. Copy message copies the prompt that is on screen, not every branch.