Screens
AI Chat
A complete chat app screen — history sidebar, a thread with tool calls, cited answers and feedback, follow-up chips and a composer with a context meter.
2 variantsMIT
Preview
Onboarding interview notes
Today
Maya Chen
Draft five interview questions about the new onboarding.
opus-5.5
Here are five: what did you expect to do first, what made you pause, how did you invite your team, what did you skip, and what would you change first? I kept them open-ended so people describe what they did rather than rate it.
files · / commands
13.1k / 200k
Installation
Copies the screen into components/ and installs every component it uses, with their dependencies and theme tokens.
pnpm dlx shadcn@latest add https://tuffui.com/r/ai-chat.jsonUsage
Pass your own data and wire the callbacks to your agent runtime.
import { AIChat } from "@/components/ai-chat";
import { ToolCallBlock } from "@/components/ui/tool-call-block";
import { SourceList } from "@/components/ui/source-list";
import { StreamingMessage } from "@/components/ui/streaming-message";
export function ChatScreen({ chat, run }: { chat: Chat; run: AgentRun }) {
return (
<AIChat
data={{
conversations: chat.history,
activeId: chat.id,
title: chat.title,
model: run.model,
user: { name: chat.user.name },
starters: chat.starters,
followUps: run.done ? run.followUps : undefined,
context: { limit: run.contextLimit, segments: run.contextSegments },
// Each turn's content is any React node — compose answers from components.
messages: chat.turns.map((turn) => ({
id: turn.id,
role: turn.role,
at: turn.createdAt,
content:
turn.role === "user" ? (
turn.text
) : (
<>
{turn.toolCalls.map((call) => (
<ToolCallBlock key={call.id} data={call} />
))}
{turn.streaming ? (
<StreamingMessage
data={{ content: turn.text, status: "streaming" }}
showHeader={false}
/>
) : (
<SourceList data={{ sources: turn.sources, text: turn.text }} />
)}
</>
),
})),
}}
status={run.streaming ? "streaming" : "idle"}
onSend={(text) => run.send(text)}
onStop={() => run.stop()}
onNewChat={() => chat.create()}
onSelectConversation={(c) => chat.open(c.id)}
onShare={() => chat.share()}
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| data* | AIChatData | — | History, the open chat (title, model, turns), starter cards, follow-up chips, context usage and the signed-in user. |
| status | "idle" | "streaming" | "idle" | While streaming, the composer queues new messages and follow-up chips are hidden. |
| onSend | (text: string) => void | — | A message from the composer, a starter card or a follow-up chip. |
| onStop | () => void | — | Stop the current run. |
| onNewChat | () => void | — | From the history panel. Pass empty `messages` to show the starter cards. |
| onSelectConversation | (conversation: Conversation) => void | — | A chat picked in the history sidebar or drawer. |
| onShare | () => void | — | Share button in the header. |
| sidebarOpen | boolean | — | Controls the history sidebar at wide widths. The header button and mod+\ toggle it; below 56rem the history opens as a drawer instead. |
| defaultSidebarOpen | boolean | true | Initial sidebar state when uncontrolled. |
| onSidebarOpenChange | (open: boolean) => void | — | Called when the sidebar is shown or hidden. |
| className | string | — | Extra classes for the root. |
type AIChatData = {
conversations: Conversation[]; // from conversation-list
activeId?: string;
title: string;
model: string;
messages: ThreadMessage[]; // from chat-thread; content is a ReactNode
followUps?: PromptSuggestion[]; // chips under the last answer
starters: PromptSuggestion[]; // cards for an empty chat
context: { limit: number; segments: ContextSegment[] };
user: { name: string; avatar?: string };
};
// The screen fills its parent and lays out by its own width (container queries):
// a history sidebar from 56rem, a drawer below that. Give the parent a height.