# Bring your own data
Bring your own data [#bring-your-own-data]
LocalMode UI components are **presentational**. None of them load a model, run inference, or own message history — they render the props you pass and call back when the user acts. After `shadcn add`, the components have **no `@localmode/*` runtime dependency**: you supply the data.
This page documents the contract — the shapes each family expects — so you can wire components to a LocalMode hook, a cloud backend, or a hard-coded fixture interchangeably.
A LocalMode hook (e.g. `useChat`, `useClassify`, `useModelLoad`) is the *recommended* producer of
these shapes, but it is never required. Anything that can produce the shape below — a `fetch()`, an
AI-SDK part, a constant — drives the component identically.
Conversation [#conversation]
The chat surfaces render message and streaming state.
| Component | Key props | Shape |
| ------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Message` / `MessageContent` | `role`, `content` | `role: 'user' \| 'assistant' \| 'system'`; `content` is a markdown `string` or `MessagePart[]` (`{ type: 'text', text }` / `{ type: 'image', data, mimeType }` / `{ type: 'file', name, data, mimeType }`) |
| `Response` | `children`, `streaming` | a markdown `string` + a `boolean` |
| `PromptInput` | `onSubmit`, `streaming`, `onStop` | `onSubmit(text: string, attachments)`, a `boolean`, a `() => void` |
| `Tool` / `ToolHeader` / `ToolOutput` | `name`, `status`, `input`, `output` | a `ToolCall` (`{ name, status, input?, output?, error? }`) — `status` is one of `'pending' \| 'running' \| 'streaming' \| 'completed' \| 'error'`, with arbitrary JSON in/out |
| `Reasoning` / `ReasoningContent` | `streaming`, `children` | `streaming: boolean`; the thinking text is a `string` passed as `ReasoningContent`'s children |
| `Sources` / `Source` | `count`, `source` | `SourcesTrigger` takes `count: number`; each `Source` takes a `SourceItem` (`{ id, title, url?, score?, excerpt? }`) |
```tsx
// Static fixture — no hook, no backend:
```
See [Use with the Vercel AI SDK](/docs/use-with-ai-sdk) for a full cloud-backed example.
Results & Insights [#results--insights]
Scored-output displays render plain numbers and labels.
| Component | Shape |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `ConfidenceScoreBadge` | `score: number` (0–1), optional `thresholds` |
| `ScoredResultBarList` | `results: Array<{ label: string; score: number }>` |
| `TopResultCard` | `{ label: string; score: number }` |
| `CosineSimilarityMeter` | `similarity: number` (0–1, clamped) |
| `EntityStatsBar` | `entities: Array<{ type: string; text?; score? }>` (counts computed internally) — or a pre-computed `counts: Record` |
```tsx
// Any classifier, search ranker, or constant produces this:
```
Input Controls [#input-controls]
| Component | Shape |
| ---------------------- | -------------------------------------------------------------------------- |
| `CharLimitIndicator` | `charCount: number`, `maxLength: number` |
| `ParameterSlider` | `value: number`, `onChange`, `min`, `max` |
| `SegmentedModePicker` | `items: Array<{ id, label }>`, `selectedId`, `onSelect` |
| `LanguagePairSelector` | `languages`, `sourceCode`, `targetCode`, `onSwap`, `onSelectSource/Target` |
Media & Vision [#media--vision]
| Component | Shape |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `BoundingBoxOverlay` | `detections: Array<{ label, score?, box: { x, y, width, height } }>` + the image's `naturalWidth`/`naturalHeight` |
| `BeforeAfterImageViewer` | `originalSrc` + optional `processedSrc` (two image sources) |
| `ImageResultGallery` | `cards: Array<{ id, src, label?, score? }>` |
Data & Documents [#data--documents]
| Component | Shape |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `FileDropzone` / `MediaDropzone` | `accept`, `maxSize`, + the valid-files callback (`onUpload` on `FileDropzone`, `onFiles` on `MediaDropzone`) — validation is generic (no AI) |
| `IndexedDocumentCard` | `{ filename, chunkCount, pageCount?, sizeBytes?, … }` |
| `CategoryFacetList` | `categories: string[]` + optional `counts: Record` + `selected`/`onSelect` |
Audio [#audio]
| Component | Shape |
| ------------------------------------------ | ------------------------------------------------------------------------ |
| `WaveformActivityBars` | a `volume: number` (`0–1`) or a `state` agent-state string |
| `VoicePicker` | `voices: Array<{ id, name, gender, languageLabel }>` |
| `AudioScrubPlayer` / `TranscribedNoteCard` | `audio`: a `Blob \| string` (rendered via the copy-owned `useObjectUrl`) |
Artifacts & Canvas [#artifacts--canvas]
| Component | Shape |
| ------------------- | ---------------- |
| `DataTableArtifact` | rows + columns |
| `ChartArtifact` | a series array |
| `CodeDiffViewer` | two code strings |
Security & Privacy [#security--privacy]
| Component | Shape |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PasswordStrengthBar` | `value: number` (0–100) — the app computes it (e.g. via `@localmode/core` `deriveKey` flow, or any estimator) |
| `DifferentialPrivacyControls` | `enabled`/`onEnabledChange` + `epsilon`/`onEpsilonChange`, plus an optional `budget` object (`{ consumed, maxEpsilon }`) — all supplied by the app |
Local-First (the on-device tier) [#local-first-the-on-device-tier]
The local-first family is presentational too — it renders model/storage/capability state you pass — but it *describes* on-device AI, so it pairs naturally with the LocalMode hooks. A cloud app generally wouldn't install this family, with one exception:
* **`ContextUsageMeter`** takes the token fields (`inputTokens`, `outputTokens`, `reasoningTokens`, `cachedTokens`) and `contextWindow` directly as flat props — usable with **any** LLM's usage data, local or cloud. (The lower-level `Context` compound root instead takes a nested `usage={{ … }}` object for `ContextTrigger` / `ContextContent` / `ContextInputUsage` / `ContextOutputUsage`.)
```tsx
```
The invariant [#the-invariant]
Every non-local-first component compiles and runs with **zero `@localmode/*` packages** installed. If you ever find a component that won't, it's a bug — file it. The recommended pairing is the LocalMode hooks; the requirement is only that you pass the shape.
See also [#see-also]
* [Use with the Vercel AI SDK](/docs/use-with-ai-sdk) — a full cloud-backed chat driving these shapes from an `@ai-sdk/react` message stream.
* Live, fully-wired reference implementations in the [`/blocks` gallery](/blocks): the [chat block](/blocks/chat) drives the Conversation shapes from on-device models, and the knowledge blocks ([semantic search](/blocks/knowledge/semantic-search), [RAG chat](/blocks/knowledge/rag-chat)) drive the Results and Data & Documents shapes from your own documents. Blocks are the wiring layer, so they *do* pull the LocalMode hooks — read them as worked examples of what a producer for each shape looks like.
# Components
Components [#components]
Every component below is copy-owned and installed with the shadcn CLI under the `@localmode/ui` namespace. Click through for the live preview, install command, props, and usage.
# Device Badge
Device Badge [#device-badge]
The **Device Badge** surfaces a browser AI capability — WebGPU, WASM, or IndexedDB storage — as a themed status pill. It reads the device's capabilities via the copy-owned `useCapabilities()` hook (from the `@localmode/ui/lib/use-environment` lib, installed automatically) and shows green when the capability is available, amber when it is not, and a pulsing muted dot while detection is in flight.
**When to use it:** gate a model-download UI behind device support (e.g. "WebGPU available → offer the fast WebGPU model; otherwise fall back to WASM"), or show users why a feature is or isn't available on their device.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/device-badge`
Full source: https://localmode.ai/r/ui/device-badge.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/device-badge
```
Dependencies [#dependencies]
**Data source:** detects capabilities entirely in the browser — no backend, no model download. Capability detection is provided by the copy-owned `@localmode/ui/lib/use-environment` lib (`useCapabilities`, `detectCapabilities`), installed automatically as a registry dependency, so the component is portable to any React app.
* `@localmode/ui/lib/use-environment` — the copy-owned `useCapabilities()` hook + `detectCapabilities()` (installed automatically as a registry dependency)
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `device-badge.tsx` — the component
* `lib/use-environment.ts` — the copy-owned capability hooks (if not already present)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**DeviceBadge**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `capability` | `"storage" \| "wasm" \| "webgpu"` | `"webgpu"` | Which capability to surface. `webgpu` reports GPU acceleration availability; `wasm` reports WebAssembly support; `storage` reports IndexedDB persistence. |
| `label` | `string` | — | Label shown before the status. Defaults to a human-readable capability name. |
| `compact` | `boolean` | `false` | When true, render a compact dot-only badge without the text label. |
Examples [#examples]
Default (WebGPU) [#default-webgpu]
```tsx
import { DeviceBadge } from '@/components/device-badge';
export function Example() {
return ;
}
```
Gate a model choice [#gate-a-model-choice]
```tsx
import { DeviceBadge } from '@/components/device-badge';
import { useCapabilities } from '@/lib/use-environment';
export function ModelPicker() {
const { capabilities } = useCapabilities();
const hasWebGPU = Boolean(capabilities?.features.webgpu);
return (
);
}
```
Compact (dot only) [#compact-dot-only]
```tsx
```
Customization [#customization]
The badge is styled entirely with shadcn/ui CSS-variable utilities (`border-border`, `bg-card`, `text-card-foreground`, `text-muted-foreground`), so it inherits your theme. The status colors use Tailwind's `emerald` / `amber` palettes directly — swap those classes in the copied `device-badge.tsx` to match your design system, or wire them to your own tokens.
Because you own the file, you can also extend `DeviceBadgeProps` to surface any capability returned by `detectCapabilities()` (e.g. `sharedarraybuffer`, `webworkers`) by adding an entry to the internal capability map.
# Introduction
LocalMode UI [#localmode-ui]
**LocalMode UI** is a shadcn-style registry of copy-owned, composable AI UI primitives for the browser. It is the UI layer on top of [`@localmode/core`](https://localmode.dev/docs/core) and the [`@localmode/react`](https://localmode.dev/docs/react) hooks — chat surfaces, model-download progress, device-capability gates, and the other local-first surfaces that have no off-the-shelf equivalent.
Every component runs entirely in the browser. Inference happens on-device — no servers, no API
keys, and data never leaves the user's device.
The components are presentational — they render plain props and own no orchestration state — so
they install with **zero `@localmode/*` packages** and work with any backend. Pair them with the
LocalMode hooks (recommended) or drive them from the [Vercel AI SDK](/docs/use-with-ai-sdk) or
[your own data](/docs/bring-your-own-data).
How it works [#how-it-works]
LocalMode UI is **not** an npm package. Like shadcn/ui, you install components with the shadcn CLI and **own the copied code**. Components are styled with shadcn/ui CSS variables (`bg-background`, `text-primary`, …), so they drop into your existing shadcn project and inherit your theme automatically.
```bash
npx shadcn@latest add @localmode/ui/device-badge
```
The `@localmode/ui` namespace [#the-localmodeui-namespace]
All UI registry items live under the `ui/*` path so they stay distinct from the `@localmode/*` npm packages (core / react / providers). The item-naming scheme is:
| Pattern | Installs | Example |
| ------------------------- | ------------------ | ------------------------------------ |
| `ui/all` | The whole catalog | `@localmode/ui/all` |
| `ui/` | A whole family | `@localmode/ui/conversation` |
| `ui//` | A single component | `@localmode/ui/conversation/message` |
The shadcn namespace key is the single token `@localmode`; the `ui/` separation lives in the item name.
Next steps [#next-steps]
# Installation
Installation [#installation]
LocalMode UI components are distributed through a [shadcn registry](https://ui.shadcn.com/docs/registry). You install them with the shadcn CLI and own the copied code.
Prerequisites [#prerequisites]
You need a project with [shadcn/ui set up](https://ui.shadcn.com/docs/installation) (Tailwind CSS 4 with CSS variables and a `components.json`). LocalMode UI components are styled with the same CSS-variable tokens, so they inherit your theme.
1\. Add the `@localmode` namespace [#1-add-the-localmode-namespace]
Map the single-token `@localmode` namespace to the registry in your project's `components.json`:
```json title="components.json"
{
"registries": {
"@localmode": "https://localmode.ai/r/{name}.json"
}
}
```
For private/premium items (future), use the object form with a token:
```json title="components.json"
{
"registries": {
"@localmode": {
"url": "https://localmode.ai/r/{name}.json",
"headers": { "Authorization": "Bearer ${LOCALMODE_TOKEN}" }
}
}
}
```
2\. Install a component [#2-install-a-component]
```bash
npx shadcn@latest add @localmode/ui/device-badge
```
This copies the component's files into your project and installs any npm dependencies it declares (many components — like `device-badge` — declare none; others pull only small libraries such as `lucide-react`). No `@localmode/*` package is installed. Cross-namespace `registryDependencies` (like the shared `cn()` util) are resolved automatically.
3\. Install a whole family or everything [#3-install-a-whole-family-or-everything]
The registry ships aggregate items so you can install in bulk:
```bash
# A single component
npx shadcn@latest add @localmode/ui/device-badge
# An entire family (e.g. once the conversation family ships)
npx shadcn@latest add @localmode/ui/conversation
# The whole catalog
npx shadcn@latest add @localmode/ui/all
```
LocalMode UI components perform on-device inference. The first time a component loads its model,
it downloads from a model host (e.g. HuggingFace) and caches it. After that it works offline.
# Use with an AI agent (MCP)
Use with an AI agent (MCP) [#use-with-an-ai-agent-mcp]
LocalMode UI is a standard shadcn registry, so it works with the **shadcn MCP server out of the box** — no LocalMode-specific server to run. Once configured, an AI coding agent can search the catalog, read component descriptions, and install items into your project on request.
The shadcn MCP server reads any registry that exposes a root `registry.json`. LocalMode UI serves
its catalog at [https://localmode.ai/registry.json](https://localmode.ai/registry.json) and each item at
/r/\.json, so the MCP can discover and install everything under the
`@localmode` namespace.
1\. Add the registry to your project [#1-add-the-registry-to-your-project]
In your project's `components.json`, map the single-token `@localmode` namespace to the registry endpoint:
```json
{
"registries": {
"@localmode": "https://localmode.ai/r/{name}.json"
}
}
```
2\. Initialize the shadcn MCP for your client [#2-initialize-the-shadcn-mcp-for-your-client]
```bash
npx shadcn@latest mcp init --client claude
```
Swap `claude` for `cursor`, `vscode`, or `codex` as needed. This registers the shadcn MCP server with your agent; it can now read every registry configured in `components.json`, including `@localmode`.
3\. Ask the agent [#3-ask-the-agent]
With the MCP connected, prompt your agent in natural language. Because every item ships a precise description, the agent can resolve components by intent, not just by exact name:
* *"Add the model-download-progress component from @localmode/ui."*
* *"Install the @localmode/ui conversation family so I can build a chat."*
* *"Find a LocalMode UI component that shows a confidence score and add it."*
* *"Scaffold a local-first chat using @localmode/ui components."*
* *"Install the @localmode/ui chat block."* — composed, ready-to-run blocks live in the same catalog and install the same way (e.g. `@localmode/ui/blocks/knowledge/semantic-search`).
The agent searches the catalog, picks the matching item(s), and runs the install — copying the owned `.tsx` into your project exactly as a manual `shadcn add` would.
What the agent sees [#what-the-agent-sees]
* **The catalog**: [`/registry.json`](/registry.json) — every item with its title, description, dependencies, and `registryDependencies`.
* **Per-item JSON**: `/r/ui//.json` — the files and metadata the CLI installs.
* **Aggregates**: `@localmode/ui/all` and `@localmode/ui/` for bulk installs.
* **Composed blocks**: `@localmode/ui/blocks//` (e.g. `blocks/chat`, `blocks/knowledge/rag-chat`, `blocks/vision/object-detector`) — full, wired mini-apps in the same catalog. They're excluded from the aggregates by design, so an agent only installs one when asked for it by name.
Other agent-readable resources [#other-agent-readable-resources]
Beyond MCP, the site publishes plain-text indexes agents can ingest directly:
* [`llms.txt`](/llms.txt) — the catalog as a markdown index.
* [`llms-full.txt`](/llms-full.txt) — the full component reference.
* Every docs page is available as raw markdown via its **View as Markdown** action.
See also [#see-also]
* [Installation](/docs/installation) — manual setup and `components.json`.
* [Bring your own data](/docs/bring-your-own-data) — the prop contracts an agent-installed component renders.
# Use with the Vercel AI SDK
Use with the Vercel AI SDK [#use-with-the-vercel-ai-sdk]
LocalMode UI components are **presentational and hook-driven**: they render plain prop shapes and emit callbacks, owning no model, no inference, and no message history. That means the same conversation elements work with **any** backend — the [Vercel AI SDK](https://sdk.vercel.ai), direct OpenAI/Anthropic calls, or your own cloud API — not just LocalMode.
After installing, the components have **no `@localmode/*` dependency** to remove: nothing in the catalog imports a LocalMode package at runtime. You wire your own data source.
LocalMode UI leads with on-device inference via the `@localmode/react` hooks — that's the
recommended pairing. But the components render plain props, so a cloud backend works just as
well. This page shows the Vercel AI SDK; the same shapes apply to any source.
Install the elements [#install-the-elements]
```bash
npx shadcn@latest add @localmode/ui/conversation
```
This installs the conversation family (chat shell, message, response, prompt input, tool, reasoning, sources, branch) into your project. No LocalMode package is added.
A complete chat on `@ai-sdk/react` [#a-complete-chat-on-ai-sdkreact]
The only difference from a LocalMode chat is the logic line — `useChat` from `@ai-sdk/react` (which hits your `/api/chat` route) instead of `@localmode/react` — and a few lines mapping the SDK's message parts onto the component props.
```tsx
'use client';
import { useChat } from '@ai-sdk/react'; // cloud logic instead of @localmode/react
import {
Conversation,
ConversationContent,
} from '@/components/conversation';
import { Message, MessageAvatar, MessageContent } from '@/components/message';
import {
PromptInput,
PromptInputTextarea,
PromptInputTools,
PromptInputSubmit,
} from '@/components/prompt-input';
// Map an @ai-sdk/react message's parts onto MessageContent's `content` prop.
const textOf = (m) =>
m.parts.filter((p) => p.type === 'text').map((p) => p.text).join('');
export default function Chat() {
const { messages, status, sendMessage, stop } = useChat(); // hits /api/chat
const streaming = status === 'streaming';
return (
<>
{messages.map((m) => (
))}
sendMessage({ text })}
>
>
);
}
```
That's the whole integration: `messages` → `Message`/`MessageContent`, `status` → `streaming`, `sendMessage`/`stop` → `PromptInput`. The components never knew which backend produced the data.
`MessageContent` renders finished markdown. For a blinking cursor on the *in-flight*
assistant message, swap it for the `Response` renderer — a standalone streaming-aware
markdown component (`{textOf(m)}`), installed via
`@localmode/ui/conversation/response`.
Mapping the richer parts [#mapping-the-richer-parts]
The Vercel AI SDK emits tool invocations, reasoning, and sources as message *parts*. Each maps cleanly onto a LocalMode element.
Tool invocations → `Tool` [#tool-invocations--tool]
```tsx
import { Tool, ToolHeader, ToolContent, ToolInput, ToolOutput } from '@/components/tool';
// Inside the message map, for each part where p.type === 'tool-'.
// The tool name lives in the part's `type` (`tool-${name}`), not a `toolName` field.
const toolName = p.type.replace('tool-', '');
;
// AI SDK tool-part `state` → the component's `ToolStatus` taxonomy
function mapToolStatus(state: string) {
switch (state) {
case 'input-streaming':
return 'streaming';
case 'input-available':
return 'running';
case 'output-available':
return 'completed';
case 'output-error':
return 'error';
default:
return 'pending';
}
}
```
Reasoning parts → `Reasoning` [#reasoning-parts--reasoning]
```tsx
import { Reasoning, ReasoningTrigger, ReasoningContent } from '@/components/reasoning';
const reasoning = m.parts
.filter((p) => p.type === 'reasoning')
.map((p) => p.text)
.join('');
{reasoning && (
{reasoning}
)}
```
Source parts → `Sources` [#source-parts--sources]
```tsx
import { Sources, SourcesTrigger, SourcesContent, Source } from '@/components/sources';
const sources = m.parts.filter((p) => p.type === 'source-url');
{sources.length > 0 && (
{sources.map((s) => (
))}
)}
```
`Branch` and `ChainOfThought` map the same way — from the SDK's message alternatives and step lists onto the components' props.
What changes vs. LocalMode [#what-changes-vs-localmode]
| | LocalMode (recommended) | Vercel AI SDK |
| -------------------- | --------------------------------- | ------------------------------ |
| Logic hook | `useChat` from `@localmode/react` | `useChat` from `@ai-sdk/react` |
| Where inference runs | on-device (no server) | your API route / provider |
| Component code | **identical** | **identical** |
| Data mapping | none (shapes match the hooks) | \~3 lines per part type |
The components are the constant. Pick the logic layer that fits your app — and reach for the [local-first families](/docs/local-first) (model download, capability gates, on-device storage observability) that no cloud UI library ships.
See also [#see-also]
* [Chat block](/blocks/chat) — a live, fully-wired reference doing the local counterpart of this page: the same conversation elements driven by 76 on-device models across four providers (with vision, reasoning, and agent mode). Install it with `npx shadcn@latest add @localmode/ui/blocks/chat`.
* [Bring your own data](/docs/bring-your-own-data) — the generic prop contract and per-family shape tables, for any backend or static fixtures.
* [Conversation components](/docs/conversation) — the full family reference.
# Artifact
Artifact [#artifact]
The **Artifact** is a Claude-style docked side-panel/canvas: a vertical surface with a header (title + description), an action toolbar (copy / download / refresh / close), and a scrollable content region. It renders generated output — code, a markdown doc, an SVG, raw HTML — *beside* the chat instead of inline in the message stream.
It is composed from sub-parts: `Artifact` (root) → `ArtifactHeader` → `ArtifactTitle` / `ArtifactDescription` and `ArtifactActions` → `ArtifactAction` / `ArtifactClose`, plus `ArtifactContent` for the scrollable body.
**Bring your own content.** The shell is purely presentational — it renders whatever content you pass in, from any backend. A local language model is a recommended (optional) producer: drive it with [`useGenerateText()`](https://localmode.dev/docs/react) / [`useGenerateObject()`](https://localmode.dev/docs/react) and there is **no server, no sandbox, no remote execution.** Copy writes to the clipboard, download produces a real `Blob`, and refresh re-runs your generation — all in-browser.
**When to use it:** show a generated file/document in a dedicated canvas that the user can copy, download, or regenerate, separate from the conversational stream.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/artifacts/artifact`
Full source: https://localmode.ai/r/ui/artifacts/artifact.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/artifacts/artifact
```
Dependencies [#dependencies]
* **Data source:** the canvas renders whatever content you pass it — works with any backend. Recommended LocalMode producers: `useGenerateText()` / `useGenerateObject()` from `@localmode/react` (optional).
* `@localmode/ui/lib/browser-utils` — the copy-owned `downloadBlob` helper for artifact export (installed automatically as a registry dependency)
* `lucide-react` — toolbar icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `artifact.tsx` — the canvas shell and all sub-parts
* `lib/browser-utils.ts` — the copy-owned file helpers (if not already present)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**Artifact**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `true` | When false, the canvas is hidden (returns `null`). Lets the host toggle the docked panel open/closed. Defaults to `true`. |
ArtifactAction [#artifactaction]
**ArtifactAction**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | **Required.** Accessible label for the icon button (also used as the tooltip). |
| `icon` | `ReactNode` | — | Icon to render. Defaults are wired by convention: pass `content` to make a copy/download action, or `onClick` for a custom (e.g. refresh) action. |
| `content` | `string` | — | String content the action operates on. When provided without `fileName`, clicking copies it to the clipboard. When provided with `fileName`, clicking downloads it as a real `Blob`. Both happen entirely client-side. |
| `fileName` | `string` | — | When set (with `content`), the action downloads `content` as this file. |
| `mimeType` | `string` | `"text/plain"` | MIME type for the downloaded blob. |
ArtifactClose [#artifactclose]
**ArtifactClose**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | `"Close"` | Accessible label. |
Examples [#examples]
Drive the canvas from a local model [#drive-the-canvas-from-a-local-model]
```tsx
import { useGenerateText } from '@localmode/react';
import { transformers } from '@localmode/transformers';
import {
Artifact,
ArtifactHeader,
ArtifactTitle,
ArtifactDescription,
ArtifactActions,
ArtifactAction,
ArtifactClose,
ArtifactContent,
} from '@/components/artifacts/artifact';
export function GeneratedCodeCanvas() {
const model = transformers.languageModel('onnx-community/granite-4.0-350m-ONNX-web');
const { data, isLoading, execute } = useGenerateText({ model, maxTokens: 200 });
const code = data?.text ?? '';
return (
generated.tsFrom a local model run
{/* content only → copy to clipboard */}
{/* content + fileName → download a Blob */}
{/* onClick only → custom action (re-run) */}
execute('Write a slugify function')} label="Refresh" />
{isLoading ?
Generating…
:
{code}
}
);
}
```
Toggle the panel open/closed [#toggle-the-panel-openclosed]
```tsx
{/* … */}
```
When `open` is `false`, the canvas renders `null`. Wire `ArtifactClose`'s `onClick` to your state setter to hide it.
Toolbar action behaviors [#toolbar-action-behaviors]
`ArtifactAction` picks a built-in behavior from its props — all client-side:
| Props | Behavior |
| ------------------------ | --------------------------------------------------------- |
| `content` only | Copy `content` to the clipboard (shows a transient check) |
| `content` + `fileName` | Download `content` as a `Blob` named `fileName` |
| `onClick` (no `content`) | Run your callback (e.g. refresh re-runs generation) |
Local-first boundary [#local-first-boundary]
This is a **local canvas**. It does not embed a Sandbox, a Web Preview iframe, a Terminal, Test Results, a Commit/Package-Info panel, a File Tree, or Env Vars — those members of external artifact catalogs require remote code execution, deploys, or CI/VCS tooling and are deliberately out of scope (no server / no API key / no telemetry). See [Out of scope](#out-of-scope) below.
Out of scope [#out-of-scope]
The following server/IDE-centric artifact members from external AI-UI libraries are **refused** in LocalMode because they reintroduce a server/cloud dependency:
* **Sandbox** / **Terminal** — remote/cloud code execution.
* **Web Preview** — deployed-app iframe.
* **Test Results** / **Stack Trace** / **Commit** / **Package Info** / **File Tree** / **Env Vars** — CI/VCS/IDE dev tooling.
A live **JSXPreview** (generative-UI: rendering model-emitted JSX/JSON) is **deferred, not refused** — it is local-first-capable and high-interest, but rendering arbitrary model-emitted UI carries arbitrary-code-execution risk and needs a safe allowlist/sandbox design first. Tracked as a future change.
Customization [#customization]
The shell is styled entirely with shadcn/ui CSS-variable utilities (`bg-card`, `text-card-foreground`, `border-border`, `bg-primary`, `hover:bg-accent`), so it inherits your theme. Because you own the file, swap the `lucide-react` icons, adjust the toolbar layout, or render markdown/SVG/HTML inside `ArtifactContent` however you like.
# Chart Artifact
Chart Artifact [#chart-artifact]
The **Chart Artifact** is a client-side data-viz chart rendered from **local** data. It supports six types — `line`, `bar`, `area`, `scatter`, `radar`, `gauge` — and targets genuinely on-device metrics:
* **radar** — evaluation curves: precision / recall / F1 / accuracy from [`useEvaluateModel()`](https://localmode.dev/docs/react).
* **scatter** — 2D embedding projections (PCA / UMAP).
* **line / area** — drift-over-time ([`useReindex()`](https://localmode.dev/docs/react)), latency / tok-s trends.
* **bar** — embedding-similarity distributions, per-class counts.
* **gauge** — a single normalized score (e.g. overall F1).
It is **not** a generic BI widget — its value is visualizing what runs in the browser. The renderer is a **minimal, dependency-free inline SVG** (no charting library), so the copied component stays small and tree-shakeable. Colors come from `currentColor` + shadcn/ui tokens, so charts inherit your theme.
**Local content only.** No server, no sandbox — you pass in local numbers and it draws them.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/artifacts/chart-artifact`
Full source: https://localmode.ai/r/ui/artifacts/chart-artifact.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/artifacts/chart-artifact
```
Dependencies [#dependencies]
* **Data source:** draws the `ChartPoint[]` you pass — works with any backend (any in-memory numbers). Recommended LocalMode producers: `useEvaluateModel()` / `useReindex()` from `@localmode/react` for eval curves and drift series (optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
> No charting library is installed — the chart is rendered with inline SVG.
Files installed [#files-installed]
* `chart-artifact.tsx` — the component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ChartArtifact**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `ChartType` | — | **Required.** Chart kind. |
| `data` | `array` | — | **Required.** Data points. Shape requirements vary by `type` (see each field's docs). |
| `width` | `number` | `360` | Drawing width in px. |
| `height` | `number` | `220` | Drawing height in px. |
| `max` | `number` | — | For `gauge`: the maximum value (the dial's full-scale). Defaults to the single data point's `value` if ≤ 1 then 1, else the value itself. |
| `title` | `string` | — | Accessible label / title for the chart region. |
ChartPoint [#chartpoint]
**ChartPoint**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `x` | `number` | — | Numeric x value (line/area/scatter). |
| `y` | `number` | — | Numeric y/value. |
| `label` | `string` | — | Category label (bar/radar axes). |
| `value` | `number` | — | Numeric value (bar/radar/gauge). |
Examples [#examples]
Radar of precision / recall / F1 (from `useEvaluateModel`) [#radar-of-precision--recall--f1-from-useevaluatemodel]
```tsx
import { useEvaluateModel } from '@localmode/react';
import { ChartArtifact } from '@/components/artifacts/chart-artifact';
export function EvalRadar({ result }) {
// result is a real evaluateModel() output
const data = [
{ label: 'Precision', value: result.metrics.precision },
{ label: 'Recall', value: result.metrics.recall },
{ label: 'F1', value: result.metrics.f1 },
];
return ;
}
```
Scatter of a 2D embedding projection [#scatter-of-a-2d-embedding-projection]
```tsx
// projection = real local embeddings reduced to 2D (PCA / UMAP)
({ x, y }))}
/>
```
Line / area for drift or latency [#line--area-for-drift-or-latency]
```tsx
({ x: i, y: d }))} />
```
Gauge for a single score [#gauge-for-a-single-score]
```tsx
```
Data shapes by type [#data-shapes-by-type]
| Type | Expected `ChartPoint` fields |
| --------------------------- | ---------------------------------------------------- |
| `line` / `area` / `scatter` | `{ x, y }` |
| `bar` | `{ label, value }` |
| `radar` | `{ label, value }` (values typically 0–1) |
| `gauge` | a single `{ value }` (0–`max`, default full-scale 1) |
Local-first boundary [#local-first-boundary]
This is a **local** chart. It draws numbers your app computed on-device (evaluation metrics, embedding projections, drift, latency). It makes no network requests and pulls in no charting dependency.
Customization [#customization]
The chart uses `currentColor` for the series (defaults to `text-primary`) and shadcn/ui token classes for grid/axis text, so it inherits your theme. Because you own the file, restyle the series color, adjust `width` / `height`, add tooltips, or render the chart inside an [`Artifact`](/docs/artifacts/artifact) canvas.
# Code Diff Viewer
Code Diff Viewer [#code-diff-viewer]
The **Code Diff Viewer** shows a line-level diff of two **local** strings — additions in green, deletions in red — in either a **unified** (single-column) or **split** (side-by-side) layout. The diff is computed in-browser with a dependency-free longest-common-subsequence algorithm. There is **no ML hook and no server**.
It is the text/code analog of the image-only `BeforeAfterImageViewer`. Use it for the before/after of:
* **PII redaction** — `redactPII()` raw → redacted.
* **Translation** — `translate()` source → target.
* **Any text transform** — prompt edits, formatting, rewrites.
**Local content only.** You pass two strings; it draws the diff. Nothing leaves the device.
**When to use it:** make a text transformation legible — show exactly what your local pipeline changed.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/artifacts/code-diff-viewer`
Full source: https://localmode.ai/r/ui/artifacts/code-diff-viewer.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/artifacts/code-diff-viewer
```
Dependencies [#dependencies]
* **Data source:** diffs the two strings you pass — works with any backend; the diff itself needs no hook. Recommended LocalMode producers: `redactPII()` / `translate()` from `@localmode/core` to generate the before/after strings (optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
> No diff library is installed — the line diff is computed in-component (LCS).
Files installed [#files-installed]
* `code-diff-viewer.tsx` — the component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**CodeDiffViewer**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `original` | `string` | — | **Required.** The original ("before") string. |
| `modified` | `string` | — | **Required.** The modified ("after") string. |
| `mode` | `"split" \| "unified"` | `"unified"` | Display mode. |
| `originalLabel` | `string` | `"Original"` | Label for the original side (split header / unified caption). |
| `modifiedLabel` | `string` | `"Modified"` | Label for the modified side. |
| `showLineNumbers` | `boolean` | `true` | When false, line numbers are hidden. |
Examples [#examples]
PII-redaction before/after [#pii-redaction-beforeafter]
```tsx
import { redactPII } from '@localmode/core';
import { CodeDiffViewer } from '@/components/artifacts/code-diff-viewer';
export function RedactionDiff({ raw }: { raw: string }) {
const redacted = redactPII(raw);
return (
);
}
```
Translation source/target (split view) [#translation-sourcetarget-split-view]
```tsx
```
Unified vs split [#unified-vs-split]
```tsx
```
Local-first boundary [#local-first-boundary]
This is a **local** diff. It takes two in-memory strings and computes the diff client-side. It needs no model, makes no network request, and runs no remote tooling.
Customization [#customization]
Additions/deletions use Tailwind's `emerald` / `red` palettes; everything else is shadcn/ui CSS-variable utilities (`bg-card`, `border-border`, `text-muted-foreground`), so it inherits your theme. Because you own the file, swap the highlight colors, hide line numbers via `showLineNumbers={false}`, or render the viewer inside an [`Artifact`](/docs/artifacts/artifact) canvas.
# Data Table Artifact
Data Table Artifact [#data-table-artifact]
The **Data Table Artifact** is a docked-canvas, sortable data table for any **local** row data: VectorDB search results, a model catalog, evaluation rows, or the array output of [`generateObject()`](https://localmode.dev/docs/react). Click a column header to sort the rows ascending → descending → unsorted — all computed in-browser, never on a server.
It is distinct from the inline `ScoredResultBarList` (a message-stream primitive): this is a *docked canvas* table you place beside the chat.
**Local content only.** No server, no sandbox. Sorting reorders a copy of your rows client-side; the input array is untouched.
**When to use it:** render a structured result set the user can scan and re-sort — e.g. a model picker, retrieval results, or an extracted-records table from a `generateObject()` call.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/artifacts/data-table-artifact`
Full source: https://localmode.ai/r/ui/artifacts/data-table-artifact.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/artifacts/data-table-artifact
```
Dependencies [#dependencies]
* **Data source:** renders the rows you pass and sorts them client-side — works with any backend (any in-memory array). Recommended LocalMode producers: `useGenerateObject()` from `@localmode/react` or VectorDB search results (optional).
* `lucide-react` — sort-direction icons
* `ui/table` — the shadcn/ui table primitive (installed automatically as a registry dependency)
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `data-table-artifact.tsx` — the component (uses the `ui/table` primitive)
* `ui/table.tsx` — the shadcn/ui table primitive (registry dependency)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**DataTableArtifact**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rows` | `array` | — | **Required.** The rows to render. Sorting reorders a copy; the input array is untouched. |
| `columns` | `array` | — | Column definitions. When omitted, columns are inferred from the keys of the first row. |
| `caption` | `ReactNode` | — | Optional caption rendered under the table. |
| `initialSortKey` | `string & keyof Row` | — | Initial sort column key. |
| `initialSortDirection` | `"desc" \| "asc"` | `"asc"` | Initial sort direction. |
| `emptyMessage` | `ReactNode` | `"No data"` | Message shown when `rows` is empty. |
DataTableColumn [#datatablecolumn]
**DataTableColumn**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string & keyof Row` | — | **Required.** Key into the row object (used as the default accessor and sort key). |
| `header` | `string` | — | Header label. Defaults to `key`. |
| `cell` | `function` | — | Custom cell renderer. Defaults to `String(row[key])`. |
| `sortable` | `boolean` | `true` | When false, the column header is not clickable to sort. |
| `align` | `"center" \| "right" \| "left"` | `"left"` | Align the column's text. |
Examples [#examples]
From a `generateObject()` array [#from-a-generateobject-array]
```tsx
import { useGenerateObject, jsonSchema } from '@localmode/react';
import { z } from 'zod';
import { DataTableArtifact } from '@/components/artifacts/data-table-artifact';
const schema = jsonSchema(
z.object({ rows: z.array(z.object({ name: z.string(), score: z.number() })) }),
);
export function ExtractedTable({ model }) {
const { data, execute } = useGenerateObject({ model, schema });
const rows = data?.object.rows ?? [];
return (
);
}
```
From VectorDB search results [#from-vectordb-search-results]
```tsx
({ id: r.id, score: r.score, title: r.metadata.title }))}
initialSortKey="score"
initialSortDirection="desc"
columns={[
{ key: 'title', header: 'Title' },
{ key: 'score', header: 'Similarity', align: 'right', cell: (r) => r.score.toFixed(3) },
{ key: 'id', header: 'ID' },
]}
/>
```
Inferred columns [#inferred-columns]
Omit `columns` and the table infers them from the first row's keys:
```tsx
```
Sorting [#sorting]
Clicking a sortable header cycles **ascending → descending → unsorted**. Numbers, strings (natural/numeric-aware), booleans, and `Date` values are compared correctly; `null`/`undefined` sort first. Set `sortable: false` on a column to disable it, and `initialSortKey` / `initialSortDirection` to start pre-sorted.
Local-first boundary [#local-first-boundary]
This is a **local** table. It renders data your app already has in memory (VectorDB results, model catalogs, `generateObject()` output) and sorts it client-side. It does not fetch from a server or run remote queries.
Customization [#customization]
Styled with shadcn/ui CSS-variable utilities via the `ui/table` primitive, so it inherits your theme. Because you own the file, provide a custom `cell` renderer per column (badges, links, formatted numbers), adjust alignment, or wrap it inside an [`Artifact`](/docs/artifacts/artifact) canvas.
# Audio Scrub Player
Audio Scrub Player [#audio-scrub-player]
**Audio Scrub Player** is a composable scrubbable player for local `Blob` / object-URL audio such as Kokoro TTS output ([`useSynthesizeSpeech`](https://localmode.dev/docs/react)) or a recording. It provides play/pause, a draggable seek bar, and a time/duration readout, and manages its own `
}
>
```
Gate a media surface (camera / microphone) [#gate-a-media-surface-camera--microphone]
The `camera` and `microphone` capabilities gate on media-input **availability** — a secure context, `navigator.mediaDevices.getUserMedia` present, and `enumerateDevices()` reporting at least one device of that kind. The detection never prompts the user. Runtime **permission denial** is deliberately not gated: request permission from your own start action and surface a `getUserMedia` rejection as a recoverable error with retry.
```tsx
No camera detected on this device.}
>
{/* calls getUserMedia on its own Start action */}
```
Customization [#customization]
The default fallback and pending slots are themed amber/muted notices — pass your own `fallback`/`pending` nodes for full control. The `requires` union covers `webgpu`, `wasm`, `webnn`, `simd`, `threads`, `indexeddb`, `webworkers`, `sharedarraybuffer`, and the media-availability entries `camera` and `microphone`; add more by extending the `LABELS` map. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Chrome AI Download Gate
Chrome AI Download Gate [#chrome-ai-download-gate]
Chrome's built-in AI APIs (`Summarizer`, `Translator`, `LanguageModel`) can exist in a browser while their on-device model has not been fetched yet. In that state `availability()` reports `'downloadable'`, and **Chrome refuses to start the download outside a user activation**. A button is therefore not a nicety here — it is the only way to trigger the download.
The **Chrome AI Download Gate** renders that button, the in-flight progress bar, the failed-attempt message, and the two terminal states (the API is absent, or this device cannot run it). It renders `null` once the model is `'available'`, because a ready capability needs no gate. **Chrome AI Ready Badge** is a compact confirmation you can show in its place.
It is presentational and hook-driven: bind `availability`, `progress`, and `isDownloading` to `useProviderFallback`'s Chrome state and pass its download action as `onDownload`. It owns no orchestration and starts nothing itself.
The download is Chrome's, not your app's: it is shared across every site and happens once. Gemini Nano (the Prompt API) is roughly 1.5 GB; the Translator downloads a smaller pack **per language pair**, so availability must be re-probed whenever the pair changes.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/chrome-ai-download-gate`
Full source: https://localmode.ai/r/ui/local-first/chrome-ai-download-gate.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/chrome-ai-download-gate
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the `availability` you pass — works with any backend. Recommended producer: `useProviderFallback` from `@localmode/react` (on-device, optional), whose `chromeAvailability`, `chromeDownloadProgress`, `downloadingCapability`, and `requestChromeDownload` map one-to-one onto these props.
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util
Files installed [#files-installed]
* `chrome-ai-download-gate.tsx` — `ChromeAIDownloadGate` + `ChromeAIReadyBadge`
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ChromeAIDownloadGate**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `availability` | `ChromeAvailabilityState` | — | **Required.** Chrome's reported state for the capability this gate guards. |
| `label` | `string` | — | **Required.** Human-readable capability name (e.g. "Summarizer", "Gemini Nano"). |
| `size` | `string` | — | Approximate download size (e.g. "~1.5 GB"). Shown on the prompt. |
| `onDownload` | `function` | — | **Required.** Called when the user clicks Download. Chrome only starts the download from a user activation, so this MUST be wired to a real click — it cannot be triggered on mount. |
| `isDownloading` | `boolean` | — | True while the download is in flight. |
| `progress` | `number` | — | Download completion as a 0–1 fraction. Renders an indeterminate bar when omitted. |
| `error` | `string \| null` | — | Message shown when the last download attempt failed. |
| `fallbackLabel` | `string` | — | What the app will use instead while the model is missing (e.g. "Transformers.js"). |
**ChromeAIReadyBadge**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | — | **Required.** Capability name shown in the badge. |
Examples [#examples]
Wired to `useProviderFallback` [#wired-to-useproviderfallback]
`requestChromeDownload` must be called from the click handler — Chrome will reject a download started from an effect or on mount.
```tsx
import { useEffect } from 'react';
import { useProviderFallback } from '@localmode/react';
import { ChromeAIDownloadGate } from '@/components/chrome-ai-download-gate';
export function SummarizePanel() {
const {
chromeAvailability,
refreshChromeAvailability,
requestChromeDownload,
chromeDownloadProgress,
downloadingCapability,
error,
} = useProviderFallback({
loadChromeAI: () => import('@localmode/chrome-ai'),
loadTransformers: () => import('@localmode/transformers'),
});
// Reading availability() downloads nothing.
useEffect(() => {
void refreshChromeAvailability('summarize', { chromeStyle: 'tldr', length: 'medium' });
}, [refreshChromeAvailability]);
return (
{
void requestChromeDownload('summarize', { chromeStyle: 'tldr', length: 'medium' });
}}
/>
);
}
```
Per-language-pair gating [#per-language-pair-gating]
The Translator downloads one pack per directed pair, so re-probe on every pair change and name the pair in the label.
```tsx
useEffect(() => {
void refreshChromeAvailability('translate', { source, target });
}, [source, target, refreshChromeAvailability]);
void requestChromeDownload('translate', { source, target })}
fallbackLabel="Transformers.js (Opus-MT)"
/>;
```
Showing a ready state [#showing-a-ready-state]
The gate renders `null` when the model is `'available'`, so pair it with the badge:
```tsx
{chromeAvailability.summarize === 'available' ? (
) : (
)}
```
States [#states]
| `availability` | What renders |
| -------------- | -------------------------------------------------------------------------- |
| `available` | Nothing (`null`) — pair with `ChromeAIReadyBadge` |
| `downloadable` | Heading, size copy, and the **Download model** button |
| `downloading` | Heading and a live progress bar (indeterminate when `progress` is omitted) |
| `unavailable` | A terminal message: this device cannot run the model |
| `unsupported` | A terminal message: this browser has no such API |
Customization [#customization]
The button's accessible name is deliberately the generic **"Download model"** rather than a name built from the capability label. A name like "Download Chrome Summarizer" contains the substring "Summarize", which would collide with a host block's own **Summarize** run button under role+name lookups and resolve two controls. The capability is named in the heading above the button instead. If you rename the button, keep it distinct from the run controls around it.
The progress bar is a plain `role="progressbar"` with `aria-valuenow`, and the status line is a `role="status"` live region, so screen readers announce the download without extra wiring.
# Context Usage Meter
Context Usage Meter [#context-usage-meter]
The **Context Usage Meter** is a token / context-window budget meter for local generation: a bar breaking down input / output / reasoning / cache token usage against the model's context-window limit (a hard local GGUF/LiteRT KV-cache constraint), warning as it approaches the limit. Feed it the `usage` from a generate result — or `useChat`'s per-turn `usage` / cumulative `totalUsage` (`{ inputTokens, outputTokens, totalTokens, durationMs }`). It is **local-only — there is no cost field**, and it complements the Storage Meter (disk) with a token-budget gauge. For a composable hovercard layout, use the lower-level `Context` / `ContextTrigger` / `ContextContent` / `ContextInputUsage` / `ContextOutputUsage` parts.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/context-usage-meter`
Full source: https://localmode.ai/r/ui/local-first/context-usage-meter.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/context-usage-meter
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the `usage` shape you pass — works with any backend. Recommended producer: `useChat`'s `usage` (last turn) / `totalUsage` (cumulative), or a `useGenerateText` result's `usage`, from `@localmode/react` (on-device, optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `context-usage-meter.tsx` — `ContextUsageMeter` + `Context` / `ContextTrigger` / `ContextContent` / `ContextInputUsage` / `ContextOutputUsage`
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ContextUsageMeter**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `contextWindow` | `number` | — | **Required.** The model's context-window limit in tokens. |
| `warnThreshold` | `number` | `0.85` | Fraction (0–1) at which the meter warns. |
| `inputTokens` | `number` | — | Prompt / input tokens. |
| `outputTokens` | `number` | — | Generated / output tokens. |
| `reasoningTokens` | `number` | — | Reasoning ("thinking") tokens, if the model emits them. |
| `cachedTokens` | `number` | — | Tokens served from the prompt cache. |
**Context**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `usage` | `object` | — | **Required.** Token usage breakdown (e.g. from a generate result's `usage`). |
| `contextWindow` | `number` | — | **Required.** The model's context-window limit in tokens. |
| `warnThreshold` | `number` | `0.85` | Fraction (0–1) at which the meter warns. |
| `children` | `ReactNode` | — | **Required.** Compound children (`ContextTrigger`, `ContextContent`, …). |
Examples [#examples]
Simple meter [#simple-meter]
```tsx
```
Wired to `useChat` usage [#wired-to-usechat-usage]
```tsx
import { useChat } from '@localmode/react';
import { ContextUsageMeter } from '@/components/context-usage-meter';
export function ChatBudget({ model }) {
const { totalUsage } = useChat({ model });
return (
);
}
```
`useChat` exposes `usage` (the last completed turn) and `totalUsage` (a running sum across the conversation) — use `totalUsage` for a context-budget gauge and `usage` for a per-turn readout.
Composable parts [#composable-parts]
```tsx
```
Customization [#customization]
The warn state (amber) triggers past `warnThreshold` (default `0.85`). There is deliberately **no cost field** — local inference has no per-token billing. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Device Capability Grid
Device Capability Grid [#device-capability-grid]
The **Device Capability Grid** is the expanded sibling of the Device Badge: a full diagnostic card reading `useCapabilities`. It renders a stats bar (CPU cores / memory / GPU), a status row per feature flag (WebGPU, WASM, SIMD, Threads, IndexedDB, Web Workers), a storage row, and a browser/OS footer, with a spinner while detection runs. It does not replace the Device Badge.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/device-capability-grid`
Full source: https://localmode.ai/r/ui/local-first/device-capability-grid.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/device-capability-grid
```
Data source & dependencies [#data-source--dependencies]
**Data source:** detects device capabilities via the bundled `useCapabilities` navigator hook — self-contained, works in any React app, no backend required. (It mirrors the detection `useCapabilities` from `@localmode/react` performs.)
* `@localmode/ui/lib/use-environment` — the bundled `useCapabilities` navigator hook (installed automatically as a registry dependency)
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `device-capability-grid.tsx` — the `DeviceCapabilityGrid` component
* `lib/use-environment.ts` — the bundled `useCapabilities` navigator hook
* `lib/utils.ts` — the `cn()` helper (if not already present)
Examples [#examples]
Full diagnostic [#full-diagnostic]
```tsx
```
Customization [#customization]
Each feature row shows a colored dot (emerald when on, muted when off). Add or remove flags by editing the `FEATURES` array in the copied file. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Embedding Drift Banner
Embedding Drift Banner [#embedding-drift-banner]
The **Embedding Drift Banner** is a warning panel shown when the active embedding model is incompatible with vectors already stored (the model changed or dimensions mismatch). It explains the stored-vs-current model ids, shows a reindex progress bar + phase label while re-embedding, and offers "Re-embed All" / Cancel. Bind to `useReindex` + your compatibility check.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/embedding-drift-banner`
Full source: https://localmode.ai/r/ui/local-first/embedding-drift-banner.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/embedding-drift-banner
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the drift/reindex state you pass and emits Re-embed/Cancel — works with any backend. Recommended producer: `useReindex` from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `embedding-drift-banner.tsx` — the `EmbeddingDriftBanner` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**EmbeddingDriftBanner**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `storedModelId` | `string` | — | **Required.** Model id used for the vectors already stored. |
| `currentModelId` | `string` | — | **Required.** The active embedding model id. |
| `isReindexing` | `boolean` | — | Whether a reindex is currently running. |
| `progress` | `object \| null` | — | Live reindex progress (null when not started). |
| `onReindex` | `function` | — | Fired when the user starts a re-embed. |
| `onCancel` | `function` | — | Fired when the user cancels a running reindex. |
**ReindexProgressLike**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `completed` | `number` | — | **Required.** Documents processed so far. |
| `total` | `number` | — | **Required.** Total documents to process. |
| `skipped` | `number` | — | Documents skipped (no text to re-embed). |
| `phase` | `"indexing" \| "embedding"` | — | **Required.** Current phase. |
Examples [#examples]
Bound to useReindex [#bound-to-usereindex]
```tsx
const { isReindexing, progress, reindex, cancel } = useReindex({ db, model });
```
Customization [#customization]
The banner uses the `amber` palette to read as a warning. The phase label maps `useReindex` phases (`embedding` / `indexing`) to human strings in `PHASE_LABEL`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Catalog Card
Model Catalog Card [#model-catalog-card]
The **Model Catalog Card** is a rich tile for a single model catalog entry (wllama / webllm / transformers / litert / mediapipe shapes): name, a size badge, an optional description, a metadata chip row (architecture, params, quantization, context), and a capability sub-row (tools / vision / embedding / reranking / reasoning) rendered conditionally from the entry's flags. It complements the Model Selector (a picker) — this is a presentation tile. It fires `onClick(id)` and supports a selected ring.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-catalog-card`
Full source: https://localmode.ai/r/ui/local-first/model-catalog-card.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-catalog-card
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the catalog entry you pass — works with any backend. Recommended producer: `useModelRecommendations` / your registry data from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-catalog-card.tsx` — the `ModelCatalogCard` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelCatalogCard**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `entry` | `object` | — | **Required.** The catalog entry to render. |
| `selected` | `boolean` | — | Whether this card is the selected one (adds an accent ring). |
| `onClick` | `function` | — | Fired with the entry id when the card is clicked. |
**CatalogEntry**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | **Required.** Stable model id (passed to `onClick`). |
| `name` | `string` | — | **Required.** Display name. |
| `size` | `string` | — | Human-readable size (e.g. "1.2 GB"). |
| `description` | `string` | — | One-line description. |
| `architecture` | `string` | — | Architecture (e.g. "llama"). |
| `parameters` | `string` | — | Parameter count label (e.g. "1.2B"). |
| `quantization` | `string` | — | Quantization label (e.g. "Q4_K_M"). |
| `contextLength` | `number` | — | Context window in tokens. |
| `tools` | `boolean` | — | Capability flags. |
| `vision` | `boolean` | — | — |
| `embedding` | `boolean` | — | — |
| `reranking` | `boolean` | — | — |
| `reasoning` | `boolean` | — | — |
Examples [#examples]
A selectable tile [#a-selectable-tile]
```tsx
```
Customization [#customization]
Capability badges render only for the flags present on the entry — edit the `CAPABILITIES` array to add more (e.g. audio). The selected state adds a `ring-1 ring-primary`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Comparison Panel
Model Comparison Panel [#model-comparison-panel]
The **Model Comparison Panel** is a side-by-side comparison of two scored entries with labeled rows (Score, Size, Speed, Quality, Device, Dimensions) where winning rows are accent-highlighted and losing rows dimmed, plus an `onClear` dismissal. Win-highlighting is derived purely from props (no orchestration state). Bind to `useModelRecommendations`.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-comparison-panel`
Full source: https://localmode.ai/r/ui/local-first/model-comparison-panel.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-comparison-panel
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the two scored entries you pass and emits `onClear` — works with any backend. Recommended producer: `useModelRecommendations` from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-comparison-panel.tsx` — the `ModelComparisonPanel` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelComparisonPanel**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `entries` | `[object, object]` | — | **Required.** The two entries to compare. |
| `onClear` | `function` | — | Fired when the comparison is dismissed. |
**ComparisonEntry**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `modelId` | `string` | — | **Required.** Stable model id. |
| `name` | `string` | — | **Required.** Display name. |
| `score` | `number` | — | **Required.** Score (0–100). |
| `sizeMB` | `number` | — | Size in MB (numeric, for "smaller is better" comparison). |
| `size` | `string` | — | Human-readable size label (display). |
| `speedTier` | `"slow" \| "medium" \| "fast"` | — | Speed tier. |
| `qualityTier` | `"high" \| "low" \| "medium"` | — | Quality tier. |
| `device` | `"cpu" \| "wasm" \| "webgpu"` | — | Recommended device. |
| `dimensions` | `number` | — | Embedding dimensions. |
Examples [#examples]
Compare two models [#compare-two-models]
```tsx
setCompare(null)} />
```
Customization [#customization]
"Better" is higher for Score/Speed/Quality/Dimensions and lower for Size. The win highlight uses the `emerald` palette; the loser is dimmed. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Downloader
Model Downloader [#model-downloader]
The **Model Downloader** is the single most defining LocalMode surface: the card a user sees while their model loads on **their** device. It renders the model name, size, context length, and category alongside a live progress bar, and clearly distinguishes a first-time download ("Downloading…") from a cache load ("Loading from cache…") and a ready state. A lower-level **Download Progress** renders just the bar + percentage.
It is presentational and hook-driven — bind `progress` to `useModelLoad`'s `progressValue` (its `percent` is a 0–1 fraction matching this component's `DownloadProgressValue` contract, with `loaded` / `total` bytes and a `cached` flag when known) and pass metadata from `useModelRecommendations` or your catalog. It does **not** initiate or own the download — `useModelLoad().load()` does.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-downloader`
Full source: https://localmode.ai/r/ui/local-first/model-downloader.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-downloader
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the `progress` + metadata you pass — works with any backend. Recommended producer: `useModelLoad` (provider-model loads; `useModelStatus` for a read-only view, `useModelLoader` for raw `createModelLoader` file downloads) from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util
Files installed [#files-installed]
* `model-downloader.tsx` — `ModelDownloader` + `DownloadProgress`
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelDownloader**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | **Required.** Display name of the model (e.g. "Llama 3.2 1B Instruct"). |
| `size` | `string` | — | Human-readable download size (e.g. "1.2 GB"). |
| `contextLength` | `number` | — | Context window length in tokens (e.g. 8192). |
| `category` | `string` | — | Category / family label (e.g. "Chat", "Vision"). |
| `progress` | `object \| number` | — | **Required.** Progress value (a 0–1 fraction, or a {@link DownloadProgressValue}). When the value reports `cached`, the copy switches to a cache-load message. |
| `cached` | `boolean` | — | Whether the model is being loaded from cache (first-time download vs cache). Overrides `progress.cached` when set explicitly. |
| `ready` | `boolean` | — | Whether the model has finished loading and is ready for inference. |
**DownloadProgress**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `object \| number` | — | **Required.** Progress value (a 0–1 fraction, or a {@link DownloadProgressValue}). |
| `complete` | `boolean` | — | Render the completed appearance: a full emerald bar with a "Ready" label instead of a percentage. When omitted, completion is inferred at 100%. |
Examples [#examples]
Bound to `useModelLoad` [#bound-to-usemodelload]
```tsx
import { useModelLoad } from '@localmode/react';
import { wllama, isModelCached } from '@localmode/wllama';
import { ModelDownloader } from '@/components/model-downloader';
export function Loading() {
const { status, progressValue } = useModelLoad({
key: 'Llama-3.2-1B-Instruct-Q4_K_M',
create: (onProgress) =>
wllama.languageModel('Llama-3.2-1B-Instruct-Q4_K_M', { onProgress }),
isCached: () => isModelCached('Llama-3.2-1B-Instruct-Q4_K_M'),
autoLoad: true,
});
return (
);
}
```
`useModelLoad` normalizes every provider's `onProgress` shape (transformers per-file, webllm percent, wllama/litert bytes) into one `progressValue` — `{ loaded?, total?, percent, cached? }` with `percent` as a 0–1 fraction — which drops straight into this component's `progress` prop. Its `cached` flag (from the `isCached` probe) switches the copy to "Loading from cache…" automatically.
Standalone progress bar [#standalone-progress-bar]
```tsx
```
Customization [#customization]
Styled entirely with shadcn/ui CSS variables (`bg-card`, `text-card-foreground`, `bg-primary`), so it inherits your theme. The cached state uses Tailwind's `emerald` palette — swap those classes in the copied file to match your design system. The progress shape accepts either a `0–1` fraction or a `{ loaded, total, percent, cached }` object, so it adapts to any provider's `onProgress` — `useModelLoad`'s `progressValue` matches this object shape exactly.
# Model Loading Panel
Model Loading Panel [#model-loading-panel]
The **Model Loading Panel** is the richer, blocking sibling of the Model Downloader: a full-height "waiting for model" splash that combines model metadata (size, context length, category, a cached-vs-downloading badge) with a progress bar and a two-path help message (first-download vs cache-load). It composes the lower-level `DownloadProgress` and binds to `useModelLoad` (`progressValue`, `cached`, `status`).
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-loading-panel`
Full source: https://localmode.ai/r/ui/local-first/model-loading-panel.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-loading-panel
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the load state + metadata you pass — works with any backend. Recommended producer: `useModelLoad` (drive the load) / `useModelStatus` (read-only view of the same lifecycle) from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Registry dependencies [#registry-dependencies]
* `@localmode/ui/local-first/model-downloader` — composes `DownloadProgress`
Files installed [#files-installed]
* `model-loading-panel.tsx` — the `ModelLoadingPanel` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelLoadingPanel**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | — | **Required.** Model display name. |
| `size` | `string` | — | Human-readable size. |
| `contextLength` | `number` | — | Context window in tokens. |
| `category` | `string` | — | Category / family label. |
| `progress` | `object \| number` | — | **Required.** Progress value (0–1 fraction or {@link DownloadProgressValue}). |
| `cached` | `boolean` | — | Whether the model is loading from cache rather than downloading fresh. |
Examples [#examples]
Blocking load state [#blocking-load-state]
```tsx
import { useModelLoad } from '@localmode/react';
import { webllm, isModelCached } from '@localmode/webllm';
import { ModelLoadingPanel } from '@/components/model-loading-panel';
export function Gate({ children }) {
const { status, progressValue } = useModelLoad({
key: 'Llama-3.2-1B-Instruct-q4f16_1-MLC',
create: (onProgress) =>
webllm.languageModel('Llama-3.2-1B-Instruct-q4f16_1-MLC', { onProgress }),
isCached: () => isModelCached('Llama-3.2-1B-Instruct-q4f16_1-MLC'),
autoLoad: true,
});
if (status !== 'ready') {
return ;
}
return children;
}
```
`progressValue` (`{ loaded?, total?, percent, cached? }`, `percent` 0–1) drops straight into the panel's `progress` prop, and its `cached` flag (from the `isCached` probe) flips the help copy between the first-download and cache-load paths.
Customization [#customization]
The help copy switches on the `cached` flag (or `progress.cached` — set automatically when you pass `useModelLoad`'s `progressValue`). It reuses `DownloadProgress` from the Model Downloader item, which the CLI installs as a registry dependency. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Metadata Card
Model Metadata Card [#model-metadata-card]
The **Model Metadata Card** (`GGUFMetadataCard`, with a `ModelMetadataCard` alias) is a structured key-value grid of parsed model metadata — architecture, parameter count, quantization, context length, embedding dimension, vocab size, head/layer counts, file size, and optional author/license — driven by a field-descriptor array so absent fields are skipped cleanly. Feed it `parseGGUFMetadata()` output from `@localmode/wllama` (or any object matching the `ModelMetadata` shape); the display is a generic metadata grid.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-metadata-card`
Full source: https://localmode.ai/r/ui/local-first/model-metadata-card.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-metadata-card
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders any object matching the `ModelMetadata` shape you pass — works with any backend. Recommended producer: `parseGGUFMetadata()` from `@localmode/wllama` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-metadata-card.tsx` — `GGUFMetadataCard` (alias `ModelMetadataCard`)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**GGUFMetadataCard**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `metadata` | `object` | — | **Required.** Parsed metadata fields. Absent optional fields are omitted from the grid. |
| `title` | `string` | `"Model metadata"` | Optional title. |
**ModelMetadata**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `architecture` | `string` | — | Architecture (e.g. "llama"). |
| `parameters` | `number \| string` | — | Parameter count (raw number or label). |
| `quantization` | `string` | — | Quantization scheme (e.g. "Q4_K_M"). |
| `contextLength` | `number` | — | Context length in tokens. |
| `embeddingDimension` | `number` | — | Embedding dimension. |
| `vocabSize` | `number` | — | Vocabulary size. |
| `headCount` | `number` | — | Number of attention heads. |
| `layerCount` | `number` | — | Number of transformer layers / blocks. |
| `fileSizeBytes` | `number` | — | File size in bytes. |
| `author` | `string` | — | Optional author / organization. |
| `license` | `string` | — | Optional license identifier. |
Examples [#examples]
Parsed GGUF metadata [#parsed-gguf-metadata]
```tsx
import { GGUFMetadataCard } from '@/components/model-metadata-card';
import { parseGGUFMetadata } from '@localmode/wllama';
const metadata = await parseGGUFMetadata(modelUrl);
```
Customization [#customization]
Field formatting (bytes, large numbers) lives in the `FIELDS` descriptor array — add fields or change labels there. Parameters/file size are auto-formatted. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Recommendation Card
Model Recommendation Card [#model-recommendation-card]
The **Model Recommendation Card** renders a single scored recommendation: a radial score dial (0–100), the model name, a monospace model id, a badge row (provider, size, speed/quality tiers, recommended device with a stable tier→color mapping), a description, and reason chips. When `onToggleCompare` is supplied it exposes a compare toggle. Bind to `useModelRecommendations`.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-recommendation-card`
Full source: https://localmode.ai/r/ui/local-first/model-recommendation-card.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-recommendation-card
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the scored recommendation you pass and emits the compare toggle — works with any backend. Recommended producer: `useModelRecommendations` from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-recommendation-card.tsx` — the `ModelRecommendationCard` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelRecommendationCard**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `recommendation` | `object` | — | **Required.** The recommendation to render. |
| `comparing` | `boolean` | — | Whether this card is selected for comparison. |
| `onToggleCompare` | `function` | — | When provided, shows a compare toggle that calls back with the model id. |
**ModelRecommendation**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `modelId` | `string` | — | **Required.** Stable model id. |
| `name` | `string` | — | **Required.** Display name. |
| `provider` | `string` | — | Provider (e.g. "transformers", "wllama"). |
| `size` | `string` | — | Human-readable size. |
| `score` | `number` | — | **Required.** Score in the 0–100 range. |
| `recommendedDevice` | `"cpu" \| "wasm" \| "webgpu"` | — | Recommended runtime device. |
| `speedTier` | `"slow" \| "medium" \| "fast"` | — | Speed tier. |
| `qualityTier` | `"high" \| "low" \| "medium"` | — | Quality tier. |
| `description` | `string` | — | One-line description. |
| `reasons` | `array` | — | Reasons the model was recommended. |
Examples [#examples]
Bound to recommendations [#bound-to-recommendations]
```tsx
const { recommendations } = useModelRecommendations({ task: 'embedding' });
{recommendations.map((r) => (
))}
```
Customization [#customization]
The score dial color steps green/amber/rose by score; tier badge colors come from the `TIER_COLOR` map. Pass `onToggleCompare` to reveal the compare control. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Search Browser
Model Search Browser [#model-search-browser]
The **Model Search Browser** is a cmdk-based searchable model-repo browser: a controlled search input, a sort selector (downloads / likes / last-modified), result rows (repo id, author, compact download/like counts, relative last-modified, capped tag badges), load-more pagination, and a per-repo expandable file list (filename, quantization badge, human-readable size) with per-file select actions — plus distinct loading, empty, and error-with-retry states.
It is purely presentational: every value arrives via props and every action leaves via a callback, so **any backend can feed it** — the HuggingFace Hub API, a private model registry, or static fixtures. The consumer owns all fetching, debouncing, and pagination state; cmdk's built-in filtering is disabled because results arrive pre-filtered from your backend.
**When to use it:** browsing a large remote model catalog (e.g. the 160K+ GGUF repos on HuggingFace) down to a concrete file the user can load.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-search-browser`
Full source: https://localmode.ai/r/ui/local-first/model-search-browser.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-search-browser
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the `results` and `files` you pass and emits every action via callbacks — works with any backend. Recommended wiring: the HuggingFace Hub API (see the example below); a private registry endpoint or a static catalog works identically (optional).
* `cmdk` — the underlying command/list primitive
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-search-browser.tsx` — the `ModelSearchBrowser` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelSearchBrowser**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `query` | `string` | — | **Required.** The controlled search query. |
| `onQueryChange` | `function` | — | **Required.** Fired with the new query on every keystroke. |
| `sort` | `ModelSearchSort` | — | **Required.** The active sort order. |
| `onSortChange` | `function` | — | **Required.** Fired with the chosen sort order. |
| `results` | `array` | — | **Required.** The result rows to render (already searched/sorted by the backend). |
| `isLoading` | `boolean` | — | Whether a search (or the next page) is in flight — renders skeleton rows. |
| `error` | `string \| null` | — | Error message for the failed search — renders the error state. |
| `onRetry` | `function` | — | Fired by the error state's retry affordance. |
| `hasMore` | `boolean` | — | Whether more pages exist — renders the load-more affordance. |
| `onLoadMore` | `function` | — | Fired when the user asks for the next page. |
| `expandedRepoId` | `string \| null` | — | The repo id whose file list is expanded (null/undefined: none). |
| `onSelectRepo` | `function` | — | Fired with the clicked repo id, or null when the expanded row is collapsed. |
| `files` | `array \| null` | — | Files of the expanded repo (null while not yet available). |
| `filesLoading` | `boolean` | — | Whether the expanded repo's file list is loading. |
| `filesError` | `string \| null` | — | Error message for the failed file listing. |
| `onSelectFile` | `function` | — | Fired when the user picks a file inside the expanded repo. |
**ModelSearchResult**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `repoId` | `string` | — | **Required.** Stable repo id (e.g. "bartowski/Llama-3.2-1B-Instruct-GGUF"). |
| `author` | `string` | — | Repo author/organization. |
| `downloads` | `number` | — | Total download count (rendered compactly, e.g. "1.2M"). |
| `likes` | `number` | — | Like count (rendered compactly). |
| `lastModified` | `string` | — | Last-modified ISO timestamp (rendered as a relative time). |
| `tags` | `array` | — | Repo tags (capped with an overflow count). |
**ModelRepoFile**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `filename` | `string` | — | **Required.** File name (e.g. "Llama-3.2-1B-Instruct-Q4_K_M.gguf"). |
| `sizeBytes` | `number` | — | File size in bytes (rendered human-readable). |
| `quantLabel` | `string` | — | Quantization label badge (e.g. "Q4_K_M"). |
Examples [#examples]
Fixture-driven (any backend) [#fixture-driven-any-backend]
The component owns no fetching — hand it rows from anywhere:
```tsx
pick(`${repoId}:${file.filename}`)}
/>
```
Wiring to the HuggingFace API [#wiring-to-the-huggingface-api]
The component doesn't care where results come from — this recipe feeds it from the public HuggingFace Hub API, but swapping the two `fetch` calls for your own registry endpoint changes nothing else:
```tsx
import { useEffect, useState } from 'react';
import {
ModelSearchBrowser,
type ModelRepoFile,
type ModelSearchResult,
type ModelSearchSort,
} from '@/components/model-search-browser';
export function GGUFBrowser() {
const [query, setQuery] = useState('');
const [sort, setSort] = useState('downloads');
const [results, setResults] = useState([]);
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [expanded, setExpanded] = useState(null);
const [files, setFiles] = useState(null);
// Debounced, abortable search — the consumer owns all orchestration.
useEffect(() => {
const controller = new AbortController();
const timer = setTimeout(async () => {
setLoading(true);
setError(null);
try {
const url = `https://huggingface.co/api/models?filter=gguf&search=${encodeURIComponent(query)}&sort=${sort}&direction=-1&limit=30`;
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`Search failed (HTTP ${res.status})`);
const repos: {
id: string;
downloads?: number;
likes?: number;
lastModified?: string;
tags?: string[];
}[] = await res.json();
setResults(
repos.map((r) => ({
repoId: r.id,
author: r.id.split('/')[0],
downloads: r.downloads,
likes: r.likes,
lastModified: r.lastModified,
tags: r.tags,
})),
);
} catch (e) {
if (!controller.signal.aborted)
setError(e instanceof Error ? e.message : 'Search failed');
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}, 300);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [query, sort]);
const selectRepo = async (repoId: string | null) => {
setExpanded(repoId);
setFiles(null);
if (!repoId) return;
const res = await fetch(`https://huggingface.co/api/models/${repoId}?blobs=true`);
const repo: { siblings?: { rfilename: string; size?: number }[] } = await res.json();
setFiles(
(repo.siblings ?? [])
.filter((s) => s.rfilename.endsWith('.gguf'))
.map((s) => ({
filename: s.rfilename,
sizeBytes: s.size,
quantLabel: s.rfilename.match(/(?:I?Q\d|F16|F32|BF16)[\w]*/i)?.[0]?.toUpperCase(),
})),
);
};
return (
setQuery((q) => q.trim())}
expandedRepoId={expanded}
onSelectRepo={selectRepo}
files={files}
filesLoading={expanded !== null && files === null}
onSelectFile={(repoId, file) => console.log(`load ${repoId}:${file.filename}`)}
/>
);
}
```
Error state with retry [#error-state-with-retry]
Pass the failure message and a retry callback — the component renders the error panel and the retry affordance:
```tsx
```
Customization [#customization]
Result rows cap tags at 4 before collapsing into a `+N` overflow chip — change `MAX_TAGS` in the copied file. The sort labels live in the `SORT_OPTIONS` array; the compact-count, byte-size, and relative-time formatters (`formatCount`, `formatBytes`, `formatRelative`) are plain local functions you can swap for `Intl` variants. The list height is the `max-h-96` on `Command.List`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Model Selector
Model Selector [#model-selector]
The **Model Selector** renders a device-aware list of models grouped by category, with backend filter chips (WebGPU / ONNX / WASM / LiteRT) that show live counts, per-model badges (vision, tool-calling, cached), a download affordance for uncached models, and a delete-from-cache affordance for cached ones. Models that cannot run on the current device — e.g. a WebGPU-only model on a device without WebGPU — are visibly de-emphasized while their "Requires WebGPU" reason stays at full contrast. It emits `onSelect(modelId)` and owns no selection state.
The backend-filter row wraps at narrow widths and only appears when **two or more** backends are actually present — a single-backend catalog (e.g. a CLIP embedding pair) hides the row rather than surfacing disabled zero-count chips. The filter chips form a labelled `role="group"` and expose their pressed state via `aria-pressed`; the active model row is marked `aria-current`; the download/delete controls carry `aria-label`s and expand to a ≥44px touch target on mobile.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/model-selector`
Full source: https://localmode.ai/r/ui/local-first/model-selector.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/model-selector
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the model list + `hasWebGPU` device-fit you pass and emits `onSelect(modelId)` — works with any backend. Recommended producer: `useCapabilities` (device-fit) and your `useModelRecommendations` data from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `model-selector.tsx` — the `ModelSelector` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ModelSelector**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `models` | `array` | — | **Required.** The models to list. |
| `selectedId` | `string` | — | Currently selected model id (for the active ring). |
| `hasWebGPU` | `boolean` | — | Whether the current device supports WebGPU. Models whose backend is `webgpu` are de-emphasized when this is false. |
| `busyIds` | `object` | — | Set of model ids whose download/delete is in progress. |
| `onSelect` | `function` | — | Fired when a fit, available model is activated. |
| `onDownload` | `function` | — | Fired when the download affordance on an uncached model is activated. |
| `onDelete` | `function` | — | Fired when the delete-from-cache affordance on a cached model is activated. |
**SelectableModel**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | **Required.** Stable model identifier passed to `onSelect`. |
| `name` | `string` | — | **Required.** Display name. |
| `backend` | `ModelBackend` | — | **Required.** Backend the model runs on (drives filter chips and device-fit). |
| `category` | `string` | — | **Required.** Category used for grouping (e.g. "Chat", "Vision"). |
| `size` | `string` | — | Human-readable size (e.g. "1.2 GB"). |
| `vision` | `boolean` | — | Whether the model supports image input. |
| `tools` | `boolean` | — | Whether the model supports tool / function calling. |
| `cached` | `boolean` | — | Whether the model is already cached on-device. |
Examples [#examples]
Bound to recommendations + capabilities [#bound-to-recommendations--capabilities]
```tsx
import { useCapabilities } from '@localmode/react';
import { ModelSelector } from '@/components/model-selector';
export function Sidebar({ models, onPick }) {
const { capabilities } = useCapabilities();
return (
prefetch(id)}
onDelete={(id) => evict(id)}
/>
);
}
```
Customization [#customization]
The de-emphasis (a muted model name, with the amber "Requires WebGPU" reason kept at full — AA — contrast rather than dimmed by opacity) and the hint itself are derived from the `hasWebGPU` prop — extend `isUnfit` in the copied file to gate on other backends (e.g. WASM-only). The filter row is auto-hidden for single-backend catalogs (`presentBackends.length > 1`); drop that guard if you always want the row. Badge icons use `lucide-react`; the active filter chip uses `bg-primary`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Network Badge
Network Badge [#network-badge]
The **Network Badge** is a reactive online/offline indicator sourced from `useNetworkStatus`. Because local models keep working without a network, "offline" is informational, not an error — pair it with **Offline Ready**, which signals that the app's required model is cached on-device and can run with no connectivity.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/network-badge`
Full source: https://localmode.ai/r/ui/local-first/network-badge.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/network-badge
```
Data source & dependencies [#data-source--dependencies]
**Data source:** reads online/offline state via the bundled `useNetworkStatus` navigator hook — self-contained, works in any React app, no backend required. (It mirrors `useNetworkStatus` from `@localmode/react`.)
* `@localmode/ui/lib/use-environment` — the bundled `useNetworkStatus` navigator hook (installed automatically as a registry dependency)
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `network-badge.tsx` — `NetworkBadge` + `OfflineReady`
* `lib/use-environment.ts` — the bundled `useNetworkStatus` navigator hook
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**NetworkBadge**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `compact` | `boolean` | `false` | When true, render a compact dot-only badge without text. |
**OfflineReady**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ready` | `boolean` | — | **Required.** Whether the model(s) the app needs are cached on-device. When true the app can run with no network. |
| `label` | `string` | — | Optional label override. |
Examples [#examples]
Online/offline + offline-ready [#onlineoffline--offline-ready]
```tsx
import { NetworkBadge, OfflineReady } from '@/components/network-badge';
```
Customization [#customization]
Both badges use shadcn/ui tokens with `emerald`/`amber` accents. `OfflineReady` takes a `ready` boolean from your cache check — when true the app can run fully offline. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Provider Badge
Provider Badge [#provider-badge]
The **Provider Badge** displays the **resolved** provider identity — composing the
[Provider Fallback Badge](/docs/local-first/provider-fallback-badge) for the tier
and name — alongside the **model id that actually served** the request. While
`providerName` is `null` it shows a "Resolving provider…" placeholder, so the
badge never claims a provider before one has resolved.
> **How it differs from Provider Fallback Badge.** The
> [Provider Fallback Badge](/docs/local-first/provider-fallback-badge) is the
> lower-level **tier chip** (built-in vs download, plus the WASM threading
> variant). The Provider Badge **composes it** and adds two things the fallback
> badge does not: the **resolved provider identity** (with a Resolving state) and
> the **served model id**. Use the fallback badge when you only want the tier
> chip; use the Provider Badge when you want to surface which provider + model
> actually answered.
> **Presentational.** It takes generic display props (`providerName`, `tier`,
> `modelId`, `note`) — no block-local provider types. Feed it from your provider
> resolution (recommended: a provider-fallback resolver such as
> `useProviderFallback` from `@localmode/react`).
**When to use it:** a Chrome-AI ⇄ Transformers.js (or any two-tier) surface where
the user should see which provider and model actually served the last result.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/provider-badge`
Full source: https://localmode.ai/r/ui/local-first/provider-badge.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/provider-badge
```
Dependencies [#dependencies]
* **Data source:** renders the resolved provenance you pass — works with any backend. Recommended LocalMode source: `useProviderFallback().resolution` (optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
* Composes `@localmode/ui/local-first/provider-fallback-badge` (installed automatically as a registry dependency)
Files installed [#files-installed]
* `provider-badge.tsx` — the component
* `provider-fallback-badge.tsx` — the composed tier chip
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ProviderBadge**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `providerName` | `string \| null` | — | **Required.** Resolved provider display name, or null while resolution is pending. |
| `tier` | `ProviderTier` | — | **Required.** The resolved provider's tier (drives the composed fallback badge). |
| `modelId` | `string \| null` | — | **Required.** The model id that actually served the most recent result, if any. |
| `note` | `string` | — | Optional note rendered after the badge (e.g. a provider disclaimer). |
Examples [#examples]
Show a resolved provider + served model [#show-a-resolved-provider--served-model]
```tsx
import { ProviderBadge } from '@/components/provider-badge';
```
Drive it from a provider-fallback resolver [#drive-it-from-a-provider-fallback-resolver]
```tsx
import { useProviderFallback } from '@localmode/react';
import { ProviderBadge } from '@/components/provider-badge';
function SummarizerBadge() {
const { resolution } = useProviderFallback();
return (
);
}
```
Customization [#customization]
The badge never estimates or probes — it reflects exactly the `providerName` /
`tier` / `modelId` you pass, so it can never claim a provider that did not serve
the request. Because you own the copied file, restyle the served-model line, add
a copy button, or change the "Resolving…" copy.
# Provider Fallback Badge
Provider Fallback Badge [#provider-fallback-badge]
The **Provider Fallback Badge** surfaces the active AI backend tier — a zero-download built-in (Chrome AI) vs a model-download provider (Transformers.js) — plus a WASM threading variant ("Multi-thread" when cross-origin isolated / SharedArrayBuffer is available, else "Single-thread"). It is a software-side sibling to the Device Badge (which surfaces hardware).
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/provider-fallback-badge`
Full source: https://localmode.ai/r/ui/local-first/provider-fallback-badge.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/provider-fallback-badge
```
Data source & dependencies [#data-source--dependencies]
**Data source:** reads the threading capability via the bundled `useCapabilities` navigator hook (the active-tier prop is yours to pass) — self-contained, works in any React app. (It mirrors `useCapabilities` from `@localmode/react`.)
* `@localmode/ui/lib/use-environment` — the bundled `useCapabilities` navigator hook (installed automatically as a registry dependency)
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `provider-fallback-badge.tsx` — the `ProviderFallbackBadge` component
* `lib/use-environment.ts` — the bundled `useCapabilities` navigator hook
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**ProviderFallbackBadge**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tier` | `ProviderTier` | — | **Required.** Active provider tier: a zero-download built-in (e.g. Chrome AI) vs a model-download provider (e.g. Transformers.js). |
| `providerName` | `string` | — | Provider display name (e.g. "Chrome AI", "Transformers.js"). |
| `crossOriginIsolated` | `boolean` | — | Override the cross-origin-isolation flag. When omitted the badge reads `useCapabilities` (and falls back to `globalThis.crossOriginIsolated`). |
| `hideThreading` | `boolean` | `false` | Hide the threading sub-badge. |
Examples [#examples]
Built-in vs download provider [#built-in-vs-download-provider]
```tsx
```
Customization [#customization]
The threading flag falls back to `useCapabilities` then `globalThis.crossOriginIsolated` when not passed. Hide the threading sub-badge with `hideThreading`. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Semantic Cache Status Bar
Semantic Cache Status Bar [#semantic-cache-status-bar]
The **Semantic Cache Status Bar** complements the Cache Badge: a compact toolbar row for `useSemanticCache` showing entry count, hit-rate %, an icon-only clear-cache button (when entries > 0), and an enable/disable toggle (with a spinner while the embedding model loads). It pairs with a per-message `CachedAnnotation` ("Cached (38ms)"). Distinct from the Cache Badge (model-download cache), this surfaces semantic-cache hits on responses.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/semantic-cache-status-bar`
Full source: https://localmode.ai/r/ui/local-first/semantic-cache-status-bar.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/semantic-cache-status-bar
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the cache `stats` you pass and emits clear/toggle — works with any backend. Recommended producer: `useSemanticCache` (backed by `createSemanticCache`) from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `semantic-cache-status-bar.tsx` — `SemanticCacheStatusBar` + `CachedAnnotation`
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**SemanticCacheStatusBar**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stats` | `object` | — | **Required.** Live cache stats (from `useSemanticCache`). |
| `enabled` | `boolean` | — | **Required.** Whether the cache is enabled. |
| `isLoading` | `boolean` | — | Whether the embedding model is still loading (shows a spinner). |
| `onToggle` | `function` | — | Fired when the enable/disable toggle is flipped. |
| `onClear` | `function` | — | Fired when the clear-cache button is pressed (shown when entries > 0). |
**CacheStatsLike**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `entries` | `number` | — | **Required.** Number of cached entries. |
| `hitRate` | `number` | — | **Required.** Hit rate in the 0–1 range. |
Examples [#examples]
Bound to useSemanticCache [#bound-to-usesemanticcache]
```tsx
const { stats, cache } = useSemanticCache({ embeddingModel });
cache?.clear()}
/>
```
Customization [#customization]
The hit-rate reads `stats.hitRate` (0–1) and renders as a percentage. The toggle is a themed switch; show the spinner via `isLoading` while the embedding model loads. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Storage Meter
Storage Meter [#storage-meter]
The **Storage Meter** shows origin / IndexedDB storage usage against quota as a meter, with a warning state past a configurable threshold. Storage estimates are approximate and blocked in some browsers (e.g. Safari private mode), so the component degrades to a graceful "unavailable" state rather than erroring. Bind it to `useStorageQuota` (the default) or pass an explicit `quota`.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/storage-meter`
Full source: https://localmode.ai/r/ui/local-first/storage-meter.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/storage-meter
```
Data source & dependencies [#data-source--dependencies]
**Data source:** reads `navigator.storage.estimate()` via the bundled `useStorageQuota` navigator hook (or pass an explicit `quota`) — self-contained, works in any React app. (It mirrors `useStorageQuota` from `@localmode/react`.)
* `@localmode/ui/lib/use-environment` — the bundled `useStorageQuota` navigator hook (installed automatically as a registry dependency)
* `@localmode/ui/lib/browser-utils` — `formatBytes` for the usage readout (installed automatically as a registry dependency)
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `storage-meter.tsx` — the `StorageMeter` component
* `lib/use-environment.ts` — the bundled `useStorageQuota` navigator hook
* `lib/browser-utils.ts` — generic browser helpers (`formatBytes`)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**StorageMeter**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `warnThreshold` | `number` | `0.8` | Fraction (0–1) at which the meter enters its warning state. |
| `quota` | `object` | — | Override the live quota source (used / total bytes). When omitted the component reads `useStorageQuota()`. |
Examples [#examples]
Default (live quota) [#default-live-quota]
```tsx
```
Explicit quota [#explicit-quota]
```tsx
```
Customization [#customization]
The warning state turns the bar amber and shows a hint past `warnThreshold` (default `0.8`). When no estimate is available the component renders a muted "Storage estimate unavailable" row — never an error. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Vector Export Panel
Vector Export Panel [#vector-export-panel]
The **Vector Export Panel** is the counterpart to the [Vector Import Flow](/docs/local-first/vector-import-flow): a record/dimension count line, a row per export format (native JSON with vectors, CSV, JSONL — each with a label, description, and vectors-included / text-only indicator), per-format export actions emitting `onExport(formatId)`, a busy state that disables all actions (with a spinner on the active format), a zero-records disabled state, and an optional last-export banner (format, records, human-readable size, filename).
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/vector-export-panel`
Full source: https://localmode.ai/r/ui/local-first/vector-export-panel.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/vector-export-panel
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the formats/counts/exporting/last-export state you pass and emits `onExport(formatId)` — works with any backend. Recommended producer: `useImportExport` (`exportCSV`, `exportJSONL`) from `@localmode/react` plus a native JSON export (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `vector-export-panel.tsx` — the `VectorExportPanel` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**VectorExportPanel**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `formats` | `array` | — | **Required.** Export formats to offer, one action per format. |
| `recordCount` | `number` | — | **Required.** Number of records currently in the corpus. Zero disables all actions. |
| `dimensions` | `number` | — | Vector dimensions of the corpus (shown next to the record count). |
| `exporting` | `boolean \| string` | — | Busy state: `true` disables all actions; a format id additionally renders a spinner on that format's action. |
| `lastExport` | `object \| null` | — | Summary of the last completed export (shown as a result banner). |
| `onExport` | `function` | — | **Required.** Fired with the format id when the user activates a format's export action. |
| `disabled` | `boolean` | — | Disables all actions regardless of state. |
**ExportFormat**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | **Required.** Stable format id (passed to `onExport`, e.g. `"native-json"`, `"csv"`, `"jsonl"`). |
| `label` | `string` | — | **Required.** Display label (e.g. "Native JSON"). |
| `description` | `string` | — | One-line description (e.g. "Full fidelity, re-importable"). |
| `vectors` | `boolean` | — | Whether the format includes vectors (renders a "Vectors" / "Text only" indicator when set). |
**LastExportSummary**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `formatId` | `string` | — | **Required.** The format that was exported. |
| `records` | `number` | — | **Required.** Number of records exported. |
| `bytes` | `number` | — | **Required.** Export size in bytes. |
| `filename` | `string` | — | Downloaded filename. |
| `at` | `string` | — | When the export finished (preformatted display string, e.g. "just now"). |
Examples [#examples]
Wiring to export functions [#wiring-to-export-functions]
Works with any export backend — pass a busy format id while the export runs, then a summary when it completes:
```tsx
const { exportCSV, exportJSONL } = useImportExport({ db });
const [exporting, setExporting] = useState(false);
const [lastExport, setLastExport] = useState(null);
const runExport = async (formatId: string) => {
setExporting(formatId);
try {
const blob = formatId === 'csv' ? await exportCSV() : await exportJSONL();
downloadBlob(blob, `vectors.${formatId}`);
setLastExport({ formatId, records: count, bytes: blob.size, filename: `vectors.${formatId}` });
} finally {
setExporting(false);
}
};
```
Disabling while other work runs [#disabling-while-other-work-runs]
```tsx
```
Customization [#customization]
`exporting` accepts `true` (disable everything) or a format id (also spin that format's action). The vectors-included / text-only indicator renders only when a format sets `vectors`; the zero-records note and disabled state kick in at `recordCount === 0`. The size in the last-export banner is formatted by the local `formatBytes` helper — edit its thresholds or units freely. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Vector Import Flow
Vector Import Flow [#vector-import-flow]
The **Vector Import Flow** wraps `useImportExport`: a preview panel (detected-format badge, total / with-vectors / text-only counts, detected dimensions, dimension-mismatch warning, Cancel/Confirm), a phased progress bar (parsing → validating → embedding → importing), a result stats banner (imported / skipped / re-embedded, format, duration), and a record-preview table for row-level sanity checks before a destructive ingest. It references the cross-family `FormatDetectionBadge` and inlines a minimal fallback so it builds independently.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/vector-import-flow`
Full source: https://localmode.ai/r/ui/local-first/vector-import-flow.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/vector-import-flow
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the import preview/progress/result you pass and emits Cancel/Confirm — works with any backend. Recommended producer: `useImportExport` (backed by the import/export parsers) from `@localmode/react` (on-device, optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Registry dependencies [#registry-dependencies]
* `@localmode/ui/data-documents/format-detection-badge` — the shared format badge (a minimal inline fallback ships with this item)
Files installed [#files-installed]
* `vector-import-flow.tsx` — the `VectorImportFlow` component
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**VectorImportFlow**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `preview` | `object \| null` | — | Parse preview shown before a destructive import. |
| `targetDimensions` | `number` | — | Target VectorDB dimensions (for the mismatch warning). |
| `records` | `array` | — | Sample records for the preview table. |
| `progress` | `object \| null` | — | Live import progress. |
| `stats` | `object \| null` | — | Final import stats (shown when the import completes). |
| `isImporting` | `boolean` | — | Whether an import is running. |
| `onConfirm` | `function` | — | Fired to confirm the import. |
| `onCancel` | `function` | — | Fired to cancel/dismiss the preview. |
**ImportPreview**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `format` | `ExternalFormat` | — | **Required.** |
| `totalRecords` | `number` | — | **Required.** |
| `recordsWithVectors` | `number` | — | **Required.** |
| `recordsWithTextOnly` | `number` | — | **Required.** |
| `dimensions` | `number \| null` | — | **Required.** |
Examples [#examples]
Preview then import [#preview-then-import]
```tsx
const { parseResult, progress, stats, parsePreview, importData } = useImportExport({ db, model });
importData({ content })}
/>
```
Customization [#customization]
The dimension-mismatch warning compares `preview.dimensions` against `targetDimensions`. Swap the inline `FormatBadge` for the installed `@localmode/ui/data-documents/format-detection-badge` when present. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Vector Storage Observability
Vector Storage Observability [#vector-storage-observability]
The **Vector Storage Observability** set complements the Storage Meter (quota) with VectorDB-specific observability: a compression-stats badge (SQ8 ratio + before/after size, e.g. "4.0× — 15KB→3.7KB"), a three-tier storage estimate (Raw Float32 / SQ8 4× / PQ 8–32× with the active tier highlighted), and a GPU-aware search-latency badge (accented when WebGPU-accelerated). Values derive from compression stats and search timing, passed in as props (recommended producer: `getCompressionStats()` / `useStorageQuota` from `@localmode/react`, on-device, optional).
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/local-first/vector-storage-observability`
Full source: https://localmode.ai/r/ui/local-first/vector-storage-observability.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/local-first/vector-storage-observability
```
Data source & dependencies [#data-source--dependencies]
**Data source:** renders the compression stats + search timing you pass — works with any backend. Recommended producer: `getCompressionStats()` and `useStorageQuota` from `@localmode/react` (on-device, optional).
* `@localmode/ui/lib/browser-utils` — `formatBytes` for the size readouts (installed automatically as a registry dependency)
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `vector-storage-observability.tsx` — the `VectorStorageObservability` component
* `lib/browser-utils.ts` — generic browser helpers (`formatBytes`)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**VectorStorageObservability**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stats` | `object` | — | **Required.** Compression stats (from `getCompressionStats()`). |
| `tier` | `QuantizationTier` | — | **Required.** The active quantization tier. |
| `searchLatencyMs` | `number` | — | Last search latency in milliseconds. |
| `webgpuAccelerated` | `boolean` | — | Whether search ran on a WebGPU-accelerated distance kernel. |
**CompressionStatsLike**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `ratio` | `number` | — | **Required.** Compression ratio (original / compressed). |
| `originalSizeBytes` | `number` | — | **Required.** Estimated uncompressed size in bytes. |
| `compressedSizeBytes` | `number` | — | **Required.** Estimated compressed size in bytes. |
| `vectorCount` | `number` | — | Number of stored vectors. |
Examples [#examples]
Bound to compression stats [#bound-to-compression-stats]
```tsx
import { getCompressionStats } from '@localmode/core';
const stats = await getCompressionStats(db); // async — returns Promise
```
Customization [#customization]
The latency badge accents (violet) when `webgpuAccelerated` is true. The three-tier estimate highlights the `tier` prop; non-active tiers dim. Styled entirely with shadcn/ui CSS-variable utilities so it inherits your theme — because you own the copied file, every class and threshold is yours to change.
# Before / After Image Viewer
Before / After Image Viewer [#before--after-image-viewer]
The **Before / After Image Viewer** compares an original image with a transformed result. It supports two modes: a **two-panel grid** (both images side-by-side, the result panel ring-highlighted and rendered on a checkerboard transparency background so alpha shows through), and a segmented **Original / Enhanced toggle** that swaps a single displayed image source.
It is presentational — both `originalSrc` and `processedSrc` are image source strings (data URL or object URL). Produce the result image with [`useImageToImage`](https://localmode.dev/docs/react) (upscaling / super-resolution), whose `UpscaleImageResult.image` is an `ImageData | Blob` you convert to an object URL (`URL.createObjectURL`) before passing it as `processedSrc`. It owns only the toggle's local UI state.
**When to use it:** background removers, photo enhancers / upscalers, segmentation viewers — any "here's your input, here's the result" comparison.
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/media-vision/before-after-image-viewer`
Full source: https://localmode.ai/r/ui/media-vision/before-after-image-viewer.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/media-vision/before-after-image-viewer
```
Dependencies [#dependencies]
* **Data source:** renders the `originalSrc` / `processedSrc` image strings you pass — works with any backend that produces a transformed image. Recommended LocalMode producer: `useImageToImage` (upscale / super-resolution) from `@localmode/react` — convert its `image` (`ImageData | Blob`) to an object URL for `processedSrc`. For background removal, composite `useSegmentImage`'s `masks` onto the source into a transparent PNG first (optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `before-after-image-viewer.tsx` — the component (checkerboard CSS shipped inline)
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**BeforeAfterImageViewer**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `originalSrc` | `string` | — | **Required.** Source (data URL or URL) of the original image. |
| `processedSrc` | `string` | — | Source of the transformed result (upscaled, segmented, background-removed). When omitted, only the original is shown. |
| `mode` | `"toggle" \| "grid"` | `"grid"` | Layout. `"grid"` shows both panels side-by-side; `"toggle"` shows one image with a segmented Original/Enhanced switch. |
| `originalLabel` | `string` | `"Original"` | Label for the original panel/toggle. |
| `processedLabel` | `string` | `"Enhanced"` | Label for the result panel/toggle. |
| `checkerboard` | `boolean` | `true` | Render the result panel on a checkerboard transparency background so alpha shows through (segmentation / background removal). |
| `alt` | `string` | `""` | Base alt text for the subject. Distinct alts are derived for the two images so assistive tech never hears the same description for both — the original becomes `"{originalLabel}: {alt}"` and the result `"{processedLabel}: {alt}"`. Empty (the default) keeps both images decorative. Use {@link originalAlt} / {@link resultAlt} to set them explicitly. |
| `originalAlt` | `string` | — | Explicit alt for the original image. Overrides the value derived from {@link alt}. |
| `resultAlt` | `string` | — | Explicit alt for the processed/result image. Overrides the value derived from {@link alt}. |
Backing hooks [#backing-hooks]
Produce `processedSrc` with `useImageToImage` (upscale / super-resolution) from [`@localmode/react`](https://localmode.dev/docs/react): its `UpscaleImageResult.image` is an `ImageData | Blob`, so wrap it in an object URL (`URL.createObjectURL`) before passing it. For background removal, `useSegmentImage` returns `masks` (not a ready image) — composite a mask onto the source to build a transparent PNG, then pass that URL.
Examples [#examples]
Grid (default) [#grid-default]
```tsx
import { BeforeAfterImageViewer } from '@/components/before-after-image-viewer';
import { useImageToImage } from '@localmode/react';
const { data } = useImageToImage({ model });
// data.image is ImageData | Blob — turn the Blob result into a displayable URL
const processedSrc = data?.image instanceof Blob ? URL.createObjectURL(data.image) : undefined;
```
Toggle mode [#toggle-mode]
```tsx
```
Background-removal result on checkerboard [#background-removal-result-on-checkerboard]
```tsx
const { data } = useSegmentImage({ model });
// useSegmentImage returns `masks` (SegmentMask[]), not a ready image —
// composite a mask onto the source to produce a transparent-background PNG URL.
const cutoutUrl = data ? compositeMaskToTransparentPng(inputDataUrl, data.masks) : undefined;
```
Accessibility [#accessibility]
The two images get **distinct** alt text so assistive tech never announces the same description for both. Pass a single `alt` and the viewer derives `"{originalLabel}: {alt}"` for the original and `"{processedLabel}: {alt}"` for the result; or set `originalAlt` / `resultAlt` explicitly for full control. An empty `alt` (the default) keeps both images decorative.
```tsx
```
Customization [#customization]
The checkerboard is a self-contained inline CSS-gradient background, so the component works standalone after `shadcn add` with no global CSS. Adjust the checker size/color in the copied `before-after-image-viewer.tsx`, or set `checkerboard={false}` for opaque results. Panels use shadcn/ui tokens (`border-border`, `bg-card`, `ring-primary`).
# Bounding Box Overlay
Bounding Box Overlay [#bounding-box-overlay]
The **Bounding Box Overlay** renders detection results (`{ label, score, box }`) as absolutely-positioned, color-coded boxes with label chips over a parent image. Pixel-coordinate boxes are converted to **percentage offsets** using the image's natural width/height, so placement is correct at any display size — resize the container and the boxes stay aligned. A companion **`DetectionLabelLegend`** renders a wrapping strip of color → class pills that match the overlay colors.
The overlay consumes one shape — `{ label, score, box }`, where `box` is `{ x, y, width, height }` in natural image pixels. [`useDetectObjects`](https://localmode.dev/docs/react) returns this directly (`DetectObjectsResult.objects`, each a `DetectedObject`). `useDetectFace` returns boxes too (`FaceDetectionResultItem` has `box` + `score`, but no `label`), and `useDetectHands` / `useDetectPose` return landmark sets (`{ landmarks, worldLandmarks, … }`, no `box`) — for those, derive a `label` (and, for hands/pose, a `box` from the landmark extents) before passing them in.
**When to use it:** object/face detection viewers, pose/hand annotation, anything that draws boxes over a still image. (For real-time webcam landmark drawing, use the [Video Canvas](/docs/media-vision/video-canvas) instead.)
Preview [#preview]
Install: `npx shadcn@latest add @localmode/ui/media-vision/bounding-box-overlay`
Full source: https://localmode.ai/r/ui/media-vision/bounding-box-overlay.json
Installation [#installation]
```bash
npx shadcn@latest add @localmode/ui/media-vision/bounding-box-overlay
```
Dependencies [#dependencies]
* **Data source:** renders the `{ label, score, box }` detections you pass — works with any backend that returns boxes. Recommended LocalMode producer: `useDetectObjects` from `@localmode/react` (its `DetectedObject` matches this shape directly). `useDetectFace` boxes (no `label`) and `useDetectHands` / `useDetectPose` landmark output map in with a small adapter (optional).
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
Files installed [#files-installed]
* `bounding-box-overlay.tsx` — `BoundingBoxOverlay` + `DetectionLabelLegend`
* `lib/utils.ts` — the `cn()` helper (if not already present)
Props [#props]
**BoundingBoxOverlay**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `detections` | `array` | — | **Required.** Detections to render. Boxes are in natural-pixel coordinates. |
| `naturalWidth` | `number` | — | **Required.** The image's NATURAL width in pixels (e.g. `imgEl.naturalWidth`). Required to convert pixel boxes to display-independent percentage offsets. |
| `naturalHeight` | `number` | — | **Required.** The image's NATURAL height in pixels (e.g. `imgEl.naturalHeight`). |
| `hideLabels` | `boolean` | `false` | Hide the per-box label chip (border-only boxes). Useful for dense landmark/pose output. |
**DetectionLabelLegend**
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `labels` | `array` | — | **Required.** Detected class labels (duplicates allowed — they are de-duplicated). Colors match {@link BoundingBoxOverlay} for the same label set/order. |
Backing hooks [#backing-hooks]
Feed it `{ label, score, box }` detections plus the image's `naturalWidth` / `naturalHeight` (read off the rendered ``). `useDetectObjects` from [`@localmode/react`](https://localmode.dev/docs/react) returns `objects` in this shape directly; `useDetectFace` returns boxes without a `label`, and `useDetectHands` / `useDetectPose` return landmark sets (no `box`) — map those to `{ label, score, box }` before passing them in.
Examples [#examples]
Over a detection result [#over-a-detection-result]
```tsx
import { BoundingBoxOverlay, DetectionLabelLegend } from '@/components/bounding-box-overlay';
import { useDetectObjects } from '@localmode/react';
export function Example({ src }: { src: string }) {
const imgRef = useRef(null);
const { data } = useDetectObjects({ model });
return (
<>