diff --git a/apps/fabro-web/app/components/state.tsx b/apps/fabro-web/app/components/state.tsx index 1cd229be9..0e947b759 100644 --- a/apps/fabro-web/app/components/state.tsx +++ b/apps/fabro-web/app/components/state.tsx @@ -82,7 +82,7 @@ export function LoadingState({ label }: { label?: string }) { ); } -function Spinner({ className = "" }: { className?: string }) { +export function Spinner({ className = "" }: { className?: string }) { return ( (""); const [searchInput, setSearchInput] = useState(""); const debouncedSearch = useDebouncedValue(searchInput, 250); const [sortKey, setSortKey] = useState("provider"); const [direction, setDirection] = useState("asc"); + const [results, setResults] = useState>(new Map()); + const [sweep, setSweep] = useState(null); const { data, isLoading } = useModels(providerFilter, debouncedSearch); @@ -248,6 +271,75 @@ function ModelsSection({ providers }: { providers: Provider[] }) { [sortKey], ); + const running = sweep !== null && sweep.done < sweep.total; + + const runSweep = useCallback(async () => { + if (running) return; + const ids = rows.map((r) => r.id); + if (ids.length === 0) return; + + const seed = new Map(); + for (const id of ids) seed.set(id, { phase: "queued" }); + setResults(seed); + setSweep({ done: 0, total: ids.length, ok: 0, failed: 0 }); + + let cursor = 0; + const worker = async () => { + while (cursor < ids.length) { + const i = cursor; + cursor += 1; + const id = ids[i]; + setResults((prev) => { + const next = new Map(prev); + next.set(id, { phase: "running" }); + return next; + }); + + let outcome: RowState; + try { + const result = await apiData(() => modelsApi.testModel(id)); + if (result.status === "ok") { + outcome = { phase: "ok" }; + } else if (result.status === "error") { + outcome = { + phase: "error", + message: result.error_message ?? "Failed", + }; + } else { + outcome = { phase: "error", message: "provider not configured" }; + } + } catch (err) { + outcome = { + phase: "error", + message: err instanceof Error ? err.message : String(err), + }; + } + + setResults((prev) => { + const next = new Map(prev); + next.set(id, outcome); + return next; + }); + setSweep((prev) => + prev + ? { + done: prev.done + 1, + total: prev.total, + ok: prev.ok + (outcome.phase === "ok" ? 1 : 0), + failed: prev.failed + (outcome.phase === "error" ? 1 : 0), + } + : prev, + ); + } + }; + + await Promise.all( + Array.from({ length: Math.min(TEST_CONCURRENCY, ids.length) }, () => + worker(), + ), + ); + }, [rows, running]); + const showEmpty = !isLoading && rows.length === 0; return ( @@ -271,6 +363,28 @@ function ModelsSection({ providers }: { providers: Provider[] }) { onChange={(e) => setSearchInput(e.target.value)} className="w-44 rounded-md border border-line bg-panel/80 px-3 py-2 text-xs text-fg-2 placeholder:text-fg-muted focus:border-line-strong focus:outline-none" /> + {sweep && !running ? ( + + {sweep.ok} ok · {sweep.failed} failed + + ) : null} +
@@ -308,6 +422,12 @@ function ModelsSection({ providers }: { providers: Provider[] }) { align="right" onClick={onSort} /> + + Test + @@ -318,6 +438,7 @@ function ModelsSection({ providers }: { providers: Provider[] }) { providerLabel={ providerNameById.get(model.provider) ?? model.provider } + state={results.get(model.id)} /> ))} @@ -338,9 +459,11 @@ function ModelsSection({ providers }: { providers: Provider[] }) { function ModelTableRow({ model, providerLabel, + state, }: { model: Model; providerLabel: string; + state: RowState | undefined; }) { return ( @@ -356,10 +479,52 @@ function ModelTableRow({ {formatTokensPerSecond(model.estimated_output_tps)} + + + ); } +function TestStatusCell({ state }: { state: RowState | undefined }) { + if (!state) { + return ; + } + if (state.phase === "queued") { + return ( + + + Queued + + ); + } + if (state.phase === "running") { + return ( + + + Testing… + + ); + } + if (state.phase === "ok") { + return ( + + + Ok + + ); + } + return ( + + + {state.message} + + ); +} + function ModelNameCell({ model }: { model: Model }) { const hasAliases = model.aliases.length > 0; const nameNode = hasAliases ? (