import { useCallback, useEffect, useState } from "react"; import { deleteModelFromCache, getModelStorageInfo, type ModelStorageInfo, } from "../lib/modelCache"; import { MODELS, getModel } from "../lib/models"; import { TRANSLATION_MODEL_IDS, getTranslationModelById } from "../lib/translation"; interface StorageManagerProps { /** Downloaded-badge set from App — changes re-measure an open panel. */ downloaded: ReadonlySet; /** True while transcribing/live — weights may be mid-read, so no deletes. */ disabled: boolean; /** Called after a delete so App refreshes the downloaded badges (D-005). */ onChanged: () => void; } // Whisper weights plus the Opus-MT translation models (D-045). const MODEL_IDS = [...MODELS.map((model) => model.id), ...TRANSLATION_MODEL_IDS]; function displayName(modelId: string): string { const translation = getTranslationModelById(modelId); if (translation) return `Translation, ${translation.label}`; return getModel(modelId)?.displayName ?? modelId; } function formatBytes(bytes: number): string { if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(2)} GB`; if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)} MB`; if (bytes >= 1e3) return `${(bytes / 1e3).toFixed(0)} kB`; return `${bytes} B`; } /** * Storage management panel (M6): lists models with cached weights and their * exact on-disk size, with per-model delete. Collapsed by default — measuring * reads every cached response body, so it only runs when the panel is opened * (and again when the downloaded set changes or a model is deleted). */ export function StorageManager({ downloaded, disabled, onChanged }: StorageManagerProps) { const [open, setOpen] = useState(false); const [info, setInfo] = useState(null); const [deleting, setDeleting] = useState(null); const measure = useCallback(() => { getModelStorageInfo(MODEL_IDS).then(setInfo, () => setInfo([])); }, []); useEffect(() => { if (open) measure(); }, [open, downloaded, measure]); async function handleDelete(modelId: string) { setDeleting(modelId); try { await deleteModelFromCache(modelId); measure(); // Refresh the picker's downloaded/offline badges too. onChanged(); } finally { setDeleting(null); } } const totalBytes = (info ?? []).reduce((sum, entry) => sum + entry.bytes, 0); return (
setOpen(event.currentTarget.open)} > Storage {open && info !== null && info.length > 0 && ( — {formatBytes(totalBytes)} used )}

Model weights cached in this browser. Deleting frees the space; the model simply re-downloads the next time you use it.

{info === null ? (

Measuring…

) : info.length === 0 ? (

No models downloaded yet.

) : (
    {info.map((entry) => (
  • {displayName(entry.modelId)} {entry.modelId}
    {formatBytes(entry.bytes)} · {entry.fileCount}{" "} {entry.fileCount === 1 ? "file" : "files"}
  • ))}
)}
); }