575 lines
24 KiB
TypeScript
575 lines
24 KiB
TypeScript
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<DraftState>(() => {
|
|
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<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem("fm.mc.wizard.draft", JSON.stringify(draft));
|
|
}, [draft]);
|
|
|
|
const updateDraft = (patch: Partial<DraftState>) => {
|
|
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<WizardStep, string> = {
|
|
Intake: "Describe",
|
|
Analyze: "Steps",
|
|
Generate: "Form fields",
|
|
Validate: "Rules",
|
|
"Draft saved": "Review",
|
|
Publish: "Done",
|
|
};
|
|
|
|
const renderStepNav = () => (
|
|
<div className="wizard-progress">
|
|
{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 (
|
|
<div key={step} className={`wizard-step-marker ${isActive ? "active" : ""} ${isPast ? "past" : ""}`}>
|
|
<span className={`step-num ${isDone ? "step-num-done" : ""}`}>
|
|
{isDone ? <Check size={11} /> : i + 1}
|
|
</span>
|
|
<span className="step-label">{STEP_LABELS[step]}</span>
|
|
{i < STEPS.length - 1 && <ChevronRight size={12} className="step-chevron" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
const renderGraphPreview = () => (
|
|
<div className="wizard-preview-section">
|
|
<div className="wizard-preview-label">Flow · {draft.nodes.length} step{draft.nodes.length === 1 ? "" : "s"}</div>
|
|
<div className="wizard-preview-graph">
|
|
{draft.nodes.map((n, i) => (
|
|
<div key={n._key || i} className="wizard-graph-node">
|
|
<div className={`wizard-graph-bubble kind-${n.dispatch_kind || "human"}`}>
|
|
<span className="wizard-graph-idx">{i + 1}</span>
|
|
<span className="wizard-graph-name">{n.display_name || "Untitled"}</span>
|
|
</div>
|
|
<div className="wizard-graph-meta">
|
|
{({ human: "Manual", agent: "Assistant", system: "System" } as Record<string, string>)[n.dispatch_kind || "human"]}
|
|
</div>
|
|
{i < draft.nodes.length - 1 && <div className="wizard-graph-edge" aria-hidden />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderFormPreview = () => (
|
|
<div className="wizard-preview-section">
|
|
<div className="wizard-preview-label">Form preview · what users will fill in</div>
|
|
<div className="wizard-form-preview">
|
|
{draft.fields.filter((f) => f.name).length === 0 ? (
|
|
<div className="wizard-preview-empty">Add fields on the left to see the form here.</div>
|
|
) : (
|
|
draft.fields.filter((f) => f.name).map((f, i) => (
|
|
<div key={i} className="wizard-form-field">
|
|
<label className="wizard-form-label">{f.name}</label>
|
|
{f.type === "boolean" ? (
|
|
<select className="wizard-form-input" disabled defaultValue=""><option value="">Yes / No</option></select>
|
|
) : f.type === "number" ? (
|
|
<input className="wizard-form-input" type="number" placeholder="0" disabled />
|
|
) : (
|
|
<input className="wizard-form-input" type="text" placeholder={`Enter ${f.name.toLowerCase()}`} disabled />
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderRulesPreview = () => (
|
|
<div className="wizard-preview-section">
|
|
<div className="wizard-preview-label">Rules · {draft.rules.filter((r) => r.trim()).length}</div>
|
|
<ul className="wizard-preview-rules">
|
|
{draft.rules.filter((r) => r.trim()).map((r, i) => <li key={i}>{r}</li>)}
|
|
{draft.rules.filter((r) => r.trim()).length === 0 && (
|
|
<li className="wizard-preview-empty">No rules typed yet</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
);
|
|
|
|
const renderPreviewPane = () => {
|
|
if (draft.nodes.length === 0 && draft.fields.length === 0 && draft.rules.length === 0) return null;
|
|
return (
|
|
<aside className="wizard-preview">
|
|
<div className="wizard-preview-head">
|
|
<span className="mc-hero-eyebrow">Live preview</span>
|
|
<div className="wizard-preview-title">{draft.name || "Untitled process"}</div>
|
|
{draft.description && <div className="wizard-preview-desc">{draft.description.slice(0, 140)}</div>}
|
|
</div>
|
|
|
|
{(draft.step === "Analyze" || draft.step === "Generate" || draft.step === "Validate" || draft.step === "Draft saved") && draft.nodes.length > 0 && renderGraphPreview()}
|
|
{(draft.step === "Generate" || draft.step === "Validate" || draft.step === "Draft saved") && renderFormPreview()}
|
|
{(draft.step === "Validate" || draft.step === "Draft saved") && renderRulesPreview()}
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div className="wizard-container">
|
|
<header className="wizard-header">
|
|
<div>
|
|
<div className="mc-hero-eyebrow"><Branch size={12} /> Process Creation Wizard</div>
|
|
<h2 className="mc-hero-title">Build a new process</h2>
|
|
</div>
|
|
{renderStepNav()}
|
|
</header>
|
|
|
|
<div className="wizard-body">
|
|
<div className="wizard-content">
|
|
{draft.step === "Intake" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">What process do you want to build?</h3>
|
|
<div className="field-group">
|
|
<label>Name (optional)</label>
|
|
<input
|
|
type="text"
|
|
value={draft.name}
|
|
onChange={e => updateDraft({ name: e.target.value })}
|
|
placeholder="e.g. Laptop Procurement"
|
|
/>
|
|
</div>
|
|
<div className="field-group">
|
|
<label>Describe the process</label>
|
|
<textarea
|
|
value={draft.description}
|
|
onChange={e => updateDraft({ description: e.target.value })}
|
|
placeholder="When a store manager requests a new laptop, run a procurement approval..."
|
|
rows={4}
|
|
/>
|
|
</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleIntakeSubmit}
|
|
disabled={!draft.description || working || !canPublish}
|
|
>
|
|
{working ? "Saving..." : "Start drafting →"}
|
|
</button>
|
|
{!canPublish && <p className="warning-text">Switch to live mode + sign in to create processes.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Analyze" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">What steps does it have?</h3>
|
|
<p className="panel-sub">Reorder, rename, or pick who does each step. Order is preserved when we write to EA2.</p>
|
|
<div className="node-list">
|
|
{draft.nodes.map((n, i) => (
|
|
<div key={n._key} className="node-card">
|
|
<div className="node-header">
|
|
<span className="node-idx">{i + 1}</span>
|
|
<input
|
|
value={n.display_name}
|
|
onChange={e => {
|
|
const newNodes = [...draft.nodes];
|
|
newNodes[i].display_name = e.target.value;
|
|
updateDraft({ nodes: newNodes });
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="node-meta">
|
|
<select
|
|
value={n.dispatch_kind}
|
|
onChange={e => {
|
|
const newNodes = [...draft.nodes];
|
|
newNodes[i].dispatch_kind = e.target.value;
|
|
updateDraft({ nodes: newNodes });
|
|
}}
|
|
>
|
|
<option value="human">Manual review</option>
|
|
<option value="agent">Assistant</option>
|
|
<option value="system">System</option>
|
|
</select>
|
|
<div className="node-controls">
|
|
<button className="icon-btn" onClick={() => moveStep(i, -1)} disabled={i === 0} title="Move up">↑</button>
|
|
<button className="icon-btn" onClick={() => moveStep(i, 1)} disabled={i === draft.nodes.length - 1} title="Move down">↓</button>
|
|
<button className="icon-btn" onClick={() => removeStep(i)} title="Remove"><Close size={12} /></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<button className="btn btn-secondary" onClick={addStep}>+ Add step</button>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={handleStructureSave} disabled={working}>
|
|
{working ? "Saving..." : "Confirm structure →"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Generate" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">What information do you need to collect?</h3>
|
|
<p className="panel-sub">These become the form every step asks for. You can add as many as you like.</p>
|
|
<div className="field-list">
|
|
{draft.fields.map((f, i) => (
|
|
<div key={i} className="field-row">
|
|
<input
|
|
placeholder="e.g. requester name"
|
|
value={f.name}
|
|
onChange={e => {
|
|
const newFields = [...draft.fields];
|
|
newFields[i].name = e.target.value;
|
|
updateDraft({ fields: newFields });
|
|
}}
|
|
/>
|
|
<select
|
|
value={f.type}
|
|
onChange={e => {
|
|
const newFields = [...draft.fields];
|
|
newFields[i].type = e.target.value;
|
|
updateDraft({ fields: newFields });
|
|
}}
|
|
>
|
|
<option value="string">Text</option>
|
|
<option value="number">Number</option>
|
|
<option value="boolean">Yes / No</option>
|
|
</select>
|
|
<button className="icon-btn" onClick={() => updateDraft({ fields: draft.fields.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
|
</div>
|
|
))}
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ fields: [...draft.fields, { name: "", type: "string" }] })}>+ Add field</button>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={handleDataSave} disabled={working}>
|
|
{working ? "Saving..." : "Confirm fields →"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Validate" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">Anything special the process should check?</h3>
|
|
<p className="panel-sub">Plain-English rules. We'll attach them as exception checks once published.</p>
|
|
<div className="field-list">
|
|
{draft.rules.map((r, i) => (
|
|
<div key={i} className="field-row">
|
|
<input
|
|
placeholder="e.g. If cost is over 5000, require VP approval"
|
|
value={r}
|
|
onChange={e => {
|
|
const newRules = [...draft.rules];
|
|
newRules[i] = e.target.value;
|
|
updateDraft({ rules: newRules });
|
|
}}
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<button className="icon-btn" onClick={() => updateDraft({ rules: draft.rules.filter((_, idx) => idx !== i) })}><Close size={12} /></button>
|
|
</div>
|
|
))}
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ rules: [...draft.rules, ""] })}>+ Add rule</button>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={handleRulesSave} disabled={working}>
|
|
{working ? "Saving..." : "Review process →"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Draft saved" && (
|
|
<div className="wizard-panel">
|
|
<h3 className="panel-h">Review & publish</h3>
|
|
<p className="panel-sub">Last look. Hit publish to write the definition + steps + form + version to EA2 in one atomic batch.</p>
|
|
<div className="wizard-review">
|
|
<div className="wizard-review-row">
|
|
<span className="wizard-review-label">Process</span>
|
|
<strong>{draft.name || "Untitled"}</strong>
|
|
</div>
|
|
{draft.description && (
|
|
<div className="wizard-review-row">
|
|
<span className="wizard-review-label">Purpose</span>
|
|
<span>{draft.description}</span>
|
|
</div>
|
|
)}
|
|
<div className="wizard-review-row">
|
|
<span className="wizard-review-label">Steps</span>
|
|
<span>{draft.nodes.length} ({draft.nodes.map((n) => n.display_name || "Untitled").join(" → ")})</span>
|
|
</div>
|
|
<div className="wizard-review-row">
|
|
<span className="wizard-review-label">Form fields</span>
|
|
<span>{draft.fields.filter((f) => f.name).length || "none"}</span>
|
|
</div>
|
|
<div className="wizard-review-row">
|
|
<span className="wizard-review-label">Rules</span>
|
|
<span>{draft.rules.filter((r) => r.trim()).length || "none"}</span>
|
|
</div>
|
|
</div>
|
|
<div className="actions-row">
|
|
<button className="btn btn-secondary" onClick={() => updateDraft({ step: "Intake" })}>Back to edit</button>
|
|
<button className="btn btn-primary" onClick={handlePublish} disabled={working}>
|
|
{working ? "Publishing..." : "Publish to EA2"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{draft.step === "Publish" && (
|
|
<div className="wizard-panel success-panel">
|
|
<div className="success-icon"><Check size={32} /></div>
|
|
<h3 className="panel-h">Process published</h3>
|
|
<p>Your process <strong>{draft.name || "Untitled"}</strong> is now active and ready to run.</p>
|
|
<div className="actions-row">
|
|
<button className="btn btn-secondary" onClick={() => { setDraft({ step: "Intake", name: "", description: "", nodes: [], edges: [], fields: [], rules: [] }); setPublishedKey(null); }}>Create another</button>
|
|
<button className="btn btn-primary" onClick={handleStartInstance} disabled={working}>
|
|
Start first instance
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{renderPreviewPane()}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|