import { useEffect, useState } from "react"; import { api } from "./api"; import CompareView from "./components/CompareView"; import ModelCard from "./components/ModelCard"; import Sentinel2View from "./components/Sentinel2View"; import type { CuratedPair, ModelSummary, PredictResult, Sentinel2AOI } from "./types"; type Tab = "curated" | "sentinel2" | "card"; // friendly labels for the served bundle ids (falls back to the raw id) const MODEL_LABELS: Record = { levircd_dinov2: "DINOv2 + LoRA", levircd_segformer: "SegFormer MiT-b2", }; const modelLabel = (id: string) => MODEL_LABELS[id] ?? id; // short scene tag from a source string like "LEVIR-CD test_10" -> "T10" const sceneTag = (source: string) => { const m = source.match(/(\d+)\s*$/); return m ? `T${m[1]}` : source.slice(0, 3).toUpperCase(); }; // strip the "(test N)" suffix from a pair title for a tighter label const sceneTitle = (title: string) => title.replace(/\s*\(test[^)]*\)\s*$/i, ""); // annotated ground-truth change % from the description, if present const annotatedPct = (desc: string): number | null => { const m = desc.match(/([\d.]+)\s*%/); return m ? parseFloat(m[1]) : null; }; // full-scene tile+stitch inference is seconds on CPU (baked once, then served from cache) — format // the real model cost sensibly instead of a raw millisecond count. const fmtDuration = (ms: number): { v: string; u: string } => ms >= 1000 ? { v: (ms / 1000).toFixed(ms >= 10000 ? 0 : 1), u: "s" } : { v: String(Math.round(ms)), u: "ms" }; const fmtDurStr = (ms: number): string => { const d = fmtDuration(ms); return `${d.v} ${d.u}`; }; export default function App() { const [tab, setTab] = useState("curated"); const [models, setModels] = useState([]); const [pairs, setPairs] = useState([]); const [s2, setS2] = useState([]); const [modelId, setModelId] = useState(""); const [pairId, setPairId] = useState(""); const [result, setResult] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [showOverlay, setShowOverlay] = useState(true); const [opacity, setOpacity] = useState(0.75); useEffect(() => { Promise.all([api.models(), api.curated()]) .then(([m, p]) => { setModels(m); setPairs(p); if (m.length) setModelId(m[0].id); if (p.length) setPairId(p[0].id); }) .catch((e) => setError(String(e))); // Sentinel-2 AOIs load independently; a missing S2 cache must not break the aerial tab. api .sentinel2() .then(setS2) .catch((e) => console.warn("sentinel2 list failed:", e)); }, []); useEffect(() => { if (!modelId || !pairId) return; let cancelled = false; setBusy(true); setError(null); setResult(null); // clear stale overlay/stats so the new imagery never shows the old mask api .predict(pairId, modelId) .then((r) => { if (!cancelled) setResult(r); }) .catch((e) => !cancelled && setError(String(e))) .finally(() => !cancelled && setBusy(false)); return () => { cancelled = true; }; }, [modelId, pairId]); const model = models.find((m) => m.id === modelId); const pair = pairs.find((p) => p.id === pairId); const placeholder = result?.is_placeholder ?? model?.is_placeholder ?? false; const healthy = models.length > 0 && !error; const activeThreshold = result?.threshold ?? model?.threshold; return (

Satellite Change Detection

{tab === "sentinel2" ? ( <> TRACK B · SENTINEL-2 10 M/PX · ONNX · CPU ) : ( <> TRACK A · AERIAL 0.5 M/PX · ONNX · CPU )}
{placeholder && tab === "curated" && (
Serving placeholder (random-init) weights — the pipeline is real, the predictions are not. Swap in the trained bundle to ship.
)} {error && (
{error}
)} {tab === "card" ? ( ) : tab === "sentinel2" ? ( ) : (
{pairId && modelId ? ( ) : (
Initializing…
)}
MODEL {modelId || "—"}
INPUT{" "} {model ? `${model.input_size}×${model.input_size}` : "—"}
GRID {model?.fixed_grid ? "FIXED" : "DYN H/W"}
THR{" "} {activeThreshold != null ? activeThreshold.toFixed(3) : "—"}
INFER{" "} {result ? fmtDurStr(result.elapsed_ms) : busy ? "…" : "—"}
BANDS {model ? model.band_order.join("") : "—"}
GSD 0.5 m/px
SRC {pair?.source || pair?.id || "—"}
)}
); }