src/components/ai/,
each wrapping shadcn/ui with these patterns built in. Every component is purely
presentational: no global state, no hidden side effects, no Suspense boundaries.
Drop one in, replace any 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 (5)
<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 (7)
<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. Token columns render as — for image
calls (where the provider returned no token counts).
<InsufficientCreditsBanner needed={50} balance={3} topUpHref?="/pricing" /> —
Amber banner with a “Top up” CTA. Render when chat() returns INSUFFICIENT_CREDITS.
Inputs (3)
<ModelPicker models={[{ id, label, hint? }]} value={id} onChange={fn} /> —
Dropdown for selecting a model. hint shows below the label (use it for latency / model speed).
<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
<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,RegenerateButtonare'use client'. Server components that import them break the build. Wrap or promote the parent to a client component. <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
<LatencyPill>to add an icon, also audit<ProviderPill>so the row still aligns. - No dollar / cost component. We deliberately don’t ship a
CostBadge— see Observability for why. If your product genuinely needs to show $ in the UI, add a column toai_callwith your own pricing logic and render it however you like.
Official docs
- shadcn/ui: ui.shadcn.com
- Tailwind CSS: tailwindcss.com
- Lucide icons: lucide.dev
- Source:
src/components/ai/,src/components/ai/index.ts