"use client"; import { useEffect, useRef, useState } from "react"; import type { DiscoverEntry, DiscoverHardware, PullProgress } from "@/lib/modelfit"; import { formatBytes, formatEta, pullModel, streamPullProgress } from "@/lib/modelfit"; import { VERDICT_META } from "@/components/modelfit/verdict"; // Explicit "agree to pull" gate. The user sees exactly what will be downloaded // (size, destination, command) and confirms with a size-labelled button before // anything hits the network — no silent auto-pull. // // The download itself runs as a server-side job: this modal subscribes to its // progress, so closing the modal does not cancel the pull and reopening it // re-attaches to the running download. type PullState = "idle" | "pulling" | "done" | "error"; const PHASE_LABEL: Record = { queued: "Starting…", manifest: "Fetching manifest…", downloading: "Downloading", verifying: "Verifying…", success: "Installed", error: "Failed", }; function ProgressBar({ progress }: { progress: PullProgress }) { const determinate = progress.total_bytes > 0; return (
{PHASE_LABEL[progress.phase]} {determinate && ( {progress.percent.toFixed(1)}% )}
{determinate && ( {formatBytes(progress.completed_bytes)} / {formatBytes(progress.total_bytes)} )} {progress.speed_bps > 0 && {formatBytes(progress.speed_bps)}/s} {progress.eta_s != null && ETA {formatEta(progress.eta_s)}} {progress.layers_total > 0 && ( layer {Math.min(progress.layers_done + 1, progress.layers_total)}/ {progress.layers_total} )}

You can close this — the download keeps running on the server.

); } function Line({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
); } export function PullConfirmModal({ entry, hardware, onClose, onPulled, }: { entry: DiscoverEntry; hardware: DiscoverHardware; onClose: () => void; onPulled: (modelId: string) => void; }) { const [state, setState] = useState("idle"); const [error, setError] = useState(null); const [progress, setProgress] = useState(null); const abortRef = useRef(null); // Detach the stream on unmount; the server-side job is unaffected. useEffect(() => () => abortRef.current?.abort(), []); const re = entry.resource_estimate; const meta = entry.model_meta; const isOllama = entry.source === "ollama"; const sizeGb = re?.estimated_disk_gb ?? null; const displayName = (meta.display_name || entry.model_id).replace(/^(ollama:|hf:|local:)/, ""); const verdict = re?.verdict ? VERDICT_META[re.verdict] : null; async function confirmPull() { setState("pulling"); setError(null); setProgress(null); try { const started = await pullModel(entry.model_id); // HF downloads are synchronous and return no job to follow. if (!started.job_id) { setState("done"); onPulled(entry.model_id); return; } const controller = new AbortController(); abortRef.current = controller; const final = await streamPullProgress(started.job_id, setProgress, controller.signal); if (final.phase === "error") { setState("error"); setError(final.error || "The download failed."); return; } setState("done"); onPulled(entry.model_id); } catch (e) { if ((e as Error)?.name === "AbortError") return; setState("error"); setError(String(e).replace(/^Error:\s*/, "")); } } return (
e.stopPropagation()} > {/* Header */}

Download this model?

You are about to fetch a model onto this machine.

{/* Verdict banner */} {verdict && (
{verdict.label} {re && ( ~{re.estimated_vram_gb} GB {hardware.total_vram_gb > 0 ? "VRAM" : "RAM"} )}
)} {/* Details */}
{meta.parameter_count_b != null && } {sizeGb != null && ( ≈ {sizeGb} GB} /> )}
{/* Command preview */} {entry.pull_command && (

Command

{entry.pull_command}
)} {/* Estimator warnings */} {re && re.warnings.length > 0 && state === "idle" && (
    {re.warnings .filter((w) => !w.startsWith("Memory figures are estimates")) .slice(0, 2) .map((w, i) => (
  • ⚠ {w}
  • ))}
)} {/* State feedback */} {state === "pulling" && (progress ? ( ) : (
Starting download…
))} {state === "done" && (
Installed — {displayName} is ready to use.
)} {state === "error" && (

{error}

)}
{/* Actions */}
{state === "done" ? ( ) : ( <> {isOllama ? ( ) : ( )} )}
); }