"use client"; import { useEffect, useMemo, useState } from "react"; import { api, ApiError } from "@/lib/api"; import { useLive } from "@/lib/events"; import { fmtAgo, fmtBytes, fmtGB, fmtParams } from "@/lib/format"; import type { Candidate, Job } from "@/lib/types"; import { CompatPill, Modal, PageHeader, Pill, Progress, SizePill, Spinner, useToast } from "@/components/ui"; const TASKS = ["", "general", "coding", "reasoning", "vision", "embedding", "reranker"]; const SIZES = ["", "TINY", "SMALL", "MEDIUM", "LARGE", "XL"]; export default function HarvesterPage() { const live = useLive(); const toast = useToast(); const [rows, setRows] = useState([]); const [starter, setStarter] = useState<{ slot: string; candidate: Candidate | null }[]>([]); const [lastScan, setLastScan] = useState(null); const [task, setTask] = useState(""); const [rt, setRt] = useState(""); const [size, setSize] = useState(""); const [q, setQ] = useState(""); const [dups, setDups] = useState(false); const [opts, setOpts] = useState(false); const [scanOpts, setScanOpts] = useState({ runtimes: ["mlx", "gguf"], min_downloads: 500, limit_per_author: 150, max_ram_gb: "", search: "" }); const [busy, setBusy] = useState(false); const scanJob = Object.values(live.jobs).find((j) => j.kind === "harvest" && (j.status === "running" || j.status === "queued")); const load = () => { const p = new URLSearchParams(); if (task) p.set("task", task); if (rt) p.set("runtime", rt); if (size) p.set("size_class", size); if (q) p.set("q", q); if (dups) p.set("include_duplicates", "true"); api.get<{ candidates: Candidate[]; last_scan: number | null; starter: { slot: string; candidate: Candidate | null }[] }>(`/api/harvest/candidates?${p}`) .then((r) => { setRows(r.candidates); setLastScan(r.last_scan); setStarter(r.starter); }).catch(() => {}); }; useEffect(() => { load(); }, [task, rt, size, q, dups, live.version]); // eslint-disable-line react-hooks/exhaustive-deps const scan = async () => { setBusy(true); try { await api.post("/api/harvest/scan", { ...scanOpts, max_ram_gb: scanOpts.max_ram_gb ? Number(scanOpts.max_ram_gb) : null, search: scanOpts.search || null }); toast.push("Harvest started — this takes a minute or two", "good"); setOpts(false); } catch (e) { toast.push(e instanceof ApiError ? e.message : "Failed", "bad"); } finally { setBusy(false); } }; const toggle = async (c: Candidate) => { await api.post("/api/harvest/select", { repo_id: c.repo_id, selected: !c.selected }); load(); }; const dismiss = async (c: Candidate) => { await api.post("/api/harvest/dismiss", { repo_id: c.repo_id }); load(); }; const queue = async () => { setBusy(true); try { const r = await api.post<{ queued: { repo: string; job?: string; error?: string }[] }>("/api/harvest/queue"); const ok = r.queued.filter((x) => x.job).length; const bad = r.queued.filter((x) => x.error); toast.push(`${ok} download${ok === 1 ? "" : "s"} queued${bad.length ? ` · ${bad.length} failed: ${bad[0].error}` : ""}`, bad.length ? "bad" : "good"); load(); } finally { setBusy(false); } }; const selected = rows.filter((r) => r.selected); const selectedBytes = selected.reduce((a, r) => a + (r.download_bytes || 0), 0); const families = useMemo(() => [...new Set(rows.map((r) => r.family))].sort(), [rows]); return (
{toast.view} Explores Hugging Face (mlx-community, unsloth, bartowski, ggml-org…), keeps only models that fit this Mac, removes duplicate quantizations and proposes a download queue. {lastScan ? <>Last scan {fmtAgo(lastScan)}. : "No scan yet."}} actions={<> } /> {scanJob &&
{String((scanJob.detail as { stage?: string }).stage || "starting")}
} {starter.some((s) => s.candidate) && (
Suggested starter library
best candidate per slot — tick the ones you want
{starter.map((s) => (
{s.slot}
{s.candidate ? ( ) : "no candidate"}
))}
)}
setQ(e.target.value)} />
{selected.length > 0 && {selected.length} selected · {fmtBytes(selectedBytes)}}
{rows.map((c) => ( ))} {!rows.length && }
ModelTaskRuntimeQuantParamsDownloadEst. RAMCompatDownloadsScore
toggle(c)} /> {c.name}
{c.repo_id}{c.duplicate_of ? ` · duplicate of ${c.duplicate_of.split("/")[1]}` : ""}{c.installed ? " · installed" : ""}
{c.task} {c.runtime === "mlx" ? "MLX" : "GGUF"} {c.quantization || "—"} {fmtParams(c.parameter_count)} {fmtBytes(c.download_bytes)} {fmtGB(c.estimated_ram_gb)} {c.downloads.toLocaleString()} {c.score.toFixed(0)}
{lastScan ? "No candidates match." : "Run a scan to discover compatible models."}
{families.length > 0 &&
Families: {families.join(", ")}
} setOpts(false)} title="Harvest options">
{["mlx", "gguf"].map((r) => )}
); } export type { Job };