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/utilscn). - Read
src/components/ai/index.tsfor 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.
<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.
Chat (2)
<ChatBubble author="user" trailing?={<CopyButton text=".." />}>...</ChatBubble> —
Single message bubble. author is 'system' | 'user' | 'assistant'; the user variant
right-aligns.
<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.
<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.
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).
<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
import { CostBadge } from '@/components/ai';in any client component, render<CostBadge microCents={42_000_000} />— should display$42.00.<TokenMeter inputTokens={100} outputTokens={300} />— bar should show roughly 1/4 vs 3/4 split.- Mount
<CreditsBadge />in your header. Sign in. Make a paid call. The number updates afterrefetch()(manually call it in your post-call effect). - 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,PromptVariableFormare'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 atriggerprop. Auto-scroll won’t fire on new messages. Passtrigger={[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
- shadcn/ui: ui.shadcn.com
- Tailwind CSS: tailwindcss.com
- Lucide icons: lucide.dev
- Source:
src/components/ai/,src/components/ai/index.ts