src/components/ai/ 是一组扁平的 19 个组件。它们在 shadcn/ui 之上封了一层 AI
工作站特有的模式:成本徽章、token 柱、流式光标、生成卡、图片画廊。每个都是展示型:
无全局 state、无隐藏副作用、不带 Suspense。即拿即用,想换就 copy-paste 改。
前置条件
- shadcn/ui 已经接好(脚手架自带)。
- Tailwind 类名已被你的主题识别(
@/lib/utils的cn)。 - 看一遍
src/components/ai/index.ts的导出清单。
API 参考
展示型(6 个)
<CostBadge microCents={1234} compact?={true} /> —— 把 micro-cents 整数渲染成金额。
不到 1¢ 显示 4 位小数,≥ 1¢ 显示 2 位。
<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。
容器(8 个)
<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 形状。
<InsufficientCreditsBanner needed={50} balance={3} topUpHref?="/pricing" /> ——
琥珀色横幅,带「充值」CTA。在 chat() 返回 INSUFFICIENT_CREDITS 时渲染。
输入型(4 个)
<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} /> ——
模型选择下拉。hint 显示在 label 下面(用来标注成本/延迟)。
<PromptVariableForm variables={spec} values={vals} onChange={setVals} /> ——
按 {{variable}} 自动生成带 label 的输入框。配
usePrompt(slug).prompt.latestVersion.variables 一起用。
<RegenerateButton onClick={fn} isLoading?={true} iconOnly?={true} /> —— 放在
GenerationCard.actions 里。loading 时图标变 spinner。
<CopyButton text="..." iconOnly?={true} resetMs?={1500} /> —— 复制到剪贴板,
1.5 秒内显示对勾。老浏览器自动 fallback 到 document.execCommand('copy')。
验证生效
- 在任意客户端组件
import { CostBadge } from '@/components/ai';,渲染<CostBadge microCents={42_000_000} />,应该显示$42.00。 <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、PromptVariableForm都是'use client'。 server component 引它们会构建失败 —— 包一层,或者把父组件升成 client。 - 把 cents 当 micro-cents 传。
<CostBadge />要的是 micro-cents(百分之一美分)。 100 美分换算成 micro-cents 是 10_000_000。如果数字小了 10⁴ 倍,那就是单位错了。 <TokenMeter total={0} />。 除零有保护(safeDenom = 1),但柱条会撑满 100%+。 要么传真实 total,要么干脆别传。<ChatList>没传trigger。 新消息进来不会自动滚。请传trigger={[messages.length, streamingText]}。- 改了一个忘了它的兄弟。 这些组件共享视觉节奏(一样的 padding、字号)。如果你
fork
<CostBadge />加个图标,记得同步审查<LatencyPill>和<ProviderPill>, 否则一行就对不齐了。
官方文档
- shadcn/ui:ui.shadcn.com
- Tailwind CSS:tailwindcss.com
- Lucide 图标:lucide.dev
- 源码:
src/components/ai/、src/components/ai/index.ts