跳转到主要内容
AI 工作站有一套通用组件库不覆盖的 UI 词汇:流式更新的 token 计量、带”重新生成”按钮 的生成卡、带下载/复制/分享操作的图片画廊、带模型徽标的历史行。每个从零开始做都要 浪费几天处理样式和边界情况。 vibestrap 在 src/components/ai/ 提供一套 17 个组件的 AI 原子组件库,每个都在 shadcn/ui 之上封了对应模式。每个都是展示型:无全局 state、无隐藏副作用、不带 Suspense。即拿即用,想换就 copy-paste 改。

前置条件

  • shadcn/ui 已经接好(脚手架自带)。
  • Tailwind 类名已被你的主题识别(@/lib/utilscn)。
  • 看一遍 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 时尾巴有闪烁光标。
<StreamingText text={text} isStreaming={isStreaming} />

聊天(2 个)

<ChatBubble author="user" trailing?={<CopyButton text=".." />}>...</ChatBubble> —— 单条消息气泡。author'system' | 'user' | 'assistant',user 靠右对齐。
<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。
<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" /> —— 进度条 + 标签,绑定 useTaskTaskStatus。不传 progress 时是不确定模式。 <HistoryRow row={row} /> —— ai_call 历史列表里的一行。row 用的是 useHistoryAICallHistoryRow 形状。图像类调用没 token,这一栏会显示 <InsufficientCreditsBanner needed={50} balance={3} topUpHref?="/pricing" /> —— 琥珀色横幅,带”充值”CTA。在 chat() 返回 INSUFFICIENT_CREDITS 时渲染。
if (isInsufficientCredits(r)) return <InsufficientCreditsBanner needed={r.needed} balance={r.balance} />;

输入型(3 个)

<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} /> —— 模型选择下拉。hint 显示在 label 下面(用来标注速度/特性)。
<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-demoimage-demodocument-demo —— 看这些组件在真实流程里是怎么组合的。

常见坑

  • Server component 引入 client 组件。 CreditsBadgeCopyButtonModelPickerChatListRegenerateButton 都是 '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 + 自己写算法 + 自己渲染。

官方文档