src/components/ai/ 提供一套 17 个组件的 AI 原子组件库,每个都在
shadcn/ui 之上封了对应模式。每个都是展示型:无全局 state、无隐藏副作用、不带
Suspense。即拿即用,想换就 copy-paste 改。
前置条件
- shadcn/ui 已经接好(脚手架自带)。
- Tailwind 类名已被你的主题识别(
@/lib/utils的cn)。 - 看一遍
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 时尾巴有闪烁光标。
聊天(2 个)
<ChatBubble author="user" trailing?={<CopyButton text=".." />}>...</ChatBubble> —— 单条消息气泡。author 是 'system' | 'user' | 'assistant',user 靠右对齐。
<ChatList trigger={messages.length}>...</ChatList> —— 自动滚动容器。聪明:用户本来就贴底才会贴底跟。把每次 scroll 之间会变的东西塞 trigger。
容器(7 个)
<GenerationCard title?="GPT-4o-mini" actions?={...} footer?={...}>...</GenerationCard> —— 任何 AI 结果的卡片外壳:标题行 + 内容 + 元数据 footer。
<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 时渲染。
输入型(3 个)
<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} /> —— 模型选择下拉。hint 显示在 label 下面(用来标注速度/特性)。
<RegenerateButton onClick={fn} isLoading?={true} iconOnly?={true} /> —— 放在 GenerationCard.actions 里。loading 时图标变 spinner。
<CopyButton text="..." iconOnly?={true} resetMs?={1500} /> —— 复制到剪贴板,1.5 秒内显示对勾。老浏览器自动 fallback 到 document.execCommand('copy')。
验证生效
<TokenMeter inputTokens={100} outputTokens={300} />—— 柱条看起来约 1/4 vs 3/4。- 顶栏挂上
<CreditsBadge />。登录、发一次付费调用,调完手动refetch()后数字更新。 - 看 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—— 见 可观测性 的解释。如果你的产品确实要在 UI 显示美元数,自己加一列到ai_call+ 自己写算法 + 自己渲染。
官方文档
- shadcn/ui:ui.shadcn.com
- Tailwind CSS:tailwindcss.com
- Lucide 图标:lucide.dev
- 源码:
src/components/ai/、src/components/ai/index.ts