import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
api, assetFileURL, cancelJob, connectWS, deleteAsset, deleteJob, getMe, getSettings,
listAssets, listJobs, listOperators, logout, retryJob, runJob, uploadFile,
} from "./api";
import Viewer from "./Viewer";
import SplatViewer from "./SplatViewer";
import Settings from "./Settings";
import Login from "./Login";
import Dashboard from "./Dashboard";
const MODEL_EXTS = ["glb", "gltf", "obj", "fbx", "ply", "stl"];
const fmtSize = (n) =>
n > 1e9 ? (n / 1e9).toFixed(1) + " GB" : n > 1e6 ? (n / 1e6).toFixed(1) + " MB"
: n > 1e3 ? (n / 1e3).toFixed(0) + " KB" : n + " B";
const KIND_ICON = { video: "๐ฌ", image: "๐ผ", model: "๐ง", splat: "โจ", frames: "๐", colmap_dataset: "๐", folder: "๐", other: "๐" };
const isViewable = (a) => a && (a.kind === "splat" || MODEL_EXTS.includes(a.name.split(".").pop().toLowerCase()));
function ParamForm({ schema, values, onChange }) {
const props = schema?.properties || {};
if (!Object.keys(props).length) return
No parameters.
;
return (
{Object.entries(props).map(([key, def]) => {
const val = values[key] ?? def.default ?? "";
const set = (v) => onChange({ ...values, [key]: v });
return (
);
})}
);
}
const maybeNum = (def, v) => (def.type === "integer" ? parseInt(v) : def.type === "number" ? parseFloat(v) : v);
function AssetPreview({ asset }) {
const [folder, setFolder] = useState(null);
const ext = asset ? asset.name.split(".").pop().toLowerCase() : "";
useEffect(() => {
setFolder(null);
if (asset && (asset.kind === "frames" || asset.kind === "folder" || asset.kind === "colmap_dataset")) {
api(assetFileURL(asset.id)).then(setFolder).catch(() => {});
}
}, [asset?.id]);
if (!asset) return Select an asset โ or drop / paste files anywhere
;
if (asset.kind === "splat") return ;
if (MODEL_EXTS.includes(ext)) return ;
if (asset.kind === "image") return ;
if (asset.kind === "video") return ;
if (folder?.files) {
const imgs = folder.files.filter((f) => /\.(jpg|jpeg|png)$/i.test(f));
return (
{imgs.slice(0, 60).map((f) =>

)}
{imgs.length > 60 &&
+{imgs.length - 60} more}
{!imgs.length &&
{folder.files.length} files (no image preview)}
);
}
if (ext === "json") return ;
return No preview for .{ext} โ download to use it
;
}
function JsonPreview({ url }) {
const [text, setText] = useState("โฆ");
useEffect(() => { fetch(url).then((r) => r.text()).then((t) => setText(t.slice(0, 20000))); }, [url]);
return {text};
}
function CompareGrid({ assets }) {
return (
{assets.map((a) => (
{a.name} ยท {fmtSize(a.size)}{a.meta?.tool ? ` ยท ${a.meta.tool}` : ""}
{a.kind === "splat"
?
:
}
))}
);
}
export default function App() {
const [user, setUser] = useState(undefined); // undefined=loading, null=logged out
const [view, setView] = useState("workspace"); // workspace | dashboard
const [assets, setAssets] = useState([]);
const [operators, setOperators] = useState([]);
const [jobs, setJobs] = useState([]);
const [settings, setSettings] = useState({ _env_set: [] });
const [selected, setSelected] = useState(null);
const [opId, setOpId] = useState(null);
const [params, setParams] = useState({});
const [openJob, setOpenJob] = useState(null);
const [showSettings, setShowSettings] = useState(false);
const [compareMode, setCompareMode] = useState(false);
const [compareSel, setCompareSel] = useState([]);
const fileInput = useRef(null);
const wsRef = useRef(null);
const refreshAssets = useCallback(() => listAssets().then(setAssets).catch(() => {}), []);
const refreshJobs = useCallback(() => listJobs().then(setJobs).catch(() => {}), []);
const refreshSettings = useCallback(() => getSettings().then(setSettings).catch(() => {}), []);
// auth bootstrap
useEffect(() => {
getMe().then(setUser).catch(() => setUser(null));
const onUnauth = () => setUser(null);
window.addEventListener("mb-unauth", onUnauth);
return () => window.removeEventListener("mb-unauth", onUnauth);
}, []);
// load workspace data + connect the live socket once, after login
useEffect(() => {
if (!user) return;
refreshAssets(); refreshJobs();
if (user.is_owner) refreshSettings();
listOperators().then(setOperators).catch(() => {});
if (!wsRef.current) {
wsRef.current = connectWS((msg) => {
if (msg.type === "job") {
setJobs((js) => {
const i = js.findIndex((j) => j.id === msg.job.id);
if (i === -1) return [msg.job, ...js];
const copy = [...js]; copy[i] = msg.job; return copy;
});
}
if (msg.type === "assets_changed") refreshAssets();
});
}
}, [user?.username]);
const doLogout = async () => { try { await logout(); } catch { /* ignore */ } window.location.reload(); };
useEffect(() => {
const drop = async (e) => { e.preventDefault(); for (const f of e.dataTransfer?.files || []) await uploadFile(f); refreshAssets(); };
const paste = async (e) => { for (const it of e.clipboardData?.items || []) { const f = it.getAsFile?.(); if (f) await uploadFile(f); } refreshAssets(); };
const prevent = (e) => e.preventDefault();
window.addEventListener("drop", drop); window.addEventListener("dragover", prevent); window.addEventListener("paste", paste);
return () => { window.removeEventListener("drop", drop); window.removeEventListener("dragover", prevent); window.removeEventListener("paste", paste); };
}, [refreshAssets]);
const selectedAsset = assets.find((a) => a.id === selected) || null;
const envSet = settings._env_set || [];
// no-input operators (accepts: []) are always available; others filter by asset kind
const usableOps = useMemo(() =>
operators.filter((op) => !op.accepts.length || !selectedAsset || op.accepts.includes(selectedAsset.kind)),
[operators, selectedAsset]);
const opGroups = useMemo(() => {
const groups = {};
for (const op of usableOps) (groups[op.category || "other"] ||= []).push(op);
return groups;
}, [usableOps]);
const activeOp = operators.find((o) => o.id === opId);
const needsInput = (activeOp?.accepts?.length ?? 0) > 0;
const gatedEnv = (activeOp?.requires_env || []).filter((e) => !envSet.includes(e));
const gated = gatedEnv.length > 0;
useEffect(() => {
if (activeOp) {
const defaults = {};
for (const [k, d] of Object.entries(activeOp.params_schema?.properties || {}))
if (d.default !== undefined) defaults[k] = d.default;
setParams(defaults);
}
}, [opId]);
useEffect(() => {
if (usableOps.length && !usableOps.find((o) => o.id === opId)) setOpId(usableOps[0].id);
}, [usableOps]);
const launch = async () => {
if (!activeOp || gated || (needsInput && !selectedAsset)) return;
await runJob(activeOp.id, needsInput ? selectedAsset.id : null, params);
refreshJobs();
};
const clickAsset = (a) => {
if (compareMode && isViewable(a)) {
setCompareSel((s) => s.includes(a.id) ? s.filter((x) => x !== a.id) : [...s, a.id]);
} else {
setSelected(a.id);
}
};
const compareAssets = compareSel.map((id) => assets.find((a) => a.id === id)).filter(Boolean);
if (user === undefined) return ;
if (user === null) return ;
return (
{view === "dashboard"
?
: compareMode && compareAssets.length
?
: }
{showSettings && user.is_owner &&
setShowSettings(false)} onSaved={setSettings} />}
);
}