Files
whisper_web/src/components/StorageManager.tsx
T

116 lines
4.0 KiB
TypeScript

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<string>;
/** 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<ModelStorageInfo[] | null>(null);
const [deleting, setDeleting] = useState<string | null>(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 (
<details
className="storage-manager"
onToggle={(event) => setOpen(event.currentTarget.open)}
>
<summary>
Storage
{open && info !== null && info.length > 0 && (
<span className="storage-total"> — {formatBytes(totalBytes)} used</span>
)}
</summary>
<p className="storage-hint">
Model weights cached in this browser. Deleting frees the space; the
model simply re-downloads the next time you use it.
</p>
{info === null ? (
<p className="storage-hint">Measuring…</p>
) : info.length === 0 ? (
<p className="storage-hint">No models downloaded yet.</p>
) : (
<ul className="storage-list">
{info.map((entry) => (
<li key={entry.modelId} className="storage-row">
<div className="storage-model">
<span className="storage-name">
{displayName(entry.modelId)}
</span>
<code className="storage-id">{entry.modelId}</code>
</div>
<span className="storage-size">
{formatBytes(entry.bytes)} · {entry.fileCount}{" "}
{entry.fileCount === 1 ? "file" : "files"}
</span>
<button
type="button"
className="storage-delete"
onClick={() => void handleDelete(entry.modelId)}
disabled={disabled || deleting !== null}
>
{deleting === entry.modelId ? "Deleting…" : "Delete"}
</button>
</li>
))}
</ul>
)}
</details>
);
}