116 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|