# 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>
  );
}
```
