Skip to main content
The src/components/ai/ library is a flat set of 19 components. They wrap shadcn/ui primitives with the patterns specific to AI tool-stations — cost badges, token bars, streaming cursors, generation cards, image galleries. Every component is presentational: no global state, no hidden side effects, no Suspense boundaries. Drop one in, replace any of them by copy-paste.

Prerequisites

  • shadcn/ui already wired (it is, in the scaffold).
  • Tailwind classes resolved by your theme (@/lib/utils cn).
  • Read src/components/ai/index.ts for the canonical export list.

API reference

Display (6)

<CostBadge microCents={1234} compact?={true} /> — Render a money figure from a micro-cents integer. Sub-cent values get four decimals; ≥1¢ gets two.
<CostBadge microCents={result.usage.costMicroCents} />
<TokenMeter inputTokens={120} outputTokens={480} total?={4096} /> — Stacked bar of input vs output tokens. Pass total to show context-window utilization. <LatencyPill totalMs={840} ttftMs?={120} /> — Pill showing total + TTFT for streaming calls. <ProviderPill provider="openai" model?="gpt-4o-mini" /> — Color-coded provider chip; one color per AIProviderName. <CreditsBadge label?="credits" hideWhileLoading?={true} /> — Header badge that wraps useCredits(). Renders nothing for anonymous users. <StreamingText text={text} isStreaming?={true} />whitespace-pre-wrap with a blinking caret while streaming.
<StreamingText text={text} isStreaming={isStreaming} />

Chat (2)

<ChatBubble author="user" trailing?={<CopyButton text=".." />}>...</ChatBubble> — Single message bubble. author is 'system' | 'user' | 'assistant'; the user variant right-aligns.
<ChatBubble author="assistant" trailing={<CopyButton text={msg} iconOnly />}>
  {msg}
</ChatBubble>
<ChatList trigger={messages.length}>...</ChatList> — Auto-scrolling container. Smart: only sticks to the bottom when the user is already there. Pass anything that changes between scrolls into trigger.

Containers (8)

<GenerationCard title?="GPT-4o-mini" actions?={...} footer?={...}>...</GenerationCard> — Card chrome for any AI result: title row + content + metadata footer.
<GenerationCard
  title="gpt-4o-mini"
  actions={<CopyButton text={text} iconOnly />}
  footer={<><CostBadge microCents={cost} /><LatencyPill totalMs={ms} /></>}
>
  <StreamingText text={text} isStreaming={isStreaming} />
</GenerationCard>
<ImageGallery images={[{ url }]} aspect?="square" onClick?={...} /> — Responsive 1/2/3-column grid with hover download buttons. <EmptyState icon?={Icon} title="..." description?="..." action?={...} /> — Dashed-border placeholder for “no data yet” zones. <ErrorState title?="..." description?="..." onRetry?={...} action?={...} /> — Destructive-tinted error card with optional retry button. <TaskProgress status={state.status} progress?={0.4} label?="Rendering" /> — Bar + label tied to useTask TaskStatus. Indeterminate when progress is omitted. <HistoryRow row={row} /> — Single line in an ai_call history list. Takes the AICallHistoryRow shape from useHistory. <InsufficientCreditsBanner needed={50} balance={3} topUpHref?="/pricing" /> — Amber banner with a “Top up” CTA. Render when chat() returns INSUFFICIENT_CREDITS.
if (isInsufficientCredits(r)) return <InsufficientCreditsBanner needed={r.needed} balance={r.balance} />;

Inputs (4)

<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} /> — Dropdown for selecting a model. hint shows below the label (use it for cost / latency).
<ModelPicker
  models={[
    { id: 'gpt-4o-mini', label: 'GPT-4o mini', hint: 'fast · cheap' },
    { id: 'gpt-4o', label: 'GPT-4o', hint: 'flagship' },
  ]}
  value={model}
  onChange={setModel}
/>
<PromptVariableForm variables={spec} values={vals} onChange={setVals} /> — Auto-generates a labelled input per {{variable}}. Pair with usePrompt(slug).prompt.latestVersion.variables. <RegenerateButton onClick={fn} isLoading?={true} iconOnly?={true} /> — Sit it in a GenerationCard.actions slot. Spinner replaces the icon while loading. <CopyButton text="..." iconOnly?={true} resetMs?={1500} /> — Copies to clipboard, flashes a check for 1.5s. Falls back to document.execCommand('copy') on old browsers.

Verify it works

  1. import { CostBadge } from '@/components/ai'; in any client component, render <CostBadge microCents={42_000_000} /> — should display $42.00.
  2. <TokenMeter inputTokens={100} outputTokens={300} /> — bar should show roughly 1/4 vs 3/4 split.
  3. Mount <CreditsBadge /> in your header. Sign in. Make a paid call. The number updates after refetch() (manually call it in your post-call effect).
  4. Visit any of the demo pages — chat-demo, image-demo, document-demo — to see primitives composed in real flows.

Common pitfalls

  • Server components importing client primitives. CreditsBadge, CopyButton, ModelPicker, ChatList, RegenerateButton, PromptVariableForm are 'use client'. Server components that import them break the build. Wrap or promote the parent to a client component.
  • Passing cents to <CostBadge />. It expects micro-cents (1/100¢). 100 cents in micro-cents = 10_000_000. If your number looks off by a factor of 10⁴, you passed cents.
  • <TokenMeter total={0} />. Division-by-zero is guarded (safeDenom = 1), but the bar will show 100%+. Either pass a real total or omit the prop.
  • <ChatList> without a trigger prop. Auto-scroll won’t fire on new messages. Pass trigger={[messages.length, streamingText]}.
  • Replacing one but not its sibling. These primitives share visual rhythm (same paddings, font sizes). If you fork <CostBadge /> to add an icon, also audit <LatencyPill> and <ProviderPill> so the row still aligns.

Official docs