diff --git a/.changelog/next/added-agent-a2e7c7e9.md b/.changelog/next/added-agent-a2e7c7e9.md new file mode 100644 index 0000000000..97fac62702 --- /dev/null +++ b/.changelog/next/added-agent-a2e7c7e9.md @@ -0,0 +1 @@ +- Manage MiniMax H3 text encoder downloads in Media Models diff --git a/client/src/pages/MediaModels.jsx b/client/src/pages/MediaModels.jsx index 4e85381ffe..1e51bbdeeb 100644 --- a/client/src/pages/MediaModels.jsx +++ b/client/src/pages/MediaModels.jsx @@ -1,29 +1,33 @@ /** * Media Models — manage the model catalog + clean up cached weights. * - * Two concerns share this page: + * Three concerns share this page: * 1. Model catalog (registry): the image/video base models that can be picked * in the gen forms. Built-in entries are read-only; user-added entries * (installed from HuggingFace) are editable/removable. Adding a model here * appends a `data/media-models.json` entry and hot-reloads the registry — * no server restart (issue #2124). - * 2. Cached weights: HF models live at HF's standard location - * (`~/.cache/huggingface/hub` unless HF_HOME is set). PortOS doesn't move or - * symlink them — it reads sizes for display and offers Delete to free disk. + * 2. MiniMax H3 text encoders: selectable prompt conditioners are separately + * downloadable, so they need the same install/delete lifecycle as models. + * 3. Cached weights: HF models live at Hugging Face's configured cache + * location. PortOS doesn't move or symlink them — it reads sizes for + * display and offers Delete to free disk. * LoRAs sit in `data/loras/`. * * The two views are JOINED on the HF repo id: a catalog row whose weights are * on disk shows its size and a "Delete weights" action inline, so freeing disk * for a model no longer means scrolling to a second list to find the same model - * again. The cached-weights section below only lists what the catalog does NOT - * cover (text encoders, orphaned/removed repos). + * again. The cached-weights section below only lists orphaned/removed repos. */ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { AlertTriangle, Trash2, Image as ImageIcon, Film, Plus, Pencil, Lock, X, Check, HardDrive } from 'lucide-react'; import toast from '../components/ui/Toast'; import ConfirmButtonPair from '../components/ui/ConfirmButtonPair'; import useConfirmDelete from '../hooks/useConfirmDelete'; +import { useModelDownloadStatus, textEncoderDownloadId } from '../hooks/useModelDownloadStatus'; +import ModelDownloadBadge from '../components/media/ModelDownloadBadge'; +import { formatBytes } from '../utils/formatters.js'; import { listCachedModels, deleteCachedModel, @@ -37,12 +41,13 @@ import { const DESTRUCTIVE_BTN = 'px-3 py-1.5 text-xs bg-port-error/20 hover:bg-port-error/40 text-port-error rounded disabled:opacity-50 flex items-center gap-1'; /** - * One arm-then-confirm destructive action. All four deletes on this page — - * a catalog row's weights, a catalog row's registry entry, an orphaned cache - * dir, a LoRA — are the same shape: a trigger that arms `confirmKey`, swapped - * in place for the inline confirm pair. Keeping it in one component means the - * confirm UX can't drift between them. `confirm` is a useConfirmDelete() - * result, so only one action across the whole page is ever armed. + * One arm-then-confirm destructive action. Every delete on this page — + * a catalog row's weights, a text encoder, a catalog row's registry entry, an + * orphaned cache dir, or a LoRA — are the same shape: a trigger that arms + * `confirmKey`, swapped in place for the inline confirm pair. Keeping it in + * one component means the confirm UX can't drift between them. `confirm` is a + * useConfirmDelete() result, so only one action across the whole page is ever + * armed. */ function DeleteAction({ confirm, @@ -53,6 +58,7 @@ function DeleteAction({ confirmText = 'Delete', busyText = 'Deleting…', busy = false, + disabled = false, title, className = DESTRUCTIVE_BTN, icon: Icon = Trash2, @@ -77,7 +83,7 @@ function DeleteAction({