# AI Chat

> A complete screen built from AgentMark, AgentStatus, ApprovalCard, ChatMessage and 6 more components.

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/AIChatScreen/)

## Code

```tsx
import {
  AgentMark,
  AgentStatus,
  ApprovalCard,
  ChatMessage,
  Composer,
  Reasoning,
  ToolCall,
  UsageMeter,
} from "@twentyfirstmedia/pro";
import { Shell } from "./_workspace";

const md = "I drafted a reply to **Ida Nolan**. It quotes **40 seats at €24** prorated to May 1.";
function App() {
  return (
    <Shell
      area="Agents"
      current="Chat"
      action={{
        label: "New agent",
        icon: "plus",
      }}
      pages={[
        {
          id: "Fleet",
          label: "Fleet",
          icon: "bot",
        },
        {
          id: "Chat",
          label: "Chat",
          icon: "mail",
        },
        {
          id: "Builder",
          label: "Builder",
          icon: "settings",
        },
        {
          id: "Runs",
          label: "Runs",
          icon: "refresh",
          count: 2,
        },
        {
          id: "Knowledge",
          label: "Knowledge",
          icon: "book",
        },
      ]}
    >
      <div
        className="tf-panel"
        style={{
          gap: 16,
        }}
      >
        <div
          className="tf-row"
          style={{
            flexWrap: "nowrap",
            paddingBottom: 16,
            borderBottom: "1px solid var(--line)",
          }}
        >
          <AgentMark tint="sky" live size={40} />
          <div
            style={{
              flex: 1,
            }}
          >
            <div
              className="tf-card-title"
              style={{
                padding: 0,
              }}
            >
              Atlas
            </div>
            <AgentStatus status="Waiting for approval" step="Drafts only" />
          </div>
          <div
            style={{
              width: 160,
            }}
          >
            <UsageMeter label="Context" used={19} limit={200} format={(v) => v + "k"} />
          </div>
        </div>
        <ChatMessage role="user" text="Draft a reply to Northwind about the seat upgrade" />
        <ChatMessage
          role="assistant"
          name="Atlas"
          tint="sky"
          time="09:41"
          model="Claude Sonnet 5.5"
          text={md}
          sources={[
            {
              title: "Seat upgrades · pricing v4",
            },
            {
              title: "Thread: Seat upgrade",
              icon: "mail",
            },
          ]}
          usage="1,527 tokens · €0.06"
          before={
            <div
              className="tf-col"
              style={{
                gap: 8,
              }}
            >
              <Reasoning
                duration="1.4 s"
                steps={["Find the thread", "Check pricing", "Draft, then ask"]}
              />
              <ToolCall name="Mailbox" icon="mail" summary="Read 3 messages" duration="0.8 s" />
              <ToolCall
                name="Knowledge base"
                icon="book"
                summary="Found “Seat upgrades”"
                duration="0.6 s"
              />
            </div>
          }
          after={
            <ApprovalCard
              title="Send this email to ida@northwind.io?"
              text="Atlas is set to “Drafts only”."
              approveLabel="Send email"
              onEdit={() => {}}
            >
              {
                "Hi Ida,\n\nhappy to add 40 seats before your renewal on May 1. The quote is attached.\n\nBest, Mara"
              }
            </ApprovalCard>
          }
        />
        <Composer
          placeholder="Ask Atlas something, or type / for commands"
          models={[
            {
              id: "s",
              label: "Claude Sonnet 5.5",
            },
          ]}
          model="s"
          onAttach={() => {}}
          commands={[
            {
              name: "clear",
            },
          ]}
        />
      </div>
    </Shell>
  );
}
export default App;
```

## Props

### AgentMark

| Prop | Type | Notes |
| --- | --- | --- |
| `tint?` | `Tint` |  |
| `icon?` | `IconName` |  |
| `size?` | `number` |  |
| `live?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### AgentStatus

| Prop | Type | Notes |
| --- | --- | --- |
| `status` | `"Running" \| "Thinking" \| "Waiting for approval" \| "Idle" \| "Paused" \| "Error" \| string` | Required |
| `step?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ApprovalCard

| Prop | Type | Notes |
| --- | --- | --- |
| `title` | `ReactNode` | Required |
| `text?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `agent?` | `string` |  |
| `project?` | `string` |  |
| `family?` | `RiskFamily` |  |
| `familyLabel?` | `string` |  |
| `age?` | `string` |  |
| `escalation?` | `ReactNode` |  |
| `trace?` | `ReactNode` |  |
| `note?` | `ReactNode` |  |
| `heading?` | `string` |  |
| `options?` | `ApprovalOption[]` |  |
| `approveLabel?` | `string` |  |
| `onDecide?` | `(key: ApprovalKey, text?: string) => void` |  |
| `decision?` | `ApprovalDecision \| null` |  |
| `compact?` | `boolean` |  |
| `onApprove?` | `() => void` |  |
| `onDeny?` | `() => void` |  |
| `onEdit?` | `(text: string) => void` |  |
| `state?` | `"approved" \| "denied"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ChatMessage

| Prop | Type | Notes |
| --- | --- | --- |
| `role` | `"user" \| "assistant" \| "system"` | Required |
| `name?` | `string` |  |
| `time?` | `string` |  |
| `model?` | `string` |  |
| `text?` | `string` |  |
| `streaming?` | `boolean` |  |
| `tint?` | `Tint` |  |
| `sources?` | `Source[]` |  |
| `attachments?` | `{ name: string; size?: string; kind?: string }[]` |  |
| `usage?` | `string` |  |
| `onRetry?` | `() => void` |  |
| `onFeedback?` | `(v: "up" \| "down") => void` |  |
| `actions?` | `boolean` |  |
| `before?` | `ReactNode` |  |
| `after?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `feedback?` | `"up" \| "down" \| null` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Composer

| Prop | Type | Notes |
| --- | --- | --- |
| `value?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `onSend?` | `(text: string, files: (File \| { name: string })[]) => void` |  |
| `busy?` | `boolean` |  |
| `onStop?` | `() => void` |  |
| `placeholder?` | `string` |  |
| `label?` | `string` |  |
| `models?` | `ModelOption[]` |  |
| `model?` | `string` |  |
| `onModel?` | `(id: string) => void` |  |
| `commands?` | `ComposerCommand[]` |  |
| `onCommand?` | `(c: ComposerCommand) => void` |  |
| `attachments?` | `(File \| { name: string })[]` |  |
| `onAttach?` | `(files: FileList) => void` |  |
| `onRemoveAttachment?` | `(i: number) => void` |  |
| `hint?` | `string` |  |
| `left?` | `ReactNode` |  |
| `disabled?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Icon

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `IconName` | Required |
| `size?` | `number` |  |
| `label?` | `string` |  |
| `className?` | `string` |  |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Reasoning

| Prop | Type | Notes |
| --- | --- | --- |
| `steps?` | `string[]` |  |
| `text?` | `ReactNode` |  |
| `duration?` | `string` |  |
| `streaming?` | `boolean` |  |
| `defaultOpen?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### SubNav

| Prop | Type | Notes |
| --- | --- | --- |
| `eyebrow?` | `string` |  |
| `action?` | `{ label: string; icon?: IconName; onClick?: () => void }` |  |
| `items` | `SubNavItem[]` | Required |
| `current?` | `string` |  |
| `onPick?` | `(id: string) => void` |  |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ToolCall

| Prop | Type | Notes |
| --- | --- | --- |
| `name` | `string` | Required |
| `icon?` | `IconName` |  |
| `status?` | `"running" \| "done" \| "error" \| "waiting"` |  |
| `summary?` | `ReactNode` |  |
| `duration?` | `string` |  |
| `input?` | `unknown` |  |
| `output?` | `unknown` |  |
| `defaultOpen?` | `boolean` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### TopBar

| Prop | Type | Notes |
| --- | --- | --- |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name). |
| `tag?` | `string` |  |
| `items` | `NavItem[]` | Required |
| `active?` | `string` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `extra?` | `ReactNode` |  |
| `userOpen?` | `boolean` |  |
| `onUser?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### UsageMeter

| Prop | Type | Notes |
| --- | --- | --- |
| `used` | `number` | Required |
| `limit` | `number` | Required |
| `label?` | `string` |  |
| `caption?` | `string` |  |
| `format?` | `(v: number) => string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
