import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { api, assetFileURL, connectWS, deleteAsset, listAssets, listJobs, listOperators, runJob, uploadFile, } from "./api"; import Viewer from "./Viewer"; 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: "๐ง", frames: "๐", other: "๐" }; function ParamForm({ schema, values, onChange }) { const props = schema?.properties || {}; if (!Object.keys(props).length) return
No parameters.
; return ({text};
}
export default function App() {
const [assets, setAssets] = useState([]);
const [operators, setOperators] = useState([]);
const [jobs, setJobs] = useState([]);
const [selected, setSelected] = useState(null);
const [opId, setOpId] = useState(null);
const [params, setParams] = useState({});
const [openJob, setOpenJob] = useState(null);
const fileInput = useRef(null);
const refreshAssets = useCallback(() => listAssets().then(setAssets), []);
const refreshJobs = useCallback(() => listJobs().then(setJobs), []);
useEffect(() => {
refreshAssets();
refreshJobs();
listOperators().then(setOperators);
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();
});
}, []);
// drag-drop + paste ingest
useEffect(() => {
const drop = async (e) => {
e.preventDefault();
for (const f of e.dataTransfer?.files || []) await uploadFile(f);
refreshAssets();
};
const paste = async (e) => {
const items = [...(e.clipboardData?.items || [])];
for (const it of 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 usableOps = useMemo(() =>
operators.filter((op) => !selectedAsset || op.accepts.includes(selectedAsset.kind)),
[operators, selectedAsset]);
const activeOp = operators.find((o) => o.id === opId);
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 || !selectedAsset) return;
await runJob(activeOp.id, selectedAsset.id, params);
refreshJobs();
};
return (