import { useState, useEffect } from "react"; import { useApp } from "../state/store"; import { wizardApi, type BatchOperation } from "../lib/wizardApi"; import { Branch, Check, Close, ChevronRight } from "../components/icons"; type WizardStep = "Intake" | "Analyze" | "Generate" | "Validate" | "Draft saved" | "Publish"; const STEPS: WizardStep[] = ["Intake", "Analyze", "Generate", "Validate", "Draft saved", "Publish"]; interface DraftState { step: WizardStep; flowKey?: string; name: string; description: string; nodes: any[]; edges: any[]; fields: { name: string; type: string }[]; rules: string[]; } export default function Wizard() { const mode = useApp((s) => s.mode); const pushToast = useApp((s) => s.pushToast); const refreshLive = useApp((s) => s.refreshLive); const actor = useApp((s) => s.actor); const setScene = useApp((s) => s.setScene); const canPublish = mode === "live" && !!actor?.user_id; const [draft, setDraft] = useState(() => { try { const stored = localStorage.getItem("fm.mc.wizard.draft"); if (stored) return JSON.parse(stored); } catch {} return { step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }; }); const [working, setWorking] = useState(false); const [publishedKey, setPublishedKey] = useState(null); useEffect(() => { localStorage.setItem("fm.mc.wizard.draft", JSON.stringify(draft)); }, [draft]); const updateDraft = (patch: Partial) => { setDraft(d => ({ ...d, ...patch })); }; const handleIntakeSubmit = async () => { if (!draft.description) return; setWorking(true); try { const slug = `process_${Date.now()}`; const res = await wizardApi.createDraft({ name: slug, display_name: draft.name || "Untitled Process", description: draft.description, source_context: "EA2_DRAFT_PROCESS:process_creation", config: { wizard: { marker: "EA2_DRAFT_PROCESS", maxDepth: 5, maxNodes: 40, chat: [], uploads: [], panelState: {}, debug: [] } } }); // Auto-generate basic 5-step template const templateNodes = [ { _key: "n1", kind: "step", display_name: "Capture request details", dispatch_kind: "human" }, { _key: "n2", kind: "step", display_name: "Validate requirements", dispatch_kind: "agent", agent_capability: "evaluate_business_rule" }, { _key: "n3", kind: "step", display_name: "Prepare work package", dispatch_kind: "agent", agent_capability: "process_design_assistant" }, { _key: "n4", kind: "step", display_name: "Review and approve", dispatch_kind: "human" }, { _key: "n5", kind: "step", display_name: "Close out process", dispatch_kind: "human" } ]; updateDraft({ flowKey: res._key, step: "Analyze", nodes: templateNodes }); pushToast("ok", "Draft created in EA2"); } catch (err: any) { pushToast("err", `Failed to create draft: ${err.message}`); } finally { setWorking(false); } }; const handleStructureSave = async () => { if (!draft.flowKey || !actor) return; setWorking(true); try { const stepCreateOps: BatchOperation[] = draft.nodes.map(n => wizardApi.ops.createStep({ display_name: n.display_name, dispatch_kind: n.dispatch_kind, agent_capability: n.agent_capability, }) ); const viewCreateOps: BatchOperation[] = draft.nodes.map(n => wizardApi.ops.createView({ display_name: n.display_name }) ); const versionCreateOp: BatchOperation = wizardApi.ops.createVersion(draft.name || "Process"); const allCreateOps = [...stepCreateOps, ...viewCreateOps, versionCreateOp]; const createRes = await wizardApi.applyBatch(draft.flowKey, allCreateOps, actor); const createdOps: any[] = createRes?.result?.ops || []; const stepKeys = createdOps.slice(0, draft.nodes.length).map(o => o.key); const viewKeys = createdOps.slice(draft.nodes.length, draft.nodes.length * 2).map(o => o.key); const versionKey = createdOps[createdOps.length - 1]?.key; const nodesWithKeys = draft.nodes.map((n, i) => ({ ...n, _key: stepKeys[i] || n._key, viewKey: viewKeys[i] })); const wireOps: BatchOperation[] = [ ...nodesWithKeys.map((n, _i) => wizardApi.ops.childEdge(draft.flowKey!, n._key)), ...nodesWithKeys.map(n => wizardApi.ops.dispatchEdge(n._key)), ...nodesWithKeys.map(n => wizardApi.ops.presentationEdge(n._key, n.viewKey)), ]; if (versionKey) wireOps.push(wizardApi.ops.governsEdge(versionKey, draft.flowKey!)); if (wireOps.length) await wizardApi.applyBatch(draft.flowKey, wireOps, actor); updateDraft({ step: "Generate", nodes: nodesWithKeys }); pushToast("ok", `Saved ${stepKeys.length} steps with views and version to EA2`); } catch (err: any) { pushToast("err", `Save failed: ${err.message}`); } finally { setWorking(false); } }; const handleDataSave = async () => { if (!draft.flowKey || !actor) return; setWorking(true); try { await wizardApi.updateDraftConfig(draft.flowKey, { config: { wizard: { panelState: { fields: draft.fields } } } }); if (draft.fields.length > 0 && draft.nodes.length > 0) { const token = sessionStorage.getItem("fm.mc.token.v1") || ""; const oldEdgeKeys: string[] = []; for (const n of draft.nodes) { try { const r = await fetch( `${(await import("../lib/api")).api.config.baseUrl}/api/ea2/edges/defines?from=flow/${n._key}&limit=50`, { headers: { Authorization: `Bearer ${token}` } }, ); if (!r.ok) continue; const items = ((await r.json())?.items || []) as any[]; for (const e of items) { if (e?.role === "presentation" && e?._key) oldEdgeKeys.push(e._key); } } catch { /* ignore */ } } const replaceOps: BatchOperation[] = draft.nodes.map(n => wizardApi.ops.createView({ display_name: n.display_name, fields: draft.fields }) ); const res = await wizardApi.applyBatch(draft.flowKey, replaceOps, actor); const newViewKeys = (res?.result?.ops || []).map((o: any) => o.key); const wireOps: BatchOperation[] = [ ...oldEdgeKeys.map((k): BatchOperation => ({ op: "delete_edge", edge_coll: "defines", key: k })), ...draft.nodes.map((n, i) => wizardApi.ops.presentationEdge(n._key, newViewKeys[i])), ]; if (wireOps.length) await wizardApi.applyBatch(draft.flowKey, wireOps, actor); updateDraft({ nodes: draft.nodes.map((n, i) => ({ ...n, viewKey: newViewKeys[i] })) }); } updateDraft({ step: "Validate" }); pushToast("ok", `Saved ${draft.fields.length} fields onto every step's form`); } catch (err: any) { pushToast("err", `Save failed: ${err.message}`); } finally { setWorking(false); } }; const handleRulesSave = async () => { if (!draft.flowKey || !actor) return; setWorking(true); try { await wizardApi.updateDraftConfig(draft.flowKey, { config: { wizard: { panelState: { rules: draft.rules } } } }); updateDraft({ step: "Draft saved" }); pushToast("ok", "Rules saved"); } catch (err: any) { pushToast("err", `Save failed: ${err.message}`); } finally { setWorking(false); } }; const handlePublish = async () => { if (!draft.flowKey || !actor) return; setWorking(true); try { const ops: BatchOperation[] = [wizardApi.ops.publishFlow(draft.flowKey)]; await wizardApi.applyBatch(draft.flowKey, ops, actor); updateDraft({ step: "Publish" }); setPublishedKey(draft.flowKey); pushToast("ok", "Process published successfully!"); refreshLive(); } catch (err: any) { pushToast("err", `Publish failed: ${err.message}`); } finally { setWorking(false); } }; const handleStartInstance = async () => { if(!publishedKey) return; setWorking(true); try { await wizardApi.startInstance(publishedKey); pushToast("ok", "New instance started!"); setScene("mission"); } catch(err:any) { pushToast("err", `Failed to start: ${err.message}`); } finally { setWorking(false); } } const STEP_LABELS: Record = { Intake: "Describe", Analyze: "Steps", Generate: "Form fields", Validate: "Rules", "Draft saved": "Review", Publish: "Done", }; const renderStepNav = () => (
{STEPS.map((step, i) => { const isActive = step === draft.step; const isPast = STEPS.indexOf(step) < STEPS.indexOf(draft.step); const isDone = isPast || draft.step === "Publish"; return (
{isDone ? : i + 1} {STEP_LABELS[step]} {i < STEPS.length - 1 && }
); })}
); const renderGraphPreview = () => (
Flow · {draft.nodes.length} step{draft.nodes.length === 1 ? "" : "s"}
{draft.nodes.map((n, i) => (
{i + 1} {n.display_name || "Untitled"}
{({ human: "Manual", agent: "Assistant", system: "System" } as Record)[n.dispatch_kind || "human"]}
{i < draft.nodes.length - 1 &&
}
))}
); const renderFormPreview = () => (
Form preview · what users will fill in
{draft.fields.filter((f) => f.name).length === 0 ? (
Add fields on the left to see the form here.
) : ( draft.fields.filter((f) => f.name).map((f, i) => (
{f.type === "boolean" ? ( ) : f.type === "number" ? ( ) : ( )}
)) )}
); const renderRulesPreview = () => (
Rules · {draft.rules.filter((r) => r.trim()).length}
    {draft.rules.filter((r) => r.trim()).map((r, i) =>
  • {r}
  • )} {draft.rules.filter((r) => r.trim()).length === 0 && (
  • No rules typed yet
  • )}
); const renderPreviewPane = () => { if (draft.nodes.length === 0 && draft.fields.length === 0 && draft.rules.length === 0) return null; return ( ); }; const moveStep = (i: number, dir: -1 | 1) => { const j = i + dir; if (j < 0 || j >= draft.nodes.length) return; const next = [...draft.nodes]; [next[i], next[j]] = [next[j], next[i]]; updateDraft({ nodes: next }); }; const removeStep = (i: number) => { if (draft.nodes.length <= 1) { pushToast("err", "A process needs at least one step."); return; } updateDraft({ nodes: draft.nodes.filter((_, idx) => idx !== i) }); }; const addStep = () => { const next = [...draft.nodes, { _key: `n${Date.now()}`, kind: "step", display_name: "New step", dispatch_kind: "human" }]; updateDraft({ nodes: next }); }; return (
Process Creation Wizard

Build a new process

{renderStepNav()}
{draft.step === "Intake" && (

What process do you want to build?

updateDraft({ name: e.target.value })} placeholder="e.g. Laptop Procurement" />