> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# AI 原子组件

> 17 个纯展示型组件 —— Token、聊天、图像、表单全覆盖。

AI 工作站有一套通用组件库不覆盖的 UI 词汇:流式更新的 token 计量、带"重新生成"按钮
的生成卡、带下载/复制/分享操作的图片画廊、带模型徽标的历史行。每个从零开始做都要
浪费几天处理样式和边界情况。

vibestrap 在 `src/components/ai/` 提供一套 **17 个组件的 AI 原子组件库**,每个都在
shadcn/ui 之上封了对应模式。每个都是展示型:无全局 state、无隐藏副作用、不带
Suspense。即拿即用,想换就 copy-paste 改。

## 前置条件

* shadcn/ui 已经接好（脚手架自带）。
* Tailwind 类名已被你的主题识别（`@/lib/utils` 的 `cn`）。
* 看一遍 [`src/components/ai/index.ts`](https://github.com/larryworld/vibestrap/blob/main/src/components/ai/index.ts) 的导出清单。

## API 参考

### 展示型（5 个）

**`<TokenMeter inputTokens={120} outputTokens={480} total?={4096} />`** —— 输入/输出 token 的堆叠柱状图。传 `total` 可以显示上下文窗口占用率。

**`<LatencyPill totalMs={840} ttftMs?={120} />`** —— 流式调用的 total + TTFT 胶囊条。

**`<ProviderPill provider="openai" model?="gpt-4o-mini" />`** —— 带颜色的 provider 徽章，每个 `AIProviderName` 一种颜色。

**`<CreditsBadge label?="credits" hideWhileLoading?={true} />`** —— 顶栏积分徽章，内部包了 `useCredits()`。匿名用户什么都不渲染。

**`<StreamingText text={text} isStreaming?={true} />`** —— `whitespace-pre-wrap`，streaming 时尾巴有闪烁光标。

```tsx theme={null}
<StreamingText text={text} isStreaming={isStreaming} />
```

### 聊天（2 个）

**`<ChatBubble author="user" trailing?={<CopyButton text=".." />}>...</ChatBubble>`** —— 单条消息气泡。`author` 是 `'system' | 'user' | 'assistant'`，user 靠右对齐。

```tsx theme={null}
<ChatBubble author="assistant" trailing={<CopyButton text={msg} iconOnly />}>
  {msg}
</ChatBubble>
```

**`<ChatList trigger={messages.length}>...</ChatList>`** —— 自动滚动容器。聪明：用户本来就贴底才会贴底跟。把每次 scroll 之间会变的东西塞 `trigger`。

### 容器（7 个）

**`<GenerationCard title?="GPT-4o-mini" actions?={...} footer?={...}>...</GenerationCard>`** —— 任何 AI 结果的卡片外壳：标题行 + 内容 + 元数据 footer。

```tsx theme={null}
<GenerationCard
  title="gpt-4o-mini"
  actions={<CopyButton text={text} iconOnly />}
  footer={<LatencyPill totalMs={ms} />}
>
  <StreamingText text={text} isStreaming={isStreaming} />
</GenerationCard>
```

**`<ImageGallery images={[{ url }]} aspect?="square" onClick?={...} />`** —— 响应式 1/2/3 列网格，hover 出现下载按钮。

**`<EmptyState icon?={Icon} title="..." description?="..." action?={...} />`** —— 虚线边框占位，用在"还没数据"的位置。

**`<ErrorState title?="..." description?="..." onRetry?={...} action?={...} />`** —— 带 destructive 色调的错误卡，可选重试按钮。

**`<TaskProgress status={state.status} progress?={0.4} label?="Rendering" />`** —— 进度条 + 标签，绑定 `useTask` 的 `TaskStatus`。不传 `progress` 时是不确定模式。

**`<HistoryRow row={row} />`** —— `ai_call` 历史列表里的一行。`row` 用的是 `useHistory` 的 `AICallHistoryRow` 形状。图像类调用没 token，这一栏会显示 `—`。

**`<InsufficientCreditsBanner needed={50} balance={3} topUpHref?="/pricing" />`** —— 琥珀色横幅，带"充值"CTA。在 `chat()` 返回 `INSUFFICIENT_CREDITS` 时渲染。

```tsx theme={null}
if (isInsufficientCredits(r)) return <InsufficientCreditsBanner needed={r.needed} balance={r.balance} />;
```

### 输入型（3 个）

**`<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} />`** —— 模型选择下拉。`hint` 显示在 label 下面（用来标注速度/特性）。

```tsx theme={null}
<ModelPicker
  models={[
    { id: 'gpt-4o-mini', label: 'GPT-4o mini', hint: '快 · 便宜' },
    { id: 'gpt-4o', label: 'GPT-4o', hint: '旗舰' },
  ]}
  value={model}
  onChange={setModel}
/>
```

**`<RegenerateButton onClick={fn} isLoading?={true} iconOnly?={true} />`** —— 放在 `GenerationCard.actions` 里。loading 时图标变 spinner。

**`<CopyButton text="..." iconOnly?={true} resetMs?={1500} />`** —— 复制到剪贴板，1.5 秒内显示对勾。老浏览器自动 fallback 到 `document.execCommand('copy')`。

## 验证生效

1. `<TokenMeter inputTokens={100} outputTokens={300} />` —— 柱条看起来约 1/4 vs 3/4。
2. 顶栏挂上 `<CreditsBadge />`。登录、发一次付费调用，调完手动 `refetch()` 后数字更新。
3. 看 demo 页面 —— `chat-demo`、`image-demo`、`document-demo` —— 看这些组件在真实流程里是怎么组合的。

## 常见坑

* **Server component 引入 client 组件。** `CreditsBadge`、`CopyButton`、`ModelPicker`、`ChatList`、`RegenerateButton` 都是 `'use client'`。Server component 引它们会构建失败 —— 包一层，或者把父组件升成 client。
* **`<TokenMeter total={0} />`。** 除零有保护（`safeDenom = 1`），但柱条会撑满 100%+。要么传真实 total，要么干脆别传。
* **`<ChatList>` 没传 `trigger`。** 新消息进来不会自动滚。请传 `trigger={[messages.length, streamingText]}`。
* **改了一个忘了它的兄弟。** 这些组件共享视觉节奏（一样的 padding、字号）。如果你 fork `<LatencyPill>` 加个图标，记得同步审查 `<ProviderPill>`，否则一行就对不齐了。
* **没有美元 / 成本组件。** 我们故意不发 `CostBadge` —— 见 [可观测性](/zh/ai/observability) 的解释。如果你的产品确实要在 UI 显示美元数，自己加一列到 `ai_call` + 自己写算法 + 自己渲染。

## 官方文档

* shadcn/ui：[ui.shadcn.com](https://ui.shadcn.com/docs)
* Tailwind CSS：[tailwindcss.com](https://tailwindcss.com/docs)
* Lucide 图标：[lucide.dev](https://lucide.dev/icons)
* 源码：`src/components/ai/`、`src/components/ai/index.ts`
