Utilities
Hooks for model loading, model status, capabilities, network, storage, voice recording, audit logs, and helper utilities for files and downloads.
Utility Hooks
See it in action
Try the Chat block, Knowledge Base block, and Audio Studio block for working demos of these hooks.
useVoiceRecorder
Manage browser MediaRecorder lifecycle for audio recording. Pairs naturally with useTranscribe.
import { useVoiceRecorder, useTranscribe } from '@localmode/react';
function VoiceInput() {
const recorder = useVoiceRecorder();
const transcriber = useTranscribe({ model });
const handleStop = async () => {
const blob = await recorder.stopRecording();
if (blob) await transcriber.execute(blob);
};
return (
<div>
<button onClick={recorder.isRecording ? handleStop : recorder.startRecording}>
{recorder.isRecording ? 'Stop' : 'Record'}
</button>
{recorder.error && <p>{recorder.error.message}</p>}
</div>
);
}Return Value
| Property | Type | Description |
|---|---|---|
isRecording | boolean | Whether audio is being recorded |
stream | MediaStream | null | The live MediaStream while recording (e.g. for waveform visualizers), null otherwise |
error | AppError | null | Recording error (e.g., mic denied) |
startRecording | () => Promise<void> | Request mic access and start recording |
stopRecording | () => Promise<Blob | null> | Stop recording, return audio blob |
getVolume | () => number | Current input volume as RMS in [0, 1] (0 when not recording) |
clearError | () => void | Clear the error state |
Options
| Option | Type | Description |
|---|---|---|
mimeType | string | Preferred MIME type (default: 'audio/webm;codecs=opus') |
deviceId | string | Specific microphone to record from (a deviceId from enumerateDevices()). Forwarded as { deviceId: { exact: deviceId } } — recording fails with error set if the device is unavailable, rather than silently falling back |
constraints | MediaTrackConstraints | Additional audio track constraints merged into the getUserMedia request (e.g. { echoCancellation: true }). An explicit deviceId takes precedence over constraints.deviceId |
const recorder = useVoiceRecorder({
deviceId: selectedMicId,
constraints: { echoCancellation: true },
});Volume Metering
getVolume() lazily creates an AnalyserNode on first call while recording and tears it down on stop/unmount. Call it from a requestAnimationFrame loop to drive level meters:
useEffect(() => {
if (!recorder.isRecording) return;
let raf: number;
const tick = () => {
setLevel(recorder.getVolume()); // RMS in [0, 1]
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [recorder.isRecording]);To play back the recorded blob, pair with useObjectUrl.
toAppError
Convert Error | null from hook returns to the AppError shape expected by UI components.
import { toAppError } from '@localmode/react';
import type { AppError } from '@localmode/react';
// In a hook's return statement:
return {
error: toAppError(error), // { message: '...', recoverable: true } or null
error: toAppError(error, false), // { message: '...', recoverable: false }
};All @localmode/react hooks return Error | null. Components typically render error.message and check error.recoverable. toAppError bridges the gap:
// Without toAppError (verbose):
error: error ? { message: error.message, recoverable: true } : null
// With toAppError (clean):
error: toAppError(error)When the source error is a core LocalModeError, its code is carried over to AppError.code and its hint is appended to the message:
// LocalModeError('Model failed', 'MODEL_LOAD_ERROR', { hint: 'Check your network' })
toAppError(error)
// → { message: 'Model failed — Check your network', code: 'MODEL_LOAD_ERROR', recoverable: true }AppError Type
interface AppError {
message: string;
code?: string;
recoverable?: boolean;
}useModelLoad
Own the full provider-model load lifecycle: construct the model once per key, normalize download progress across all LocalMode providers into one 0–1 aggregate, and drive a status machine from a warmup inference — the only reliable "fully loaded" signal, since providers load lazily on first inference.
State lives in a module-level registry (consumed via useSyncExternalStore), so progress survives unmount/remount mid-download and multiple components can observe the same load.
import { useModelLoad } from '@localmode/react';
import { transformers, isModelCached } from '@localmode/transformers';
const MODEL_ID = 'Xenova/all-MiniLM-L6-v2';
function SearchPanel() {
const { status, progressValue, model, load, error } = useModelLoad({
key: MODEL_ID,
create: (onProgress) => transformers.embedding(MODEL_ID, { onProgress }),
isCached: () => isModelCached(MODEL_ID),
});
if (status === 'error') return <p>Failed: {error?.message}</p>;
if (status !== 'ready') {
return (
<button onClick={load} disabled={status === 'loading'}>
Load model ({(progressValue.percent * 100).toFixed(0)}%)
</button>
);
}
return <SearchView model={model} />;
}The create-factory pattern
Providers bind onProgress at model construction, so the hook owns the progress callback and your create factory wires it in. The factory is invoked at most once per key, and only on the client (never during SSR):
// transformers
create: (onProgress) => transformers.embedding(id, { onProgress })
// webllm
create: (onProgress) => webllm.languageModel(id, { onProgress })
// wllama
create: (onProgress) => wllama.languageModel(id, { onProgress })Progress events from every provider shape (transformers per-file bytes, webllm aggregate percent, wllama/litert single-file bytes) are normalized into progress (0–1) and the perFile map. The published progress / progressValue.percent is non-decreasing within a load attempt: when a provider discovers additional files mid-download the raw Σloaded/Σtotal can dip, so the aggregate holds its high-water mark (reset on each load() attempt) while perFile keeps the raw byte counts.
Options
Prop
Type
Return Value
Prop
Type
Pairing progressValue with a download UI
progressValue is shaped to match download-progress UI components directly — for example the DownloadProgress element from the @localmode/ui registry accepts it as its value prop with no adapter:
// installed via: npx shadcn add @localmode/ui/local-first/model-downloader
import { DownloadProgress } from '@/components/ui/model-downloader';
const { progressValue } = useModelLoad({ key, create });
<DownloadProgress value={progressValue} />Not abortable in v1
load() cannot be cancelled — provider model loads do not accept an AbortSignal, so an in-flight load runs to completion (or failure). Treat the load as a commit once started.
useModelStatus
Read-only view of whether a model is ready for inference. Backed by the same registry as useModelLoad: it looks up the shared load lifecycle by model.modelId and reflects the real state — isLoading while a load is in flight, isReady once the warmup inference resolved, progress (0–1) from normalized provider progress events (non-decreasing within a load attempt, like useModelLoad), and error when the load failed.
import { useModelStatus } from '@localmode/react';
const model = transformers.embedding('Xenova/all-MiniLM-L6-v2');
function StatusBadge() {
const { isReady, isLoading, progress, error } = useModelStatus(model);
if (isLoading) return <p>Loading {(progress * 100).toFixed(0)}%...</p>;
if (error) return <p>Failed to load: {error.message}</p>;
return <p>{isReady ? 'Model ready' : 'Not loaded'}</p>;
}Corrected semantics
useModelStatus previously reported isReady: true optimistically as soon as a model instance existed — but provider models load lazily on first inference, so a constructed instance proves nothing. It now observes the actual lifecycle driven by useModelLoad({ key: model.modelId, ... }). When no load has been observed for the modelId, it reports { isReady: false, isLoading: false, progress: 0, error: null }.
useCapabilities
Detect browser AI capabilities on mount. Returns a typed DeviceCapabilities (with browser, device, hardware, features, and storage sub-objects), a detection error, and a refresh() function to re-detect.
import { useCapabilities } from '@localmode/react';
function Demo() {
const { capabilities, isDetecting, error, refresh } = useCapabilities();
if (isDetecting) return <p>Detecting...</p>;
if (error) return <button onClick={refresh}>Retry detection</button>;
return (
<ul>
<li>WebGPU: {capabilities?.features.webgpu ? 'Yes' : 'No'}</li>
<li>WASM: {capabilities?.features.wasm ? 'Yes' : 'No'}</li>
<li>IndexedDB: {capabilities?.features.indexedDB ? 'Yes' : 'No'}</li>
</ul>
);
}useNetworkStatus
Reactively track online/offline status.
import { useNetworkStatus } from '@localmode/react';
function Demo() {
const { isOnline, isOffline } = useNetworkStatus();
return <p>{isOnline ? 'Online' : 'Offline'}</p>;
}useStorageQuota
Monitor browser storage usage. Exposes the full core StorageQuota (usedBytes, quotaBytes, percentUsed, isPersisted, availableBytes) — or null while unavailable — plus loading state, the query error, and refresh().
import { useStorageQuota } from '@localmode/react';
function Demo() {
const { quota, isLoading, error, refresh } = useStorageQuota();
if (error) return <p>Quota unavailable: {error.message}</p>;
return (
<div>
{quota && (
<p>
{quota.percentUsed.toFixed(1)}% used
({(quota.availableBytes / 1024 / 1024).toFixed(0)} MB free)
{quota.isPersisted ? ' — persisted' : ''}
</p>
)}
<button onClick={refresh}>Refresh</button>
</div>
);
}Helper Utilities
Browser utility functions commonly needed when building AI-powered React apps.
readFileAsDataUrl
Read a browser File as a data URL string for passing to image/audio models.
import { readFileAsDataUrl } from '@localmode/react';
const handleFile = async (file: File) => {
const dataUrl = await readFileAsDataUrl(file);
await captioner.execute(dataUrl);
};validateFile
Validate file type and size before processing. Returns AppError | null.
import { validateFile } from '@localmode/react';
const error = validateFile({
file,
accept: ['image/png', 'image/jpeg', 'image/webp'],
maxSize: 10_000_000, // 10MB
});
if (error) {
setError(error); // { message: '...', recoverable: true }
return;
}Options
| Option | Type | Description |
|---|---|---|
file | File | The file to validate (required) |
accept | string[] | Accepted MIME types |
maxSize | number | Maximum size in bytes |
downloadBlob
Trigger a file download from in-memory content.
import { downloadBlob } from '@localmode/react';
// Download text content
downloadBlob('transcript text...', 'transcript.txt');
// Download binary content
downloadBlob(audioBlob, 'recording.webm', 'audio/webm');useObjectUrl
Derive a stable object URL from a Blob with automatic URL.revokeObjectURL when the blob changes or the component unmounts. Returns null when the blob is null/undefined, during SSR, and for the first render after a blob change (the URL is created in an effect so server and client markup match).
import { useObjectUrl } from '@localmode/react';
function NotePlayback({ audio }: { audio: Blob | null }) {
const src = useObjectUrl(audio);
return src ? <audio controls src={src} /> : null;
}Pairs naturally with useVoiceRecorder (recorded blobs) and useSynthesizeSpeech (synthesized audio).
useModelLoader
Wraps createModelLoader() from @localmode/core with React state for downloading model files directly from URLs. Use this for custom ONNX models, self-hosted models, or other direct file downloads — not for models loaded through @localmode/transformers or @localmode/webllm (those providers manage their own caching; use useModelLoad for those).
import { useModelLoader } from '@localmode/react';
function ModelManager() {
const { downloads, isDownloading, error, prefetch, cancel, evict } = useModelLoader({
maxCacheSize: '2GB',
});
if (error) return <p>Loader unavailable: {error.message}</p>; // e.g. IndexedDB blocked
return (
<button onClick={() => prefetch([{
url: 'https://your-cdn.com/models/custom-model.onnx',
modelId: 'custom-model'
}])}>
Download Model
</button>
);
}The error field surfaces loader initialization failures (e.g. IndexedDB blocked in private browsing) instead of silently no-oping. For full API reference and when-to-use guidance, see the Model Cache documentation.
useInferenceQueue
Wraps createInferenceQueue() for priority-based task scheduling with live stats.
import { useInferenceQueue } from '@localmode/react';
function QueueDemo() {
const { queue, stats, isProcessing } = useInferenceQueue({
concurrency: 1,
priorities: ['interactive', 'background'],
});
const handleSearch = async (query: string) => {
const result = await queue.add(
() => embed({ model, value: query }),
{ priority: 'interactive' }
);
};
return (
<div>
{stats && <p>Pending: {stats.pending}, Active: {stats.active}</p>}
</div>
);
}For full API reference, see the Inference Queue documentation.
useSemanticCache
Manages a SemanticCache lifecycle in React components. Creates the cache on mount and destroys it on unmount.
import { useSemanticCache } from '@localmode/react';
import { transformers } from '@localmode/transformers';
function CachedApp() {
const { cache, stats, isLoading } = useSemanticCache({
embeddingModel: transformers.embedding('Xenova/bge-small-en-v1.5'),
threshold: 0.92,
maxEntries: 100,
});
if (isLoading || !cache) return <p>Initializing cache...</p>;
return (
<div>
<p>Entries: {stats.entries}</p>
<p>Hit rate: {(stats.hitRate * 100).toFixed(1)}%</p>
</div>
);
}For full API reference, see the Semantic Cache documentation.
useAuditLog
Wraps a @localmode/core AuditLog instance (append-only, hash-chained, signed). Loads entries on mount, exposes append/verify/refresh, and retains the most recent verification result as lastVerification.
import { useAuditLog } from '@localmode/react';
import type { AuditLog } from '@localmode/core';
function AuditPanel({ log }: { log: AuditLog }) {
const { entries, append, verify, lastVerification, isLoading, error } = useAuditLog(log);
return (
<div>
{entries.map((e) => <div key={e.id}>{e.kind}</div>)}
<button onClick={() => append('user.click', {})}>Log click</button>
<button onClick={verify}>Verify chain</button>
{lastVerification && <p>{lastVerification.ok ? 'Chain intact' : `Chain broken at #${lastVerification.brokenAt}`}</p>}
</div>
);
}Return Value
| Property | Type | Description |
|---|---|---|
entries | AuditEntry[] | Entries in chain order, oldest first |
isLoading | boolean | True while the initial load or refresh is in flight |
error | Error | null | Last error from append/verify/refresh |
append | (kind, payload) => Promise<AuditEntry> | Append an entry; updates entries on success |
verify | () => Promise<AuditLogVerifyResult> | Run verifyChain against the log |
lastVerification | AuditLogVerifyResult | null | Result of the most recent resolved verify() call — { ok, brokenAt?, reason?, entriesChecked, durationMs } (null until one resolves; unchanged when verify() throws) |
refresh | () => Promise<void> | Re-read entries from storage |
There is intentionally no clear — the audit log is append-only. For full API reference, see the Audit Log documentation.
useEncryptedVault
Passphrase-locked, encrypted CRUD vault over a pluggable core StorageAdapter (default: a dedicated IndexedDBStorage('vault_<name>')). Items are AES-GCM envelopes at rest (fresh 12-byte IV per write, versioned, base64) — everything except the item id and timestamps is ciphertext. The key is derived once per unlock via core deriveEncryptionKey (PBKDF2) and held only in a ref while unlocked; the passphrase is never retained and no key material is ever persisted. lock() and unmount clear it.
A single unlock(passphrase) entrypoint initializes the vault on first use and verifies the passphrase afterwards — wrong passphrases are detected deterministically via an encrypted verifier record, which works even on an empty vault.
See it live
The Encrypted Vault block drives useEncryptedVault end-to-end in the browser — it runs the full passphrase lifecycle (create / lock / unlock), encrypted note + document CRUD with ciphertext at rest in IndexedDB, and a tamper-evident hash-chained audit log, all built on the ui/security-privacy/passphrase-gate, vault-item-card, and lock-status-badge primitives.
import { useEncryptedVault } from '@localmode/react';
function Vault() {
const { status, items, error, unlock, lock, createItem, deleteItem } =
useEncryptedVault<{ note: string }>({ name: 'notes' });
if (status !== 'unlocked') {
return (
<form onSubmit={(e) => { e.preventDefault(); unlock(passphraseInput); }}>
<button>{status === 'uninitialized' ? 'Create vault' : 'Unlock'}</button>
{error?.name === 'VaultPassphraseError' && <p>Wrong passphrase</p>}
</form>
);
}
return (
<div>
{items.map((i) => (
<div key={i.id}>
{i.data.note} <button onClick={() => deleteItem(i.id)}>x</button>
</div>
))}
<button onClick={() => createItem({ note: 'hello' })}>Add</button>
<button onClick={lock}>Lock</button>
</div>
);
}Options
| Option | Type | Default | Description |
|---|---|---|---|
name | string | 'default' | Vault namespace — names the default IndexedDB database (vault_<name>) and the vault's storage collection |
storage | StorageAdapter | — | Pluggable storage adapter (e.g. MemoryStorage, Dexie/idb/localForage adapters). When omitted, a dedicated IndexedDBStorage is created, opened, and closed by the hook |
iterations | number | 100000 | PBKDF2 iterations used when the vault is initialized (first unlock). Subsequent unlocks always use the iteration count persisted in the vault meta record |
Return Value
| Property | Type | Description |
|---|---|---|
status | 'uninitialized' | 'locked' | 'unlocked' | Vault lifecycle state, detected from the persisted meta record on mount |
items | VaultItem<T>[] | Decrypted item list while unlocked; [] otherwise |
isBusy | boolean | True while an unlock/CRUD/refresh operation is in flight |
error | Error | null | Last operation error — VaultPassphraseError on wrong passphrase, VaultLockedError on CRUD while locked |
unlock | (passphrase) => Promise<boolean> | Unlock the vault — initializes on first use, verifies the passphrase afterwards. Resolves true on success |
lock | () => void | Synchronously lock: clears the in-memory key and decrypted items |
createItem | (data) => Promise<VaultItem<T> | null> | Encrypt and persist a new item. Resolves null on failure or while locked |
readItem | (id) => Promise<VaultItem<T> | null> | Read and decrypt a single item by id |
updateItem | (id, data) => Promise<VaultItem<T> | null> | Re-encrypt an existing item with new data |
deleteItem | (id) => Promise<boolean> | Delete an item by id. Resolves false if missing or locked |
refresh | () => Promise<void> | Re-read and decrypt all items from storage into items |
cancel | () => void | Abort the in-flight operation (silent — no error state) |
Methods never throw to the caller — failures resolve null/false and surface via error. Encryption uses the Web Crypto API only, so the vault requires a secure context (HTTPS or localhost). One vault per storage namespace: give each vault its own adapter (the default IndexedDB database is already namespaced per vault name). For the underlying primitives, see the Encryption documentation.
Blocks
| App | Description | Links |
|---|---|---|
| Chat (LLM Chat) | Uses toAppError for error handling, downloadBlob for exports | Live block · Source |
| Knowledge Base (Semantic Search) | Uses toAppError, downloadBlob, validateFile | Live block · Source |
| Audio Studio (Voice Notes) | Uses toAppError for transcription error handling | Live block · Source |
| Vision (Object Detector) | Uses readFileAsDataUrl for image loading | Live block · Source |
| Privacy (PII Redactor) | Uses toAppError and downloadBlob | Live block · Source |
| Text Insights (Model Evaluator) | Uses toAppError for evaluation error handling | Live block · Source |