Skip to content

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

Full screen

Onboarding interview notes

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.json

Usage

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

PropTypeDefaultDescription
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.
sidebarOpenboolean—Controls the history sidebar at wide widths. The header button and mod+\ toggle it; below 56rem the history opens as a drawer instead.
defaultSidebarOpenbooleantrueInitial sidebar state when uncontrolled.
onSidebarOpenChange(open: boolean) => void—Called when the sidebar is shown or hidden.
classNamestring—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.