# Device

3 on-device blocks in the Device category — each installs and runs on its own, entirely in the browser.

## Device Report

See what your device and browser can do for on-device AI. It checks your hardware, browser features, and free storage, then tells you whether a small AI model will run here. Nothing is downloaded.

**Install**

```bash
npx shadcn@latest add @localmode/ui/blocks/device/device-report
```

**Full block (all files):** https://localmode.ai/r/ui/blocks/device/device-report.json

```tsx
'use client';

/**
 * @file device-report.tsx
 * @description Self-sufficient zero-download device capability report — hardware, six feature flags, storage, browser/OS, a reference-model readiness verdict, and adaptive batch sizing, all from browser APIs.
 * @constraint Renders entirely from browser APIs — zero network, zero model bytes on mount.
 */

import { useEffect, useState } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { useAdaptiveBatchSize, useCapabilities, useStorageQuota } from '@localmode/react';
import { formatBytes } from '@localmode/core';

import { DeviceBadge } from '@/components/device-badge';
import { DeviceCapabilityGrid } from '@/components/device-capability-grid';
import { BrowserCompatCard } from '@/components/browser-compat-card';
import { StorageMeter } from '@/components/storage-meter';
import { AdaptiveBatchCard } from '@/components/adaptive-batch-card';

const REFERENCE_MODEL_LABEL = 'Readiness check: 1B-parameter Q4_K_M GGUF LLM';
const REFERENCE_MODEL_SHORT = '1B Q4_K_M GGUF LLM';
const REFERENCE_REQUIRED_GB = 1.2;
const ASSUMED_MEMORY_GB = 4;

function sanitizeGpuLabel(raw: string): string {
  let out = raw.trim();
  while ((out.match(/\(/g) ?? []).length > (out.match(/\)/g) ?? []).length) {
    const cut = out.replace(/\s*\([^()]*$/, '').trim();
    if (cut === out) break;
    out = cut;
  }
  return out;
}

function simplifyGpuName(raw: string): string {
  const renderer = raw.match(/Renderer:\s*([^,()]+)/i);
  if (renderer?.[1]) return renderer[1].trim();
  if (/SwiftShader/i.test(raw)) return 'SwiftShader';
  if (/llvmpipe/i.test(raw)) return 'llvmpipe';
  const angle = raw.match(/^ANGLE\s*\((.*)\)\s*$/i);
  if (angle?.[1]) {
    const parts = angle[1]
      .split(',')
      .map((p) => p.trim())
      .filter(Boolean);
    const label = (parts[1] ?? parts[0] ?? '').replace(/\bDirect3D.*$/i, '').trim();
    if (label) return sanitizeGpuLabel(label);
  }
  return sanitizeGpuLabel(raw);
}

export function DeviceReportBlock() {
  const { capabilities, isDetecting, error: capabilitiesError, refresh } = useCapabilities();
  const { quota, error: quotaError, refresh: refreshQuota } = useStorageQuota();
  const batch = useAdaptiveBatchSize({ taskType: 'embedding', modelDimensions: 384 });

  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);

  const error = capabilitiesError ?? quotaError;
  const status = error ? 'error' : isDetecting || !capabilities ? 'detecting' : 'ready';

  const memoryGB = capabilities?.hardware.memory ?? null;
  const gpuName = capabilities?.hardware.gpu ?? null;
  const memoryUnknown = capabilities != null && memoryGB == null;

  const browserLine = capabilities
    ? [
        `${capabilities.browser.name} v${capabilities.browser.version}`,
        `${capabilities.device.os}${capabilities.device.osVersion ? ` ${capabilities.device.osVersion}` : ''}`,
        capabilities.device.type,
      ].join(' · ')
    : null;

  const estimatedSpeed = capabilities
    ? capabilities.features.webgpu
      ? 'Fast (WebGPU available)'
      : capabilities.features.simd
        ? 'Moderate (WASM SIMD)'
        : 'Slow (plain WASM)'
    : undefined;

  const readinessWarnings = memoryUnknown
    ? [
        `Device memory is unknown (navigator.deviceMemory unavailable in this browser) - assuming ${ASSUMED_MEMORY_GB} GB, so this verdict may be conservative or optimistic.`,
      ]
    : [];

  return (
    <div className="flex flex-col gap-4 p-4">
      {}
      <span data-state={status} className="sr-only">
        {status}
      </span>
      {}
      <span aria-label="Device report status" className="sr-only">
        {status}
      </span>
      {capabilities && (
        <span aria-label="Detected capabilities" className="sr-only">
          {JSON.stringify({
            cores: capabilities.hardware.cores,
            memoryGB,
            gpu: gpuName,
            webgpu: capabilities.features.webgpu,
            wasm: capabilities.features.wasm,
            simd: capabilities.features.simd,
            threads: capabilities.features.threads,
            indexeddb: capabilities.features.indexeddb,
            webworkers: capabilities.features.webworkers,
          })}
        </span>
      )}

      {}
      <div className="flex flex-wrap items-center justify-between gap-2">
        <div className="flex flex-wrap items-center gap-2">
          <DeviceBadge capability="webgpu" />
          <DeviceBadge capability="wasm" />
          <DeviceBadge capability="storage" />
        </div>
        {browserLine && (
          <p className="text-xs text-muted-foreground">
            {browserLine}
          </p>
        )}
      </div>

      {error && (
        <div
          className="flex items-center gap-2 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-xs text-destructive"
        >
          <AlertTriangle className="size-4 shrink-0" aria-hidden="true" />
          <span className="min-w-0 flex-1">{error.message}</span>
          <button
            type="button"
            onClick={() => {
              void refresh();
              void refreshQuota();
            }}
            className="inline-flex shrink-0 items-center gap-1 rounded-md border border-border bg-background px-2 py-1 font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            <RefreshCw className="size-3" aria-hidden="true" />
            Retry
          </button>
        </div>
      )}

      <div className="grid items-start gap-4 md:grid-cols-2">
        {}
        <div className="flex flex-col gap-2">
          <div>
            {}
            <DeviceCapabilityGrid className="max-w-none [&_p]:hidden" />
          </div>
          {capabilities && (
            <p className="px-1 text-xs text-muted-foreground">
              GPU:{' '}
              <span className="font-medium text-foreground">
                {gpuName ? simplifyGpuName(gpuName) : 'None'}
              </span>
            </p>
          )}
          {memoryUnknown && (
            <p
              className="flex items-start gap-1.5 px-1 text-xs text-amber-600 dark:text-amber-400"
            >
              <AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
              <span>
                navigator.deviceMemory is unavailable in this browser - device memory is unknown;
                the readiness check assumes {ASSUMED_MEMORY_GB} GB.
              </span>
            </p>
          )}
        </div>

        {}
        <div className="flex flex-col gap-4">
          <div className="flex flex-col gap-2">
            <div>
              <StorageMeter
                className="max-w-none"
                quota={
                  quota ? { usedBytes: quota.usedBytes, quotaBytes: quota.quotaBytes } : undefined
                }
              />
            </div>
            {quota && (
              <p
                aria-label="Storage availability"
                data-available-bytes={quota.availableBytes}
                data-quota-bytes={quota.quotaBytes}
                className="px-1 text-xs text-muted-foreground"
              >
                <span className="font-medium text-foreground">
                  {formatBytes(quota.availableBytes)}
                </span>{' '}
                available of {formatBytes(quota.quotaBytes)} quota
                {quota.isPersisted ? ' · persisted' : ''}
              </p>
            )}
          </div>

          <div className="flex flex-col gap-2">
            {}
            <p className="px-1 text-sm font-medium text-foreground">{REFERENCE_MODEL_LABEL}</p>
            {capabilities ? (
              <BrowserCompatCard
                className="max-w-none"
                modelName={REFERENCE_MODEL_SHORT}
                requiredGB={REFERENCE_REQUIRED_GB}
                deviceGB={memoryGB ?? ASSUMED_MEMORY_GB}
                availableStorageGB={
                  quota ? Math.round((quota.availableBytes / 1024 ** 3) * 10) / 10 : undefined
                }
                crossOriginIsolated={capabilities.features.crossOriginisolated}
                estimatedSpeed={estimatedSpeed}
                warnings={readinessWarnings}
              />
            ) : (
              <div
                role="status"
                className="rounded-xl border border-border bg-card p-5 text-sm text-muted-foreground"
              >
                Checking device readiness…
              </div>
            )}
          </div>

          <div>
            {mounted ? (
              <>
                {}
                <AdaptiveBatchCard
                  className="max-w-none [&_.grid-cols-3]:hidden"
                  result={batch}
                />
                <span
                  aria-label="Adaptive batch size"
                  data-batch-size={batch.batchSize}
                  className="sr-only"
                >
                  {batch.batchSize}
                </span>
                <span aria-label="Batch sizing reasoning" className="sr-only">
                  {batch.reasoning}
                </span>
              </>
            ) : (
              <div
                role="status"
                className="rounded-xl border border-border bg-card p-5 text-sm text-muted-foreground"
              >
                Computing optimal batch size…
              </div>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}
```

## Model Advisor

Find the best on-device model for a task on your hardware. Pick a task to get a ranked list of models that fit your device, compare any two side by side, and add your own model to the list. Nothing is downloaded.

**Install**

```bash
npx shadcn@latest add @localmode/ui/blocks/device/model-advisor
```

**Full block (all files):** https://localmode.ai/r/ui/blocks/device/model-advisor.json

```tsx
'use client';

/**
 * @file model-advisor.tsx
 * @description Self-sufficient block: computes its own device context via useCapabilities and ranks useModelRecommendations over the in-memory registry, with two-model comparison and custom model registration — zero network on mount.
 */

import { useState, type FormEvent, type ReactNode } from 'react';
import { AlertTriangle, Plus, RefreshCw, X } from 'lucide-react';
import {
  getModelRegistry,
  registerModel,
  type ModelRegistryEntry,
  type TaskCategory,
} from '@localmode/core';
import { useCapabilities, useModelRecommendations } from '@localmode/react';

import { ModelRecommendationCard } from '@/components/model-recommendation-card';
import {
  ModelComparisonPanel,
  type ComparisonEntry,
} from '@/components/model-comparison-panel';
import { cn } from '@/lib/utils';

const DEFAULT_TASK: TaskCategory = 'embedding';

const RECOMMENDATION_LIMIT = 24;

const TASK_LABELS: Record<TaskCategory, string> = {
  'embedding': 'Embedding',
  'classification': 'Classification',
  'zero-shot': 'Zero-Shot Classification',
  'ner': 'Named Entity Recognition',
  'reranking': 'Reranking',
  'generation': 'Text Generation',
  'translation': 'Translation',
  'summarization': 'Summarization',
  'fill-mask': 'Fill-Mask',
  'question-answering': 'Question Answering',
  'speech-to-text': 'Speech to Text',
  'text-to-speech': 'Text to Speech',
  'image-classification': 'Image Classification',
  'image-captioning': 'Image Captioning',
  'object-detection': 'Object Detection',
  'segmentation': 'Segmentation',
  'ocr': 'OCR',
  'document-qa': 'Document QA',
  'image-features': 'Image Features',
  'image-to-image': 'Image to Image',
  'multimodal-embedding': 'Multimodal Embedding',
};

const TASK_GROUPS: ReadonlyArray<{ label: string; tasks: readonly TaskCategory[] }> = [
  {
    label: 'Text',
    tasks: [
      'embedding',
      'classification',
      'zero-shot',
      'ner',
      'reranking',
      'fill-mask',
      'question-answering',
    ],
  },
  { label: 'Generation', tasks: ['generation'] },
  { label: 'Translation & Summarization', tasks: ['translation', 'summarization'] },
  {
    label: 'Vision',
    tasks: [
      'image-classification',
      'image-captioning',
      'object-detection',
      'segmentation',
      'ocr',
      'document-qa',
      'image-features',
      'image-to-image',
      'multimodal-embedding',
    ],
  },
  { label: 'Audio', tasks: ['speech-to-text', 'text-to-speech'] },
];

type Recommendation = ReturnType<typeof useModelRecommendations>['recommendations'][number];

function formatSize(sizeMB: number): string {
  if (sizeMB >= 1024) return `${(sizeMB / 1024).toFixed(1)} GB`;
  return `${sizeMB} MB`;
}

function sanitizeGpuLabel(raw: string): string {
  let out = raw.trim();
  while ((out.match(/\(/g) ?? []).length > (out.match(/\)/g) ?? []).length) {
    const cut = out.replace(/\s*\([^()]*$/, '').trim();
    if (cut === out) break;
    out = cut;
  }
  return out;
}

function simplifyGpuName(raw: string): string {
  const renderer = raw.match(/Renderer:\s*([^,()]+)/i);
  if (renderer?.[1]) return renderer[1].trim();
  if (/SwiftShader/i.test(raw)) return 'SwiftShader';
  if (/llvmpipe/i.test(raw)) return 'llvmpipe';
  const angle = raw.match(/^ANGLE\s*\((.*)\)\s*$/i);
  if (angle?.[1]) {
    const parts = angle[1]
      .split(',')
      .map((p) => p.trim())
      .filter(Boolean);
    const label = (parts[1] ?? parts[0] ?? '').replace(/\bDirect3D.*$/i, '').trim();
    if (label) return sanitizeGpuLabel(label);
  }
  return sanitizeGpuLabel(raw);
}

function humanMB(mb: number): string {
  if (!Number.isFinite(mb)) return `${mb} MB`;
  if (mb >= 1024 * 1024) return `${(mb / (1024 * 1024)).toFixed(1)} TB`;
  if (mb >= 1024) return `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} GB`;
  return `${Math.round(mb)} MB`;
}

function humanizeReason(reason: string): string {
  return reason.replace(
    /(\d+(?:\.\d+)?)\s*MB\s+of\s+(\d+(?:\.\d+)?)\s*MB/gi,
    (_m, a: string, b: string) => `${humanMB(Number(a))} of ${humanMB(Number(b))}`,
  );
}

function toComparisonEntry(rec: Recommendation): ComparisonEntry {
  return {
    modelId: rec.entry.modelId,
    name: rec.entry.name,
    score: rec.score,
    sizeMB: rec.entry.sizeMB,
    size: formatSize(rec.entry.sizeMB),
    ...(rec.entry.speedTier ? { speedTier: rec.entry.speedTier } : {}),
    ...(rec.entry.qualityTier ? { qualityTier: rec.entry.qualityTier } : {}),
    ...(rec.entry.recommendedDevice ? { device: rec.entry.recommendedDevice } : {}),
    ...(rec.entry.dimensions != null ? { dimensions: rec.entry.dimensions } : {}),
  };
}


interface RegistrationDraft {
  modelId: string;
  name: string;
  provider: string;
  task: TaskCategory;
  sizeMB: string;
  minMemoryMB: string;
  dimensions: string;
  recommendedDevice: 'webgpu' | 'wasm' | 'cpu';
  speedTier: 'fast' | 'medium' | 'slow';
  qualityTier: 'low' | 'medium' | 'high';
  description: string;
}

const EMPTY_DRAFT: RegistrationDraft = {
  modelId: '',
  name: '',
  provider: '',
  task: DEFAULT_TASK,
  sizeMB: '',
  minMemoryMB: '',
  dimensions: '',
  recommendedDevice: 'wasm',
  speedTier: 'medium',
  qualityTier: 'medium',
  description: '',
};

function validateDraft(draft: RegistrationDraft): Partial<Record<keyof RegistrationDraft, string>> {
  const errors: Partial<Record<keyof RegistrationDraft, string>> = {};
  if (!draft.modelId.trim()) errors.modelId = 'Model id is required.';
  if (!draft.name.trim()) errors.name = 'Name is required.';
  if (!draft.provider.trim()) errors.provider = 'Provider is required.';
  if (!draft.sizeMB.trim()) {
    errors.sizeMB = 'Size is required.';
  } else {
    const size = Number(draft.sizeMB);
    if (!Number.isFinite(size) || size < 1) errors.sizeMB = 'Size must be a number of at least 1 MB.';
  }
  if (draft.minMemoryMB.trim()) {
    const mem = Number(draft.minMemoryMB);
    if (!Number.isFinite(mem) || mem <= 0) errors.minMemoryMB = 'Min memory must be a positive number of MB.';
  }
  if (draft.dimensions.trim()) {
    const dims = Number(draft.dimensions);
    if (!Number.isInteger(dims) || dims <= 0) errors.dimensions = 'Dimensions must be a positive integer.';
  }
  return errors;
}

const INPUT_CLASS =
  'h-8 w-full rounded-md border border-input bg-background px-2 text-sm font-normal ' +
  'focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50';

function Field({
  label,
  name,
  required = false,
  error,
  className,
  children,
}: {
  label: string;
  name: keyof RegistrationDraft;
  required?: boolean;
  error?: string;
  className?: string;
  children: ReactNode;
}) {
  return (
    <label className={cn('flex min-w-0 flex-col gap-1 text-xs font-medium', className)}>
      <span>
        {label}
        {required && <span className="text-destructive"> *</span>}
      </span>
      {children}
      {error && (
        <span
          className="font-normal text-destructive"
        >
          {error}
        </span>
      )}
    </label>
  );
}


export function ModelAdvisorBlock() {
  const [selectedTask, setSelectedTask] = useState<TaskCategory>(DEFAULT_TASK);
  const [compare, setCompare] = useState<ComparisonEntry[]>([]);

  const [registerOpen, setRegisterOpen] = useState(false);
  const [draft, setDraft] = useState<RegistrationDraft>(EMPTY_DRAFT);
  const [fieldErrors, setFieldErrors] = useState<Partial<Record<keyof RegistrationDraft, string>>>({});
  const [formError, setFormError] = useState<string | null>(null);
  const [registerSuccess, setRegisterSuccess] = useState<string | null>(null);

  const { recommendations, isLoading, error, refresh } = useModelRecommendations({
    task: selectedTask,
    limit: RECOMMENDATION_LIMIT,
  });

  const { capabilities: device, isDetecting, refresh: refreshDevice } = useCapabilities();

  const registryTotal = getModelRegistry().filter((e) => e.task === selectedTask).length;

  const deviceSummary = device
    ? [
        `${device.hardware.cores} cores`,
        device.hardware.memory != null ? `${device.hardware.memory} GB RAM` : 'RAM unknown',
        device.hardware.gpu ? `GPU: ${simplifyGpuName(device.hardware.gpu)}` : null,
        device.features.webgpu ? 'WebGPU available' : 'WebGPU unavailable (WASM fallback)',
      ]
        .filter(Boolean)
        .join(' · ')
    : null;

  const onTaskChange = (task: TaskCategory) => {
    setSelectedTask(task);
    setCompare([]);
  };

  const toggleCompare = (modelId: string) => {
    setCompare((prev) => {
      if (prev.some((entry) => entry.modelId === modelId)) {
        return prev.filter((entry) => entry.modelId !== modelId);
      }
      const rec = recommendations.find((r) => r.entry.modelId === modelId);
      if (!rec) return prev;
      const entry = toComparisonEntry(rec);
      if (prev.length < 2) return [...prev, entry];
      const last = prev[prev.length - 1];
      return last ? [last, entry] : [entry];
    });
  };

  const clearCompare = () => setCompare([]);

  const openRegisterForm = () => {
    if (registerOpen) {
      setRegisterOpen(false);
      return;
    }
    setRegisterSuccess(null);
    setFormError(null);
    setFieldErrors({});
    setDraft((d) => ({ ...d, task: selectedTask }));
    setRegisterOpen(true);
  };

  const setDraftField = <K extends keyof RegistrationDraft>(key: K, value: RegistrationDraft[K]) => {
    setDraft((d) => ({ ...d, [key]: value }));
  };

  const submitRegistration = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const errors = validateDraft(draft);
    setFieldErrors(errors);
    setFormError(null);
    if (Object.keys(errors).length > 0) return;

    const entry: ModelRegistryEntry = {
      modelId: draft.modelId.trim(),
      provider: draft.provider.trim(),
      task: draft.task,
      name: draft.name.trim(),
      sizeMB: Number(draft.sizeMB),
      recommendedDevice: draft.recommendedDevice,
      speedTier: draft.speedTier,
      qualityTier: draft.qualityTier,
      ...(draft.minMemoryMB.trim() ? { minMemoryMB: Number(draft.minMemoryMB) } : {}),
      ...(draft.dimensions.trim() ? { dimensions: Number(draft.dimensions) } : {}),
      ...(draft.description.trim() ? { description: draft.description.trim() } : {}),
    };

    try {
      registerModel(entry);
      refresh();
      setRegisterSuccess(
        `Registered "${entry.name}" (${entry.modelId}) for ${TASK_LABELS[entry.task]} - recommendations refreshed.`,
      );
      setDraft({ ...EMPTY_DRAFT, task: selectedTask });
      setFieldErrors({});
      setRegisterOpen(false);
    } catch (err) {
      setFormError(err instanceof Error ? err.message : String(err));
    }
  };

  const taskSelectOptions = () =>
    TASK_GROUPS.map((group) => (
      <optgroup key={group.label} label={group.label}>
        {group.tasks.map((task) => (
          <option key={task} value={task}>
            {TASK_LABELS[task]}
          </option>
        ))}
      </optgroup>
    ));

  const [compareA, compareB] = compare;

  return (
    <section className="flex flex-col gap-4 p-4">
      {}
      <span
        data-state={isDetecting ? 'loading' : 'ready'}
        className="sr-only"
      />
      {}
      <div className="flex flex-col gap-2">
        <div className="flex flex-wrap items-end gap-2">
          <label className="flex flex-col gap-1 text-xs font-medium">
            <span>Task</span>
            <select
              aria-label="Task category"
              value={selectedTask}
              onChange={(e) => onTaskChange(e.target.value as TaskCategory)}
              className={cn(INPUT_CLASS, 'w-auto min-w-56')}
            >
              {taskSelectOptions()}
            </select>
          </label>
          <button
            type="button"
            aria-expanded={registerOpen}
            onClick={openRegisterForm}
            className="inline-flex h-8 items-center gap-1 rounded-md border border-border bg-background px-3 text-sm font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            {registerOpen ? (
              <X className="size-3.5" aria-hidden="true" />
            ) : (
              <Plus className="size-3.5" aria-hidden="true" />
            )}
            {registerOpen ? 'Close' : 'Register custom model'}
          </button>
        </div>
        {deviceSummary && (
          <p className="text-xs text-muted-foreground">
            Ranked for your device - {deviceSummary}
          </p>
        )}
        {registerSuccess && (
          <p
            role="status"
            aria-live="polite"
            className="text-xs text-emerald-600 dark:text-emerald-400"
          >
            {registerSuccess}
          </p>
        )}
      </div>

      {}
      {error && (
        <div
          role="alert"
          className="flex flex-wrap items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive"
        >
          <AlertTriangle className="size-4 shrink-0" aria-hidden="true" />
          <span className="min-w-0 flex-1">Could not compute recommendations: {error.message}</span>
          <button
            type="button"
            onClick={() => {
              refresh();
              void refreshDevice();
            }}
            className="inline-flex h-7 items-center gap-1 rounded-md border border-destructive/40 bg-background px-2 text-xs font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            <RefreshCw className="size-3" aria-hidden="true" />
            Retry
          </button>
        </div>
      )}

      {}
      {registerOpen && (
        <form
          aria-label="Register a custom model"
          noValidate
          onSubmit={submitRegistration}
          className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm"
        >
          <div className="flex items-center justify-between gap-2">
            <h2 className="text-sm font-semibold">Register a custom model</h2>
          </div>
          <p
            className="rounded-md border border-amber-500/30 bg-amber-500/10 px-2 py-1.5 text-xs text-amber-700 dark:text-amber-300"
          >
            Custom models are registered in-memory only - they are lost on page refresh.
          </p>

          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
            <Field label="Model id" name="modelId" required error={fieldErrors.modelId}>
              <input
                value={draft.modelId}
                onChange={(e) => setDraftField('modelId', e.target.value)}
                placeholder="org/my-custom-model"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Name" name="name" required error={fieldErrors.name}>
              <input
                value={draft.name}
                onChange={(e) => setDraftField('name', e.target.value)}
                placeholder="My Custom Model"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Provider" name="provider" required error={fieldErrors.provider}>
              <input
                value={draft.provider}
                onChange={(e) => setDraftField('provider', e.target.value)}
                placeholder="transformers, wllama, custom…"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Task" name="task" required error={fieldErrors.task}>
              <select
                value={draft.task}
                onChange={(e) => setDraftField('task', e.target.value as TaskCategory)}
                className={INPUT_CLASS}
              >
                {taskSelectOptions()}
              </select>
            </Field>
            <Field label="Size (MB)" name="sizeMB" required error={fieldErrors.sizeMB}>
              <input
                type="number"
                min={1}
                value={draft.sizeMB}
                onChange={(e) => setDraftField('sizeMB', e.target.value)}
                placeholder="50"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Min memory (MB, optional)" name="minMemoryMB" error={fieldErrors.minMemoryMB}>
              <input
                type="number"
                min={1}
                value={draft.minMemoryMB}
                onChange={(e) => setDraftField('minMemoryMB', e.target.value)}
                placeholder="512"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Dimensions (optional)" name="dimensions" error={fieldErrors.dimensions}>
              <input
                type="number"
                min={1}
                value={draft.dimensions}
                onChange={(e) => setDraftField('dimensions', e.target.value)}
                placeholder="384"
                className={INPUT_CLASS}
              />
            </Field>
            <Field label="Recommended device" name="recommendedDevice" required error={fieldErrors.recommendedDevice}>
              <select
                value={draft.recommendedDevice}
                onChange={(e) =>
                  setDraftField('recommendedDevice', e.target.value as RegistrationDraft['recommendedDevice'])
                }
                className={INPUT_CLASS}
              >
                <option value="webgpu">webgpu</option>
                <option value="wasm">wasm</option>
                <option value="cpu">cpu</option>
              </select>
            </Field>
            <Field label="Speed tier" name="speedTier" required error={fieldErrors.speedTier}>
              <select
                value={draft.speedTier}
                onChange={(e) => setDraftField('speedTier', e.target.value as RegistrationDraft['speedTier'])}
                className={INPUT_CLASS}
              >
                <option value="fast">fast</option>
                <option value="medium">medium</option>
                <option value="slow">slow</option>
              </select>
            </Field>
            <Field label="Quality tier" name="qualityTier" required error={fieldErrors.qualityTier}>
              <select
                value={draft.qualityTier}
                onChange={(e) => setDraftField('qualityTier', e.target.value as RegistrationDraft['qualityTier'])}
                className={INPUT_CLASS}
              >
                <option value="low">low</option>
                <option value="medium">medium</option>
                <option value="high">high</option>
              </select>
            </Field>
            <Field
              label="Description (optional)"
              name="description"
              error={fieldErrors.description}
              className="sm:col-span-2 lg:col-span-3"
            >
              <input
                value={draft.description}
                onChange={(e) => setDraftField('description', e.target.value)}
                placeholder="One-line description shown on the recommendation card"
                className={INPUT_CLASS}
              />
            </Field>
          </div>

          {formError && (
            <p role="alert" className="text-xs text-destructive">
              Registration failed: {formError}
            </p>
          )}

          <div className="flex items-center gap-2">
            <button
              type="submit"
              className="inline-flex h-8 items-center rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              Register model
            </button>
            <button
              type="button"
              onClick={() => setRegisterOpen(false)}
              className="inline-flex h-8 items-center rounded-md border border-border bg-background px-3 text-sm transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              Cancel
            </button>
          </div>
        </form>
      )}

      {}
      {compare.length > 0 && (
        <div className="flex flex-col gap-2">
          <div className="flex items-center justify-between gap-2">
            <h2 className="text-sm font-semibold">Compare models ({compare.length}/2)</h2>
            <button
              type="button"
              onClick={clearCompare}
              className="inline-flex h-7 items-center rounded-md border border-border bg-background px-2 text-xs font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              Clear comparison
            </button>
          </div>
          {compareA && compareB ? (
            <div role="group" aria-label="Model comparison">
              <ModelComparisonPanel entries={[compareA, compareB]} onClear={clearCompare} />
            </div>
          ) : (
            <p className="text-xs text-muted-foreground">
              Select one more model with a card&apos;s Compare toggle to see the side-by-side comparison.
            </p>
          )}
        </div>
      )}

      {}
      <div className="flex flex-col gap-2">
        {isLoading ? (
          <p role="status" className="text-xs text-muted-foreground">
            Detecting device capabilities…
          </p>
        ) : (
          !error && (
            <p className="text-sm">
              <span aria-label="Recommendation count" className="font-semibold tabular-nums">
                {recommendations.length}
              </span>{' '}
              of {registryTotal} registry {registryTotal === 1 ? 'model' : 'models'} fit this device for{' '}
              {TASK_LABELS[selectedTask]}
            </p>
          )
        )}

        {}
        <ul
          aria-label="Ranked model recommendations"
          className="grid min-w-0 list-none items-start gap-4 p-0 lg:grid-cols-2"
        >
          {recommendations.map((rec) => {
            const comparing = compare.some((entry) => entry.modelId === rec.entry.modelId);
            return (
              <li
                key={rec.entry.modelId}
                aria-label={rec.entry.name}
                data-model-id={rec.entry.modelId}
                data-score={String(rec.score)}
                className="min-w-0"
              >
                <ModelRecommendationCard
                  className="max-w-none"
                  comparing={comparing}
                  onToggleCompare={toggleCompare}
                  recommendation={{
                    modelId: rec.entry.modelId,
                    name: rec.entry.name,
                    provider: rec.entry.provider,
                    size:
                      rec.entry.dimensions != null
                        ? `${formatSize(rec.entry.sizeMB)} · ${rec.entry.dimensions} dims`
                        : formatSize(rec.entry.sizeMB),
                    score: rec.score,
                    recommendedDevice: rec.entry.recommendedDevice,
                    speedTier: rec.entry.speedTier,
                    qualityTier: rec.entry.qualityTier,
                    ...(rec.entry.description ? { description: rec.entry.description } : {}),
                    reasons: rec.reasons.map(humanizeReason),
                  }}
                />
              </li>
            );
          })}
        </ul>

        {!isLoading && !error && recommendations.length === 0 && (
          <div
            className="rounded-xl border border-dashed border-border bg-card px-4 py-6 text-center text-sm text-muted-foreground"
          >
            {registryTotal === 0
              ? `The registry has no models for ${TASK_LABELS[selectedTask]} yet - register a custom model to see it ranked here, or try a different task.`
              : `No registry models fit ${TASK_LABELS[selectedTask]} on this device - try a different task or register a custom model.`}
          </div>
        )}
      </div>
    </section>
  );
}
```

## GGUF Explorer

Search over 160,000 GGUF models on HuggingFace and peek inside any file to see its size, details, and whether it will run in your browser. Send a model straight to the chat block when you find one you like. No model download required.

**Install**

```bash
npx shadcn@latest add @localmode/ui/blocks/device/gguf-explorer
```

**Full block (all files):** https://localmode.ai/r/ui/blocks/device/gguf-explorer.json

```tsx
'use client';

/**
 * @file gguf-explorer.tsx
 * @description Self-sufficient block: HF GGUF search + curated wllama browse + ~4KB Range-read metadata inspection + browser-compat verdict + chat handoff, consuming the promoted `@localmode/wllama` discovery utils (searchGGUFModels/listGGUFFiles).
 * @constraint GGUF inspection is a user-initiated ~4KB HTTP Range metadata read — never a model download.
 */

import { useEffect, useRef, useState } from 'react';
import {
  checkGGUFBrowserCompatFromURL,
  deleteModelCache,
  getModelCategory,
  HFApiError,
  isModelCached,
  listGGUFFiles,
  preloadModel,
  searchGGUFModels,
  WLLAMA_MODELS,
  type GGUFBrowserCompat,
  type GGUFMetadata,
  type HFModelFile,
  type HFModelSearchResult,
  type HFSort,
  type WllamaModelEntry,
  type WllamaModelId,
} from '@localmode/wllama';
import {
  AlertTriangle,
  CloudDownload,
  Globe,
  Loader2,
  MessageSquare,
  RotateCw,
  X,
} from 'lucide-react';

import {
  ModelSearchBrowser,
  type ModelRepoFile,
  type ModelSearchResult,
  type ModelSearchSort,
} from '@/components/model-search-browser';
import { ModelCatalogCard, type CatalogEntry } from '@/components/model-catalog-card';
import { GGUFMetadataCard } from '@/components/model-metadata-card';
import { BrowserCompatCard } from '@/components/browser-compat-card';
import { CapabilityGate } from '@/components/capability-gate';
import { ModelDownloader } from '@/components/model-downloader';

function buildChatHandoffUrl(model: string, opts?: { mmproj?: string }): string {
  let url = `/blocks/chat?model=${encodeURIComponent(model)}`;
  if (opts?.mmproj) {
    url += `&mmproj=${encodeURIComponent(opts.mmproj)}`;
  }
  return url;
}


const GIB = 1024 * 1024 * 1024;

const SHORTHAND_EXAMPLE =
  'bartowski/Llama-3.2-1B-Instruct-GGUF:Llama-3.2-1B-Instruct-Q4_K_M.gguf';

interface CuratedModel {
  id: WllamaModelId;
  entry: WllamaModelEntry;
}

const ALL_CURATED: CuratedModel[] = Object.entries(WLLAMA_MODELS).map(([id, entry]) => ({
  id: id as WllamaModelId,
  entry: entry as WllamaModelEntry,
}));

const GROUP_META: { category: ReturnType<typeof getModelCategory>; title: string }[] = [
  { category: 'tiny', title: 'Tiny - under 500 MB' },
  { category: 'small', title: 'Small - 500 MB to 1 GB' },
  { category: 'medium', title: 'Medium - 1 to 2 GB' },
  { category: 'large', title: 'Large - 2 GB and up' },
];

const CURATED_GROUPS = GROUP_META.map((group) => ({
  ...group,
  models: ALL_CURATED.filter((m) => getModelCategory(m.entry.sizeBytes) === group.category),
})).filter((group) => group.models.length > 0);


function formatParams(count: number): string {
  if (count >= 1_000_000_000) return `${(count / 1_000_000_000).toFixed(2)}B`;
  if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(0)}M`;
  if (count >= 1_000) return `${(count / 1_000).toFixed(0)}K`;
  return String(count);
}

function formatBytes(bytes: number): string {
  if (bytes >= GIB) return `${(bytes / GIB).toFixed(2)} GB`;
  const mib = 1024 * 1024;
  if (bytes >= mib) return `${(bytes / mib).toFixed(0)} MB`;
  return `${(bytes / 1024).toFixed(0)} KB`;
}

function toGB(bytes: number): number {
  return Math.round((bytes / GIB) * 100) / 100;
}

function hfFileUrl(repoId: string, filename: string): string {
  return `https://huggingface.co/${repoId}/resolve/main/${filename}`;
}

function isAbortError(err: unknown): boolean {
  return (
    (err instanceof DOMException && err.name === 'AbortError') ||
    (err instanceof Error && err.name === 'AbortError')
  );
}

interface SearchErrorInfo {
  kind: 'rate-limit' | 'network' | 'not-found' | 'unknown';
  message: string;
}

function describeHFError(err: unknown): SearchErrorInfo {
  if (err instanceof HFApiError) {
    if (err.kind === 'rate-limit') {
      return {
        kind: 'rate-limit',
        message: `HuggingFace rate limit reached - wait a moment and retry. (${err.message})`,
      };
    }
    if (err.kind === 'not-found') {
      return { kind: 'not-found', message: `Not found on HuggingFace. (${err.message})` };
    }
    return {
      kind: 'network',
      message: `Network error reaching HuggingFace - check your connection and retry. (${err.message})`,
    };
  }
  return { kind: 'unknown', message: err instanceof Error ? err.message : String(err) };
}

function toSearchRow(r: HFModelSearchResult): ModelSearchResult {
  return {
    repoId: r.repoId,
    author: r.author,
    downloads: r.downloads,
    likes: r.likes,
    lastModified: r.lastModified,
    tags: r.tags,
  };
}

function toRepoFile(f: HFModelFile): ModelRepoFile {
  return { filename: f.filename, sizeBytes: f.sizeBytes, quantLabel: f.quantLabel };
}

function toCatalogEntry(id: WllamaModelId, entry: WllamaModelEntry): CatalogEntry {
  return {
    id,
    name: entry.name,
    size: entry.size,
    description: entry.description,
    architecture: entry.architecture,
    parameters: formatParams(entry.parameterCount),
    quantization: entry.quantization,
    contextLength: entry.contextLength,
    tools: entry.supportsToolCalling,
    vision: entry.vision,
    embedding: entry.isEmbeddingModel,
    reranking: entry.isRerankerModel,
    reasoning: entry.supportsReasoning,
  };
}


interface InspectTarget {
  input: string;
  label: string;
  source: 'curated' | 'custom' | 'huggingface';
  entry?: WllamaModelEntry;
}

type Inspection =
  | { status: 'idle' }
  | { status: 'inspecting' }
  | { status: 'done'; result: GGUFBrowserCompat & { metadata: GGUFMetadata } }
  | { status: 'error'; message: string };

type Prefetch =
  | { status: 'idle' }
  | { status: 'downloading'; phase: 'model' | 'mmproj'; loaded: number; total: number }
  | { status: 'done' }
  | { status: 'error'; message: string };


export function GgufExplorerBlock() {
  const [hfActivated, setHfActivated] = useState(false);
  const [query, setQuery] = useState('');
  const [sort, setSort] = useState<HFSort>('downloads');
  const [results, setResults] = useState<ModelSearchResult[]>([]);
  const [nextCursor, setNextCursor] = useState<string | null>(null);
  const [isSearching, setIsSearching] = useState(false);
  const [searchError, setSearchError] = useState<SearchErrorInfo | null>(null);
  const [expandedRepoId, setExpandedRepoId] = useState<string | undefined>(undefined);
  const [files, setFiles] = useState<ModelRepoFile[] | undefined>(undefined);
  const [filesLoading, setFilesLoading] = useState(false);
  const [filesError, setFilesError] = useState<string | undefined>(undefined);
  const searchAbort = useRef<AbortController | null>(null);
  const searchSeq = useRef(0);
  const filesAbort = useRef<AbortController | null>(null);

  const [customInput, setCustomInput] = useState('');

  const [target, setTarget] = useState<InspectTarget | null>(null);
  const [inspection, setInspection] = useState<Inspection>({ status: 'idle' });
  const inspectAbort = useRef<AbortController | null>(null);

  const [tryAnyway, setTryAnyway] = useState(false);
  const [cached, setCached] = useState<boolean | null>(null);
  const [prefetch, setPrefetch] = useState<Prefetch>({ status: 'idle' });
  const prefetchSeq = useRef(0);


  const executeSearch = async (opts: { append?: boolean; cursor?: string } = {}) => {
    searchAbort.current?.abort();
    const controller = new AbortController();
    searchAbort.current = controller;
    const seq = ++searchSeq.current;
    setIsSearching(true);
    setSearchError(null);
    if (!opts.append) {
      setExpandedRepoId(undefined);
      setFiles(undefined);
      setFilesError(undefined);
    }
    try {
      const page = await searchGGUFModels({
        query: query.trim(),
        sort,
        ...(opts.cursor ? { cursor: opts.cursor } : {}),
        abortSignal: controller.signal,
      });
      if (seq !== searchSeq.current || controller.signal.aborted) return;
      setResults((prev) =>
        opts.append ? [...prev, ...page.results.map(toSearchRow)] : page.results.map(toSearchRow),
      );
      setNextCursor(page.nextCursor);
      setIsSearching(false);
    } catch (err) {
      if (seq !== searchSeq.current || isAbortError(err)) return;
      setIsSearching(false);
      setSearchError(describeHFError(err));
    }
  };

  useEffect(() => {
    if (!hfActivated) return;
    const timer = setTimeout(() => {
      void executeSearch();
    }, 300);
    return () => clearTimeout(timer);
  }, [query, sort, hfActivated]);

  useEffect(() => {
    return () => {
      searchAbort.current?.abort();
      filesAbort.current?.abort();
      inspectAbort.current?.abort();
      prefetchSeq.current += 1;
    };
  }, []);

  const handleQueryChange = (q: string) => {
    setQuery(q);
    setHfActivated(true);
  };

  const handleSortChange = (s: ModelSearchSort) => {
    setSort(s);
    setHfActivated(true);
  };

  const handleSelectRepo = (repoId: string | null) => {
    if (repoId === null || expandedRepoId === repoId) {
      filesAbort.current?.abort();
      setExpandedRepoId(undefined);
      setFiles(undefined);
      setFilesError(undefined);
      setFilesLoading(false);
      return;
    }
    filesAbort.current?.abort();
    const controller = new AbortController();
    filesAbort.current = controller;
    setExpandedRepoId(repoId);
    setFiles(undefined);
    setFilesError(undefined);
    setFilesLoading(true);
    listGGUFFiles(repoId, { abortSignal: controller.signal })
      .then((fs) => {
        if (controller.signal.aborted) return;
        setFiles(fs.map(toRepoFile));
        setFilesLoading(false);
      })
      .catch((err: unknown) => {
        if (controller.signal.aborted || isAbortError(err)) return;
        setFilesLoading(false);
        setFilesError(describeHFError(err).message);
      });
  };

  const handleSelectFile = (repoId: string, file: ModelRepoFile) => {
    inspect({
      input: hfFileUrl(repoId, file.filename),
      label: `${repoId} · ${file.filename}`,
      source: 'huggingface',
    });
  };


  const inspect = (nextTarget: InspectTarget) => {
    inspectAbort.current?.abort();
    const controller = new AbortController();
    inspectAbort.current = controller;
    prefetchSeq.current += 1;
    setPrefetch({ status: 'idle' });
    setTryAnyway(false);
    setCached(null);
    setTarget(nextTarget);
    setInspection({ status: 'inspecting' });
    void (async () => {
      try {
        const result = await checkGGUFBrowserCompatFromURL(nextTarget.input, {
          abortSignal: controller.signal,
        });
        if (controller.signal.aborted) return;
        setInspection({ status: 'done', result });
        const alreadyCached = await isModelCached(nextTarget.input).catch(() => false);
        if (!controller.signal.aborted) setCached(alreadyCached);
      } catch (err) {
        if (controller.signal.aborted || isAbortError(err)) return;
        setInspection({
          status: 'error',
          message: err instanceof Error ? err.message : String(err),
        });
      }
    })();
  };

  const handleCuratedClick = (modelId: string) => {
    const curated = ALL_CURATED.find((m) => m.id === modelId);
    if (!curated) return;
    inspect({
      input: curated.entry.url,
      label: curated.entry.name,
      source: 'curated',
      entry: curated.entry,
    });
  };

  const handleCustomInspect = () => {
    const trimmed = customInput.trim();
    if (!trimmed) return;
    inspect({ input: trimmed, label: trimmed, source: 'custom' });
  };


  const startPrefetch = () => {
    if (!target || inspection.status !== 'done' || prefetch.status === 'downloading') return;
    const model = target;
    const seq = ++prefetchSeq.current;
    setPrefetch({
      status: 'downloading',
      phase: 'model',
      loaded: 0,
      total: inspection.result.metadata.fileSize,
    });
    void (async () => {
      try {
        await preloadModel(model.input, {
          onProgress: (p) => {
            if (seq !== prefetchSeq.current) return;
            setPrefetch({
              status: 'downloading',
              phase: 'model',
              loaded: p.loaded ?? 0,
              total: p.total ?? 0,
            });
          },
        });
        if (seq !== prefetchSeq.current) {
          await deleteModelCache(model.input).catch(() => {});
          return;
        }
        const mmproj = model.entry?.mmprojUrl;
        if (mmproj) {
          setPrefetch({ status: 'downloading', phase: 'mmproj', loaded: 0, total: 0 });
          await preloadModel(mmproj, {
            onProgress: (p) => {
              if (seq !== prefetchSeq.current) return;
              setPrefetch({
                status: 'downloading',
                phase: 'mmproj',
                loaded: p.loaded ?? 0,
                total: p.total ?? 0,
              });
            },
          });
          if (seq !== prefetchSeq.current) {
            await deleteModelCache(mmproj).catch(() => {});
            return;
          }
        }
        setPrefetch({ status: 'done' });
        setCached(true);
      } catch (err) {
        if (seq !== prefetchSeq.current) return;
        setPrefetch({
          status: 'error',
          message: err instanceof Error ? err.message : String(err),
        });
      }
    })();
  };

  const cancelPrefetch = () => {
    prefetchSeq.current += 1;
    setPrefetch({ status: 'idle' });
  };


  const compat = inspection.status === 'done' ? inspection.result : null;
  const meta = compat?.metadata ?? null;
  const handoffHref = target
    ? buildChatHandoffUrl(
        target.input,
        target.entry?.mmprojUrl ? { mmproj: target.entry.mmprojUrl } : undefined,
      )
    : null;


  return (
    <div className="flex flex-col gap-10 p-4">
      {}
      <span data-state="ready" className="sr-only" />

      {}
      <section aria-label="Inspect a custom GGUF" className="flex flex-col gap-2">
        <h2 className="text-sm font-semibold">Inspect any GGUF</h2>
        <div className="flex flex-wrap items-center gap-2">
          <input
            aria-label="GGUF URL or HuggingFace shorthand"
            value={customInput}
            onChange={(e) => setCustomInput(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter') handleCustomInspect();
            }}
            placeholder="https://huggingface.co/…/resolve/main/model.gguf or repo/name:file.gguf"
            className="h-9 min-w-72 flex-1 rounded-md border border-input bg-background px-3 text-sm focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          />
          <button
            type="button"
            onClick={handleCustomInspect}
            disabled={!customInput.trim()}
            className="inline-flex h-9 items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground disabled:opacity-50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            Inspect
          </button>
        </div>
        <p className="text-xs text-muted-foreground">
          Accepts full URLs or HuggingFace shorthand - e.g.{' '}
          <code className="break-all rounded bg-muted px-1 py-0.5 text-[11px] [overflow-wrap:anywhere]">
            {SHORTHAND_EXAMPLE}
          </code>
          . Inspection reads ~4 KB of the file header, never the model.
        </p>
      </section>

      {}
      <section
        aria-label="Curated wllama model catalog"
        className="flex flex-col gap-4"
      >
        <div className="flex items-baseline justify-between gap-2">
          <h2 className="text-sm font-semibold">Curated catalog</h2>
          <span className="text-xs text-muted-foreground">
            {ALL_CURATED.length} models verified for browser inference
          </span>
        </div>
        {CURATED_GROUPS.map((group) => (
          <div key={group.category} className="flex flex-col gap-2">
            <h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
              {group.title}
            </h3>
            <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
              {group.models.map(({ id, entry }) => (
                <div key={id} data-model-id={id}>
                  <ModelCatalogCard
                    entry={toCatalogEntry(id, entry)}
                    selected={target?.input === entry.url}
                    onClick={handleCuratedClick}
                    className="h-full max-w-none"
                  />
                </div>
              ))}
            </div>
          </div>
        ))}
      </section>

      {}
      <section aria-label="Browse HuggingFace GGUF models" className="flex flex-col gap-3">
        <div className="flex items-baseline justify-between gap-2">
          <h2 className="text-sm font-semibold">HuggingFace GGUF catalog</h2>
          <span className="text-xs text-muted-foreground">160,000+ GGUF models</span>
        </div>

        {!hfActivated && (
          <div className="flex flex-wrap items-center gap-3">
            <button
              type="button"
              onClick={() => setHfActivated(true)}
              className="inline-flex h-9 items-center gap-1.5 rounded-md border border-border bg-background px-4 text-sm font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              <Globe className="size-4" aria-hidden="true" />
              Browse HuggingFace
            </button>
            <p className="text-xs text-muted-foreground">
              Nothing is fetched from huggingface.co until you browse, type, or sort.
            </p>
          </div>
        )}

        {searchError && (
          <div
            role="alert"
            data-kind={searchError.kind}
            className="flex flex-wrap items-center gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs"
          >
            <AlertTriangle className="size-3.5 shrink-0 text-destructive" aria-hidden="true" />
            <span className="min-w-fit whitespace-nowrap rounded-full border border-destructive/40 px-1.5 py-0.5 font-medium uppercase tracking-wide text-destructive">
              {searchError.kind}
            </span>
            <span className="min-w-0 flex-1 text-destructive">{searchError.message}</span>
            <button
              type="button"
              onClick={() => void executeSearch()}
              className="inline-flex h-7 items-center gap-1 rounded-md border border-border bg-background px-2 font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              <RotateCw className="size-3" aria-hidden="true" />
              Retry
            </button>
          </div>
        )}

        {}
        {hfActivated && (
          <div className="w-full">
            <ModelSearchBrowser
              className="max-w-none"
              query={query}
              onQueryChange={handleQueryChange}
              sort={sort}
              onSortChange={handleSortChange}
              results={results}
              isLoading={isSearching}
              expandedRepoId={expandedRepoId}
              onSelectRepo={handleSelectRepo}
              files={files}
              filesLoading={filesLoading}
              filesError={filesError}
              onSelectFile={handleSelectFile}
            />
          </div>
        )}

        {hfActivated && nextCursor && (
          <button
            type="button"
            onClick={() => void executeSearch({ append: true, cursor: nextCursor })}
            disabled={isSearching}
            className="inline-flex h-8 items-center self-start rounded-md border border-border px-3 text-sm hover:bg-accent disabled:opacity-50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
          >
            {isSearching ? 'Loading…' : 'Load more'}
          </button>
        )}
      </section>

      {}
      <section aria-label="GGUF inspector" className="flex flex-col gap-4">
        <h2 className="text-sm font-semibold">Inspector</h2>

        {target === null && (
          <p className="text-sm text-muted-foreground">
            Pick a curated model, select a HuggingFace file, or paste a URL to inspect its
            GGUF header and check whether it can run on this device.
          </p>
        )}

        {target && (
          <p className="break-all text-xs text-muted-foreground [overflow-wrap:anywhere]">
            Inspecting: <span className="font-medium text-foreground">{target.label}</span>
          </p>
        )}

        {inspection.status === 'inspecting' && (
          <p
            role="status"
            className="flex items-center gap-2 text-sm text-muted-foreground"
          >
            <Loader2 className="size-4 animate-spin" aria-hidden="true" />
            Reading GGUF header (~4 KB HTTP Range request - the model file is not
            downloaded)…
          </p>
        )}

        {inspection.status === 'error' && (
          <div
            role="alert"
            className="flex flex-col gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2"
          >
            {}
            <p className="text-xs text-destructive">{inspection.message}</p>
            <button
              type="button"
              onClick={() => target && inspect(target)}
              className="inline-flex h-7 items-center gap-1 self-start rounded-md border border-border bg-background px-2 text-xs font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
            >
              <RotateCw className="size-3" aria-hidden="true" />
              Retry
            </button>
          </div>
        )}

        {compat && meta && target && (
          <div className="flex flex-col gap-4">
            <div className="grid items-start gap-4 lg:grid-cols-2">
              {}
              <div
                role="group"
                aria-label="Model metadata"
                className="flex flex-col gap-3"
              >
                <GGUFMetadataCard
                  className="max-w-none"
                  metadata={{
                    architecture: meta.architecture,
                    parameters: formatParams(meta.parameterCount),
                    quantization: meta.quantization,
                    contextLength: meta.contextLength,
                    embeddingDimension: meta.embeddingLength,
                    vocabSize: meta.vocabSize,
                    headCount: meta.headCount,
                    layerCount: meta.layerCount,
                    fileSizeBytes: meta.fileSize,
                    author: meta.author,
                    license: meta.license,
                  }}
                />
                {}
                {(meta.modelName ||
                  meta.description ||
                  target.entry?.dimensions != null ||
                  target.entry?.nGpuLayers != null ||
                  target.entry?.mmprojUrl) && (
                  <div className="rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm">
                    <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                      Additional details
                    </p>
                    <dl className="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1.5 text-xs">
                      {meta.modelName && (
                        <>
                          <dt className="text-muted-foreground">Name</dt>
                          <dd className="break-words [overflow-wrap:anywhere]">
                            {meta.modelName}
                          </dd>
                        </>
                      )}
                      {meta.description && (
                        <>
                          <dt className="text-muted-foreground">Description</dt>
                          <dd className="break-words [overflow-wrap:anywhere]">
                            {meta.description}
                          </dd>
                        </>
                      )}
                      {target.entry?.dimensions != null && (
                        <>
                          <dt className="text-muted-foreground">Embedding dims</dt>
                          <dd>{target.entry.dimensions}</dd>
                        </>
                      )}
                      {target.entry?.nGpuLayers != null && (
                        <>
                          <dt className="text-muted-foreground">GPU layers</dt>
                          <dd>{target.entry.nGpuLayers}</dd>
                        </>
                      )}
                      {target.entry?.mmprojUrl && (
                        <>
                          <dt className="text-muted-foreground">Vision projector</dt>
                          <dd>mmproj available - included in the chat handoff</dd>
                        </>
                      )}
                    </dl>
                  </div>
                )}
              </div>

              {}
              <div
                role="group"
                aria-label="Device compatibility"
                data-can-run={String(compat.canRun)}
                className="flex flex-col gap-3"
              >
                <BrowserCompatCard
                  className="max-w-none"
                  modelName={meta.modelName ?? target.label}
                  requiredGB={toGB(compat.estimatedRAM)}
                  deviceGB={toGB(compat.deviceRAM ?? 4 * GIB)}
                  availableStorageGB={
                    compat.availableStorage != null ? toGB(compat.availableStorage) : undefined
                  }
                  crossOriginIsolated={compat.hasCORS}
                  estimatedSpeed={compat.estimatedSpeed}
                  warnings={compat.warnings}
                  canRun={compat.canRun}
                />
                {}
                {compat.recommendations.length > 0 && (
                  <div className="rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm">
                    <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                      Recommendations
                    </p>
                    <ul
                      className="flex list-disc flex-col gap-1 pl-4 text-xs text-muted-foreground"
                    >
                      {compat.recommendations.map((r, i) => (
                        <li key={i}>{r}</li>
                      ))}
                    </ul>
                  </div>
                )}
              </div>
            </div>

            {}
            <div className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm">
              <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
                Use this model
              </p>

              <CapabilityGate
                requires="wasm"
                fallback={
                  <p className="text-xs text-muted-foreground">
                    Running GGUF models requires WebAssembly, which this browser does not
                    support - the chat handoff is unavailable.
                  </p>
                }
              >
                {compat.canRun || tryAnyway ? (
                  <div className="flex flex-wrap items-center gap-3">
                    <a
                      href={handoffHref ?? '#'}
                      className="inline-flex h-9 items-center gap-1.5 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
                    >
                      <MessageSquare className="size-4" aria-hidden="true" />
                      Chat with this model
                    </a>
                    {!compat.canRun && tryAnyway && (
                      <span className="text-xs text-amber-600 dark:text-amber-400">
                        Compatibility warning overridden - inference may fail or be slow.
                      </span>
                    )}
                  </div>
                ) : (
                  <div className="flex flex-col gap-2 rounded-md border border-amber-500/40 bg-amber-500/5 px-3 py-2">
                    <p className="flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400">
                      <AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
                      This model is not recommended on this device - see the warnings
                      above. You can still try it.
                    </p>
                    <button
                      type="button"
                      onClick={() => setTryAnyway(true)}
                      className="inline-flex h-8 items-center self-start rounded-md border border-border bg-background px-3 text-xs font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
                    >
                      Try anyway
                    </button>
                  </div>
                )}
              </CapabilityGate>

              <div className="flex flex-col gap-2 border-t border-border pt-3">
                <div className="flex flex-wrap items-center gap-3">
                  {(prefetch.status === 'idle' || prefetch.status === 'error') && (
                    <button
                      type="button"
                      onClick={startPrefetch}
                      className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
                    >
                      <CloudDownload className="size-3.5" aria-hidden="true" />
                      Download to cache
                    </button>
                  )}
                  {cached === true && prefetch.status !== 'downloading' && (
                    <span className="text-xs text-emerald-600 dark:text-emerald-400">
                      Already in the wllama cache - chat opens instantly.
                    </span>
                  )}
                  {prefetch.status === 'downloading' && (
                    <button
                      type="button"
                      onClick={cancelPrefetch}
                      className="inline-flex h-8 items-center gap-1 rounded-md border border-border bg-background px-3 text-xs font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
                    >
                      <X className="size-3.5" aria-hidden="true" />
                      Cancel
                    </button>
                  )}
                </div>

                {prefetch.status === 'error' && (
                  <p className="text-xs text-destructive">{prefetch.message}</p>
                )}

                {(prefetch.status === 'downloading' || prefetch.status === 'done') && (
                  <div>
                    <ModelDownloader
                      className="max-w-none"
                      name={
                        prefetch.status === 'downloading' && prefetch.phase === 'mmproj'
                          ? `${target.label} - vision projector (mmproj)`
                          : target.label
                      }
                      size={target.entry?.size ?? formatBytes(meta.fileSize)}
                      contextLength={meta.contextLength || undefined}
                      category={target.entry?.vision ? 'Vision' : meta.architecture}
                      progress={
                        prefetch.status === 'done'
                          ? 1
                          : { loaded: prefetch.loaded, total: prefetch.total }
                      }
                      ready={prefetch.status === 'done'}
                    />
                  </div>
                )}

                <p className="text-xs text-muted-foreground">
                  Optional: downloads the model into wllama&apos;s browser cache so chat
                  starts instantly. Never automatic.
                </p>
              </div>
            </div>
          </div>
        )}
      </section>
    </div>
  );
}
```
