> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vibestrap.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# AI primitives

> 17 purely-presentational components for tokens, chat, images, and forms.

AI tool-stations have a UI vocabulary that doesn't fit generic component libraries:
token meters that update during streaming, generation cards with regenerate buttons,
image galleries with download/copy/share affordances, history rows with model badges.
Building each from scratch wastes days of styling and edge-case work.

vibestrap ships a **17-component AI primitives library** in `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/utils` `cn`).
* Read [`src/components/ai/index.ts`](https://github.com/larryworld/vibestrap/blob/main/src/components/ai/index.ts) for 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.

```tsx theme={null}
<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.

```tsx theme={null}
<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 (7)

**`<GenerationCard title?="GPT-4o-mini" actions?={...} footer?={...}>...</GenerationCard>`** —
Card chrome for any AI result: title row + content + metadata footer.

```tsx theme={null}
<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?={...} />`** — 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`.

```tsx theme={null}
if (isInsufficientCredits(r)) return <InsufficientCreditsBanner needed={r.needed} balance={r.balance} />;
```

### 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).

```tsx theme={null}
<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}
/>
```

**`<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. `<TokenMeter inputTokens={100} outputTokens={300} />` — bar should show roughly
   1/4 vs 3/4 split.
2. Mount `<CreditsBadge />` in your header. Sign in. Make a paid call. The number
   updates after `refetch()` (manually call it in your post-call effect).
3. 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` are `'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 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 `<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](/ai/observability) for why. If your product genuinely
  needs to show \$ in the UI, add a column to `ai_call` with your own pricing
  logic and render it however you like.

## Official docs

* shadcn/ui: [ui.shadcn.com](https://ui.shadcn.com/docs)
* Tailwind CSS: [tailwindcss.com](https://tailwindcss.com/docs)
* Lucide icons: [lucide.dev](https://lucide.dev/icons)
* Source: `src/components/ai/`, `src/components/ai/index.ts`
