# AI Chat Page

> A React AI chat page with conversation history, a streaming thread with reasoning and tool calls, a prompt composer and plan usage.

- Type: React block (Blocks)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add ai-chat-page`
- Docs: https://wingo-ui.com/components/ai-chat-page

## Interaction

The history sits on the left from 1024px (a left drawer behind the menu button below, from 768px with sidebar="open"): New chat (or ⇧⌘O) opens the greeting with six starter cards, the search filters titles, rows group into Pinned, Today, Yesterday and older, and each row's ⋯ menu renames (also F2 or a double click), pins, archives or deletes it after a confirmation. The panel button in the header slides the history away on desktop and the thread glides into the space. Click the title to rename the conversation inline (Enter saves, Escape cancels, an empty title keeps the old one). The model picker sits in the header's center on desktop and inside the composer on phones. Share copies a link with a toast; ⋯ renames, pins, shares, exports the conversation as a Markdown file and deletes it with an Undo toast. Type in the composer and press Enter to send (Shift+Enter for a new line), attach files with + or by dropping them, toggle Web search, Extended thinking and CRM data. The answer arrives as typing dots, then reasoning (Summit Pro or extended thinking), the CRM tool calls one by one and the streamed markdown; Stop or Escape keeps what arrived. Scroll up while it streams and the thread lets go, with a jump button to come back. Under the answer: follow-up rows, copy, regenerate and thumbs; edit a question to ask again. A prompt with the word "error" fails and shows Retry. Switching conversations stops a running answer. The sidebar footer shows the person, the plan and its reset date, a usage ring that opens the four meters and the Upgrade to Business link.

## Installation

```bash
npx wingo-ui@latest add ai-chat-page
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/ai-chat-page` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/blocks/ai-chat-page.tsx`
- `components/blocks/ai-chat-page-data.ts`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Chat Message](https://wingo-ui.com/components/chat-message) ([Markdown](https://wingo-ui.com/components/chat-message.md))
- [Chat Thread](https://wingo-ui.com/components/chat-thread) ([Markdown](https://wingo-ui.com/components/chat-thread.md))
- [Conversation List](https://wingo-ui.com/components/conversation-list) ([Markdown](https://wingo-ui.com/components/conversation-list.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Inline Edit](https://wingo-ui.com/components/inline-edit) ([Markdown](https://wingo-ui.com/components/inline-edit.md))
- [Markdown](https://wingo-ui.com/components/markdown) ([Markdown](https://wingo-ui.com/components/markdown.md))
- [Model Picker](https://wingo-ui.com/components/model-picker) ([Markdown](https://wingo-ui.com/components/model-picker.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) ([Markdown](https://wingo-ui.com/components/reasoning-indicator.md))
- [Suggestion Chips](https://wingo-ui.com/components/suggestion-chips) ([Markdown](https://wingo-ui.com/components/suggestion-chips.md))
- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [Tool Call Timeline](https://wingo-ui.com/components/tool-call-timeline) ([Markdown](https://wingo-ui.com/components/tool-call-timeline.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [Usage Meter](https://wingo-ui.com/components/usage-meter) ([Markdown](https://wingo-ui.com/components/usage-meter.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
// app/assistant/page.tsx: a standalone app screen (or inside the admin shell with height="fill")
"use client"

import { AiChatPage, type AiChatSendHandler } from "@/components/blocks/ai-chat-page"

// stream the answer from your own route (server-sent text chunks)
const onSend: AiChatSendHandler = async function* (message, { conversationId, model, tools, signal }) {
  const response = await fetch("/api/assistant", {
    method: "POST",
    body: JSON.stringify({ conversationId, model, tools, text: message.text }),
    signal,
  })
  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()
  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    yield value
  }
}

export default function AssistantPage() {
  return <AiChatPage onSend={onSend} defaultConversationId={null} />
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `conversations?` | `AiConversation[]` |  | The history, controlled: ConversationItem (id, title, updatedAt, pinned, unread, archived, model) plus messages: ChatMessageData[] and optional followUps. Pair it with onConversationsChange, which also fires for every streamed chunk. |
| `defaultConversations?` | `AiConversation[]` |  | The history while uncontrolled; defaults to AI_CHAT_SAMPLE.conversations (ten sample conversations, "Overdue invoices in September" with its reasoning, tool calls, table and email). |
| `onConversationsChange?` | `(conversations: AiConversation[]) => void` |  | Called with the whole history after every change: a new chat, a message, a streamed chunk, a rename, a pin, an archive or a delete. |
| `conversationId?` | `string \| null` |  | The open conversation, controlled; null is a new chat. |
| `defaultConversationId?` | `string \| null` | `"overdue-invoices"` | The conversation open first; null ("new" in the playground) starts on the greeting. |
| `onConversationChange?` | `(id: string \| null) => void` |  | Called when a row, New chat, a delete or a first message changes the open conversation. |
| `models?` | `AiModel[]` |  | The models of the pickers; defaults to AI_CHAT_SAMPLE.models (Summit Swift, Standard, Pro, Vision, Ultra locked to Business, Local 8B). |
| `model?` | `string` |  | The chosen model's id, controlled; pair it with onModelChange. |
| `defaultModel?` | `string` | `"standard"` | The model while uncontrolled. Summit Pro answers with a reasoning block first. |
| `onModelChange?` | `(model: string) => void` |  | Called when the header or composer picker picks a model. |
| `tools?` | `PromptTool[]` |  | The composer's toggles; defaults to AI_CHAT_SAMPLE.tools (Web search, Extended thinking, CRM data). The mock runs its CRM tool calls only with crm on and thinks first with reasoning on. |
| `defaultActiveTools?` | `string[]` | `"[\"crm\"]"` | The tools switched on at first. |
| `suggestions?` | `ChatSuggestion[]` |  | The starter cards of a new chat (two columns from 640px, one below); defaults to AI_CHAT_SAMPLE.suggestions, six sample starters. |
| `usage?` | `UsageMeterItem[]` |  | The plan's meters behind the sidebar's usage ring; defaults to AI_CHAT_SAMPLE.usage (tokens, Pro messages, images, API cost). [] hides the ring. |
| `plan?` | `AiChatPlan \| null` |  | { name, resetsAt, upgrade: { label, description, href } } for the footer line "Pro · resets on Oct 1, 2026" and the upsell link; defaults to AI_CHAT_SAMPLE.plan, null hides both. |
| `user?` | `ChatPerson` |  | The signed-in person ({ id, name, avatar }): the greeting's first name, the questions' author and the footer; defaults to Anna Price. |
| `onSend?` | `(message: PromptInputMessage, context: AiChatSendContext) => (AsyncIterable<string> \| Promise<string>)` |  | AiChatSendHandler; the context is { conversationId, model, tools, signal, messages }. Plug a real model here: yield text deltas to stream the answer or resolve with the whole of it; a rejection shows the message's error with Retry. The signal aborts on Stop, on switching conversations and on unmount. Without it a scripted mock streams (reasoning for Summit Pro, CRM tool calls, the canned answers of AI_CHAT_SAMPLE.replies, a generic answer otherwise; a prompt with the word "error" fails). |
| `replies?` | `AiChatCannedReply[]` |  | The mock model's scripts ({ id, match: RegExp, reasoning?, toolCalls?, content, followUps? }), tested against the prompt folded to lowercase without diacritics; defaults to AI_CHAT_SAMPLE.replies. |
| `initialPrompt?` | `string` | `""` | Sent once, shortly after mount, into the open conversation (a new one when none is open), like a ?q= deep link. |
| `getShareUrl?` | `(conversation: AiConversation) => string` |  | The link Share copies; defaults to origin + /assistant/<id>. |
| `sidebarFooter?` | `ReactNode` |  | Content under the usage block, in the sidebar and the drawer (a version line, help links). |
| `height?` | `"fill" \| "screen"` | `"screen"` | screen is a standalone h-dvh app with its own <main>; fill takes what is left of a shell with layout="fill" (admin-shell) and scrolls inside its panes. |
| `sidebar?` | `"auto" \| "open" \| "closed"` | `"auto"` | auto: the history is open from 1024px and a left drawer below; open: inline from 768px (tablets too); closed: starts collapsed on desktop, the header button slides it in. |
| `variant?` | `"default" \| "bubbles" \| "flat"` | `"default"` | The messages' layout, passed to ChatThread: the question in a bubble and the answer as text, both in bubbles, or both on the left with names. |
| `newChatShortcut?` | `string` | `"shift+mod+o"` | Opens a new chat from anywhere, shown in the New chat tooltip; "" binds nothing. |
| `searchShortcut?` | `string` | `""` | Focuses the history search from anywhere ("mod+k"); "" binds nothing. |
| `showUsage?` | `boolean` | `true` | The sidebar footer: the person, the plan line, the usage ring and the upsell. |
| `assistantName?` | `string` | `"Summit Assistant"` | The assistant's name in the sidebar brand, the composer placeholder, the message headers (flat) and screen reader text. |
| `labels?` | `Partial<AiChatPageLabels>` |  | Every visible string, English by default (DEFAULT_AI_CHAT_PAGE_LABELS; AI_CHAT_PAGE_LABELS_EN is the same set). Nested keys (list, thread, message, markdown, reasoning, toolCalls, composer, modelPicker, usageMeter, suggestionChips, inlineEdit) translate the composed items. |
| `locale?` | `string` | `"en-US"` | Intl locale for dates, times, token counts and money. |
| `now?` | `Date \| number` |  | The reference time for the history groups and day separators; new messages move on from it. Defaults to the live clock. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The accent of the send button, the question bubbles, the selected row, the starter card icons and the brand mark. |
| `loading?` | `boolean` | `false` | The history, the title and the thread as skeletons; the composer is disabled. |
| `animateIn?` | `boolean` | `true` | The thread rises in once when the page mounts in the browser; off in thumbnails and under reduced motion. |
| `linkComponent?` | `(props: LinkComponentProps) => ReactNode` | `"<a>"` | The router link for the upsell link; defaults to the LinkProvider's (a plain anchor). |
| `className?` | `string` |  | Classes for the root. |
| `classNames?` | `Partial<Record<AiChatPageSlot, string>>` |  | Classes for the parts: sidebar, brand, list, usage, main, header, title, thread, empty, drawer. |

## Parts

### DEFAULT_AI_CHAT_PAGE_LABELS (constant)

The English strings for every visible label, the default of labels. Type: AiChatPageLabels.

### AI_CHAT_PAGE_LABELS_EN (constant)

The same English strings under their older name, kept for existing imports.

### AI_CHAT_SAMPLE (constant)

The demo data: user, models, tools, suggestions, usage, plan, ten conversations, the mock replies and their defaults.

### AI_CHAT_CONVERSATION_IDS (constant)

The ids of the sample conversations, for a playground select or a deep link.

### AI_CHAT_NOW (constant)

The demo clock (Sep 24, 2026, 4:40 PM) as a timestamp, passed as now so history groups stay stable.

### findCannedReply (function)

The mock model's matcher: (prompt, replies?) => AiChatCannedReply, the first reply whose match fits the folded prompt, else a generic one.

### AiChatPageProps (type)

Props of AiChatPage, including the div attributes of the root.

### AiChatSendHandler (type)

The onSend signature: (message, context) => AsyncIterable<string> | Promise<string>.

### AiConversation (type)

A history entry: ConversationItem plus messages and optional followUps.

## Examples

- **New chat**: The greeting and six starter cards; pick one and it streams.
- **Streaming**: Summit Pro thinks, then writes a plan with a table and code. Scroll up: it lets go.
- **Inside admin-shell**: height="fill" in the shell's fill layout at /assistant.
- **Bubbles**: variant="bubbles": both sides in bubbles.
- **Renamed assistant, en-GB**: assistantName="Atlas" in the header, the placeholder and the answers, with locale en-GB.
- **Brand, sidebar closed**: tone="brand" on a new chat with the history collapsed.
- **Loading**: The history, the title and the thread as skeletons.

## See also

- [Chat Thread](https://wingo-ui.com/components/chat-thread) ([Markdown](https://wingo-ui.com/components/chat-thread.md))
- [Conversation List](https://wingo-ui.com/components/conversation-list) ([Markdown](https://wingo-ui.com/components/conversation-list.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [Model Picker](https://wingo-ui.com/components/model-picker) ([Markdown](https://wingo-ui.com/components/model-picker.md))
- [Usage Meter](https://wingo-ui.com/components/usage-meter) ([Markdown](https://wingo-ui.com/components/usage-meter.md))
- [Admin Shell](https://wingo-ui.com/components/admin-shell) ([Markdown](https://wingo-ui.com/components/admin-shell.md))

---

Canonical: https://wingo-ui.com/components/ai-chat-page
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
