# 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]

```tsx
'use client';

import { useEffect, useState } from 'react';

import { DownloadProgress, ModelDownloader } from '@/components/model-downloader';

/**
 * Demo for ModelDownloader / DownloadProgress. Animates a simulated download
 * from 0→100% to show the "Downloading…" → ready transition, plus a standalone
 * DownloadProgress bar and a cache-load card. No real model is downloaded here.
 */
export default function ModelDownloaderDemo() {
  const [fraction, setFraction] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setFraction((f) => (f >= 1 ? 0 : Math.min(1, f + 0.04)));
    }, 200);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="flex w-full max-w-md flex-col gap-6">
      <ModelDownloader
        name="Llama 3.2 1B Instruct"
        size="1.2 GB"
        contextLength={8192}
        category="Chat"
        progress={fraction}
      />
      <ModelDownloader
        name="bge-small-en-v1.5"
        size="34 MB"
        contextLength={512}
        category="Embedding"
        progress={{ percent: 0.6, cached: true }}
      />
      <div className="flex flex-col gap-1.5">
        <p className="text-xs text-muted-foreground">
          Standalone <code className="font-mono">DownloadProgress</code> bar
        </p>
        <DownloadProgress value={fraction} />
      </div>
    </div>
  );
}
```

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 (
    <ModelDownloader
      name="Llama 3.2 1B Instruct"
      size="1.2 GB"
      contextLength={8192}
      category="Chat"
      progress={progressValue}
      ready={status === 'ready'}
    />
  );
}
```

`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
<DownloadProgress value={0.42} />
```

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.