import React, { useEffect, useMemo, useState } from "react";
import { ApiClient } from "./api-client.ts";
import {
addNodeToDraft,
createDefaultWorkflowDraft,
removeNodeFromDraft,
resolveDefinitionIdForNode,
serializeWorkflowDraft,
workflowDraftFromVersion,
type WorkflowDraft,
} from "./workflow-editor-state.ts";
type NavItem = "Assets" | "Workflows" | "Runs" | "Explore" | "Labels" | "Admin";
type BootstrapContext = {
userId: string;
workspace: { _id: string; name: string };
project: { _id: string; name: string };
};
type AppProps = {
apiBaseUrl: string;
};
function usePathname() {
const [pathname, setPathname] = useState(
typeof window === "undefined" ? "/assets" : window.location.pathname || "/assets",
);
useEffect(() => {
const handle = () => setPathname(window.location.pathname || "/assets");
window.addEventListener("popstate", handle);
return () => window.removeEventListener("popstate", handle);
}, []);
return pathname === "/" ? "/assets" : pathname;
}
function AppShell(props: {
workspaceName: string;
projectName: string;
active: NavItem;
children: React.ReactNode;
}) {
const navItems: Array<{ label: NavItem; href: string }> = [
{ label: "Assets", href: "/assets" },
{ label: "Workflows", href: "/workflows" },
{ label: "Runs", href: "/runs" },
{ label: "Explore", href: "/explore" },
{ label: "Labels", href: "/labels" },
{ label: "Admin", href: "/admin" },
];
return (
);
}
function AssetsPage(props: {
api: ApiClient;
bootstrap: BootstrapContext;
}) {
const [sourcePath, setSourcePath] = useState("");
const [assets, setAssets] = useState([]);
const [error, setError] = useState(null);
const loadAssets = async () => {
try {
setAssets(await props.api.listAssets(props.bootstrap.project._id));
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load assets");
}
};
useEffect(() => {
void loadAssets();
}, [props.bootstrap.project._id]);
return (
Assets
Register local folders, archives, or dataset files, then probe them into managed asset metadata.
{error ? {error}
: null}
{assets.length === 0 ? (
No assets have been registered yet.
) : (
assets.map((asset) => (
Type: {asset.type}
Source: {asset.sourceType}
Detected: {(asset.detectedFormats ?? []).join(", ") || "pending"}
Top-level entries: {(asset.topLevelPaths ?? []).slice(0, 6).join(", ") || "n/a"}
))
)}
);
}
function AssetDetailPage(props: {
api: ApiClient;
assetId: string;
}) {
const [asset, setAsset] = useState(null);
const [probeReport, setProbeReport] = useState(null);
const [artifactId, setArtifactId] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
void (async () => {
try {
const [assetData, reportData] = await Promise.all([
props.api.getAsset(props.assetId),
props.api.getProbeReport(props.assetId).catch(() => null),
]);
setAsset(assetData);
setProbeReport(reportData);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load asset detail");
}
})();
}, [props.assetId]);
if (error) {
return ;
}
if (!asset) {
return ;
}
return (
{asset.displayName}
Asset ID: {asset._id}
Type: {asset.type}
Status: {asset.status}
Source path: {asset.sourcePath ?? "n/a"}
{artifactId ?
Open Explore View : null}
);
}
function WorkflowsPage(props: {
api: ApiClient;
bootstrap: BootstrapContext;
}) {
const [workflows, setWorkflows] = useState([]);
const [error, setError] = useState(null);
const load = async () => {
try {
setWorkflows(await props.api.listWorkflows(props.bootstrap.project._id));
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load workflows");
}
};
useEffect(() => {
void load();
}, [props.bootstrap.project._id]);
return (
Workflows
{error ? {error}
: null}
{workflows.length === 0 ? (
No workflows yet.
) : (
workflows.map((workflow) => (
{workflow.name}
Status: {workflow.status}
Latest version: {workflow.latestVersionNumber}
))
)}
);
}
function WorkflowEditorPage(props: {
api: ApiClient;
workflowId: string;
}) {
const [workflow, setWorkflow] = useState(null);
const [nodes, setNodes] = useState([]);
const [assets, setAssets] = useState([]);
const [selectedAssetId, setSelectedAssetId] = useState(null);
const [versions, setVersions] = useState([]);
const [draft, setDraft] = useState(() => createDefaultWorkflowDraft());
const [selectedNodeId, setSelectedNodeId] = useState("rename-folder");
const [lastRunId, setLastRunId] = useState(null);
const [dirty, setDirty] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
void (async () => {
try {
const workflowDefinition = await props.api.getWorkflowDefinition(props.workflowId);
const [nodeDefs, workflowVersions, workflowAssets] = await Promise.all([
props.api.listNodeDefinitions(),
props.api.listWorkflowVersions(props.workflowId),
props.api.listAssets(workflowDefinition.projectId),
]);
setWorkflow(workflowDefinition);
setNodes(nodeDefs);
setAssets(workflowAssets);
setSelectedAssetId((previous) => {
if (previous && workflowAssets.some((asset) => asset._id === previous)) {
return previous;
}
return workflowAssets[0]?._id ?? null;
});
setVersions(workflowVersions);
const nextDraft = workflowDraftFromVersion(workflowVersions[0] ?? null);
setDraft(nextDraft);
setSelectedNodeId(nextDraft.logicGraph.nodes[0]?.id ?? "rename-folder");
setDirty(false);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load workflow");
}
})();
}, [props.workflowId]);
const selectedNode = useMemo(
() =>
nodes.find((node) => node.id === resolveDefinitionIdForNode(draft, selectedNodeId)) ?? null,
[draft, nodes, selectedNodeId],
);
async function saveCurrentDraft() {
const version = await props.api.saveWorkflowVersion(
props.workflowId,
serializeWorkflowDraft(draft),
);
setVersions((previous) => [version, ...previous.filter((item) => item._id !== version._id)]);
setDirty(false);
return version;
}
return (
{workflow?.name ?? "Workflow Editor"}
{lastRunId ?
Open Latest Run : null}
{error ? {error}
: null}
Canvas
{draft.logicGraph.nodes.map((node) => (
setSelectedNodeId(node.id)}
style={{ cursor: "pointer" }}
>
{node.id}
Type: {node.type}
Definition: {resolveDefinitionIdForNode(draft, node.id)}
))}
Latest saved versions: {versions.length > 0 ? versions.map((item) => item.versionNumber).join(", ") : "none"}
Draft status: {dirty ? "unsaved changes" : "synced"}
);
}
function RunsIndexPage() {
return (
Runs
Open a specific run from the workflow editor.
);
}
function RunDetailPage(props: {
api: ApiClient;
runId: string;
}) {
const [run, setRun] = useState(null);
const [tasks, setTasks] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
void (async () => {
try {
const [runData, taskData] = await Promise.all([
props.api.getRun(props.runId),
props.api.listRunTasks(props.runId),
]);
setRun(runData);
setTasks(taskData);
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load run detail");
}
})();
}, [props.runId]);
if (error) {
return ;
}
if (!run) {
return ;
}
return (
Run Detail
Run ID: {run._id}
Status: {run.status}
Input assets:{" "}
{(run.assetIds ?? []).length > 0
? run.assetIds.map((assetId: string) => assetId).join(", ")
: "none"}
Run Graph
{tasks.map((task) => (
{task.nodeId}
{task.status}
Node type: {task.nodeType}
Bound assets: {(task.assetIds ?? []).join(", ") || "none"}
))}
);
}
function ExplorePage(props: {
api: ApiClient;
artifactId: string | null;
}) {
const [artifact, setArtifact] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
if (!props.artifactId) {
return;
}
void (async () => {
try {
setArtifact(await props.api.getArtifact(props.artifactId!));
} catch (loadError) {
setError(loadError instanceof Error ? loadError.message : "Failed to load artifact");
}
})();
}, [props.artifactId]);
if (!props.artifactId) {
return (
Explore
Create an artifact from asset detail to inspect it here.
);
}
if (error) {
return ;
}
if (!artifact) {
return ;
}
return (
{artifact.title}
Artifact ID: {artifact._id}
Type: {artifact.type}
{JSON.stringify(artifact.payload, null, 2)}
);
}
export function App(props: AppProps) {
const api = useMemo(() => new ApiClient(props.apiBaseUrl), [props.apiBaseUrl]);
const pathname = usePathname();
const [bootstrap, setBootstrap] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
void (async () => {
try {
setBootstrap(await api.bootstrapDev());
} catch (bootstrapError) {
setError(
bootstrapError instanceof Error ? bootstrapError.message : "Failed to bootstrap local context",
);
}
})();
}, [api]);
if (error) {
return ;
}
if (!bootstrap) {
return Bootstrapping local workspace...;
}
const assetMatch = pathname.match(/^\/assets\/([^/]+)$/);
const workflowMatch = pathname.match(/^\/workflows\/([^/]+)$/);
const runMatch = pathname.match(/^\/runs\/([^/]+)$/);
const exploreMatch = pathname.match(/^\/explore\/([^/]+)$/);
let active: NavItem = "Assets";
let content: React.ReactNode = ;
if (pathname === "/workflows") {
active = "Workflows";
content = ;
} else if (workflowMatch) {
active = "Workflows";
content = ;
} else if (pathname === "/runs") {
active = "Runs";
content = ;
} else if (runMatch) {
active = "Runs";
content = ;
} else if (exploreMatch) {
active = "Explore";
content = ;
} else if (pathname === "/explore") {
active = "Explore";
content = ;
} else if (assetMatch) {
active = "Assets";
content = ;
}
return (
{content}
);
}