# 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.ts From 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 `