Chat
A conversation with a following transcript
New Thread
Ask the agent to seal a dossier or move a licence.
Chat is the whole conversation: a fixed header, the flexible transcript and a
fixed composer. The transcript is a MessageScroller, each turn is a Message
around a Bubble, and the field is ChatComposer. The transport is yours; the
status machine is shared so the composer and the header cannot disagree.
Every turn wears a bubble — the reader's on the default fill and the end edge,
the agent's muted on the start — and the row supplies the avatar, author and
timestamp. Only the row that starts a run paints its avatar and meta line; the
rows after it are a MessageGroup (6px apart) whose bubbles name group="first"
and group="last" so their inner corners join.
Installation
nub add @gpuixui/componentsUsage
import {
Bubble,
BubbleContent,
ChatComposer,
ChatHeader,
ChatMessages,
ChatRoot,
Message,
MessageAvatar,
MessageContent,
MessageHeader,
chatMove,
} from "@gpuixui/components";
<ChatRoot
status={status}
onStatusChange={setStatus}
onSubmit={send}
onStop={stop}
onRetry={retry}
>
<ChatHeader title="Release thread" />
<ChatMessages>
{messages.map((message) => (
<Message key={message.id} variant={message.role} timestamp={message.at}>
<MessageAvatar />
<MessageContent streaming={message.streaming}>
<MessageHeader />
<Bubble
align={message.role === "user" ? "end" : "start"}
variant={message.role === "user" ? "default" : "muted"}
group={message.group}
>
<BubbleContent>{message.body}</BubbleContent>
</Bubble>
</MessageContent>
</Message>
))}
</ChatMessages>
<ChatComposer hint="Enter sends · Shift+Enter adds a line" />
</ChatRoot>;Composition
ChatRoot
├── ChatHeader
├── ChatMessages the MessageScroller
│ ├── MessageScrollerViewport
│ │ └── MessageScrollerContent
│ │ └── MessageScrollerItem
│ └── MessageScrollerJumpToLatest
└── ChatComposerThe fixed rows
ChatRoot is a column whose middle grows. ChatHeader and ChatComposer do
not shrink; ChatMessages is flex: 1 1 0 with min-height: 0, so a row
arriving, a reply streaming or the jump control appearing changes nothing above
or below the transcript. Give ChatRoot a bounded height — directly, or through
a sized flex parent — because the transcript needs one to scroll.
The passed-in ChatMessages children are rows. Wrap each turn in a Message
(or a MessageGroup for consecutive rows from one sender). A Bubble is always
a child of MessageContent; the message never builds one.
Status
The four states are idle, submitting, streaming and error, and
chatMove is the only rule set. Send is refused while a reply is open, the
refused draft stays in the field, Stop emits the stop intent, and Escape stops a
reply. The header announces the state; the failure sentence replaces the status
line in place, so a failed reply does not take height from the transcript.
While a reply is open
- Send is replaced by Stop.
- Enter still arrives as the field's image submit; the machine refuses it, so the draft is kept.
- A reply's row can grow token by token; the transcript follows it only while the reader is at the end.
Failed replies
The failure copy is one line in the header with the full sentence in
aria-description; Retry is beside it. It used to be a row between the viewport
and the composer, which shrank the viewport when it appeared — the layout shift
this release removes.
API
| Part | Renders | Props |
|---|---|---|
ChatRoot | role="region" named by label | status?, defaultStatus?, onStatusChange?, onSubmit (required), onStop?, onRetry?, error?, label?, className?, children |
ChatHeader | a fixed row: title, status/error, actions | title?, description?, actions?, className? |
ChatMessages | the scroller, the empty panel and the jump rail | empty?, emptyTitle?, emptyDescription?, className?, children |
ChatComposer | a field and its Send/Stop control | value?, defaultValue?, onValueChange?, label?, placeholder?, submitLabel?, stopLabel?, hint?, attachments?, disabled?, className?, style? |
ChatMessages is also the transcript's public seam: render
MessageScrollerItem rows inside it (through Message) and pass
MessageScrollerItem scrollAnchor on a turn's first row if you want that turn
placed near the top.
Keyboard
- Escape
- stops a reply that is streaming, while the composer is busy
read from chat.tsx
Edit this page on GitHubpackages/components/docs/chat.mdx