How should I brew the new Huila beans?
The conversation: turns from you and the agent that follow the newest message and group by speaker.
A pour-over brings out the red fruit. Want the recipe?
'use client';
import { useState } from 'react';
import { ChatComposer, ChatMessage, ChatThread, MessageActions, StreamingText } from '@brand-studio/ui';
const replies = [
'For a 250 ml cup, use 15 g of coffee ground like coarse sand, and water around 94°C.',
'Pour in three stages over about three minutes. If it tastes sour, grind a little finer next time.',
'The Huila beans rested nine days, so they are ready. The Guji needs two more.',
];
interface Turn { id: number; from: 'user' | 'agent'; text: string }
export default function ChatThreadDemo() {
const [turns, setTurns] = useState<Turn[]>([
{ id: 1, from: 'user', text: 'How should I brew the new Huila beans?' },
{ id: 2, from: 'agent', text: 'A pour-over brings out the red fruit. Want the recipe?' },
]);
const [busy, setBusy] = useState(false);
const send = (text: string) => {
const reply = replies[(turns.length / 2 - 1) % replies.length];
setTurns(list => [...list, { id: list.length + 1, from: 'user', text }]);
setBusy(true);
setTimeout(() => { setTurns(list => [...list, { id: list.length + 1, from: 'agent', text: reply }]); setBusy(false); }, 700);
};
return (
<div style={{ width: '100%', maxWidth: 560, height: 440, display: 'grid', gridTemplateRows: '1fr auto', gap: 12 }}>
<ChatThread label="Brew assistant">
{turns.map(turn => (
<ChatMessage key={turn.id} from={turn.from} name={turn.from === 'user' ? 'You' : 'Brew assistant'} avatar={turn.from === 'agent' ? 'B' : undefined}
actions={turn.from === 'agent' ? <MessageActions copyText={turn.text} /> : undefined}>
{turn.from === 'agent' && turn.id > 2 ? <StreamingText text={turn.text} /> : <p>{turn.text}</p>}
</ChatMessage>
))}
</ChatThread>
<ChatComposer placeholder="Ask about your coffee" busy={busy} onSubmit={send} onStop={() => setBusy(false)} />
</div>
);
}Installation
pnpm add @brand-studio/uinpm install @brand-studio/uiyarn add @brand-studio/uibun add @brand-studio/uiInstall the peer dependencies
npm install react react-domImport the stylesheet once
import '@brand-studio/ui/styles.css';Wrap your app in a brand theme
import { BrandTheme, ChatThread } from '@brand-studio/ui'; import brand from './brand.json'; <BrandTheme palette={brand.tokens} mode="system"> {/* your app */} </BrandTheme>
Usage
import { ChatThread, ChatMessage } from '@brand-studio/ui';<ChatThread label="Brew assistant">
<ChatMessage from="user" name="You">How should I brew the new beans?</ChatMessage>
<ChatMessage name="Brew assistant" avatar="B" actions={<MessageActions copyText={answer} />}>
<StreamingText text={answer} />
</ChatMessage>
</ChatThread>Give the thread a height, or put it in a grid row that has one. While you are at the bottom, the thread follows each new message and each streamed word. Scroll up to read and it stays put; a Jump to latest button brings you back. Older messages added above keep your place in browsers that support scroll anchoring.
Your messages sit on the right in a bubble; the agent's read as open text. Messages in a row from one side share one name.
Accessibility
- The thread is a log named by
label, so screen readers announce new messages politely as they arrive. - The scroll area takes focus, so arrow keys scroll it.
- Each message is an article named by its speaker.
API Reference
ChatThread
| Prop | Type | Default |
|---|---|---|
children | React.ReactNode | – |
label? | string | 'Conversation' |
jumpLabel? | string | 'Jump to latest' |
className? | string | '' |
ChatMessage
| Prop | Type | Default |
|---|---|---|
from? | "agent" | "user" | 'agent' |
name? | string | – |
avatar? | React.ReactNode | – |
time? | string | – |
actions? | React.ReactNode | – |
children | React.ReactNode | – |
className? | string | '' |