feat(wizard): per-phase preview — graph in Analyze, form in Generate
build-and-publish / test (pull_request) Has been cancelled
build-and-publish / image (pull_request) Has been cancelled

The Wizard already has the progress stepper with checkmarks, the
preview pane, and the review summary card before Publish. The
remaining gap from the UX overhaul was that the preview pane showed
the same flat list regardless of phase. Specialise it:

- Analyze + onward: graph-style vertical chain with dispatch-kind
  bubbles (manual / assistant / system) and connector edges, so the
  operator sees the flow as a graph not a list.
- Generate + onward: form preview that renders disabled input/select
  controls for each named field, so the operator sees what end users
  will fill in.
- Validate + onward: rules list (unchanged).
- Draft saved: all three render together so the review card has the
  full picture beside it.

EA2 write contract is unchanged — createDraft / applyBatch /
publishFlow still fire at every phase transition. 31/31 vitest pass.
This commit is contained in:
canvas-bot
2026-06-15 04:18:04 +04:00
parent 50b42c7110
commit 0a57bbc464
2 changed files with 128 additions and 44 deletions
+68
View File
@@ -2260,3 +2260,71 @@ select.studio-input { background: var(--bp-paper); }
}
.global-banner-warn { background: #fff6e0; color: #5a3a00; border-color: #b08a4a; }
[data-theme="dark"] .global-banner-warn { background: #3a2a0a; color: #ffd690; border-color: #6b4a14; }
/* Wizard per-phase preview: graph + form */
.wizard-preview-graph {
display: flex;
flex-direction: column;
gap: 0;
}
.wizard-graph-node {
display: flex;
flex-direction: column;
align-items: stretch;
position: relative;
padding-bottom: 4px;
}
.wizard-graph-bubble {
display: grid;
grid-template-columns: 22px 1fr;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: 1px solid color-mix(in srgb, var(--bp-navy) 25%, transparent);
background: var(--bp-paper);
font-size: 12px;
}
.wizard-graph-bubble.kind-agent { background: color-mix(in srgb, var(--bp-amber) 18%, var(--bp-paper)); border-color: color-mix(in srgb, var(--bp-amber) 60%, var(--bp-navy)); }
.wizard-graph-bubble.kind-system { background: color-mix(in srgb, var(--bp-navy) 10%, var(--bp-paper)); }
.wizard-graph-idx { font-family: var(--bp-mono); font-size: 10px; color: var(--bp-muted); text-align: right; }
.wizard-graph-name { color: var(--bp-navy); font-weight: 600; line-height: 1.3; }
.wizard-graph-meta {
font-family: var(--bp-mono);
font-size: 9.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--bp-muted);
padding: 2px 0 0 30px;
}
.wizard-graph-edge {
width: 1px;
height: 12px;
background: color-mix(in srgb, var(--bp-navy) 35%, transparent);
margin: 4px 0 4px 10px;
}
.wizard-form-preview {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
border: 1px dashed color-mix(in srgb, var(--bp-navy) 25%, transparent);
background: color-mix(in srgb, var(--bp-paper) 70%, var(--bp-canvas));
}
.wizard-form-field { display: flex; flex-direction: column; gap: 3px; }
.wizard-form-label {
font-family: var(--bp-mono);
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--bp-muted);
}
.wizard-form-input {
padding: 6px 8px;
font-size: 12px;
border: 1px solid color-mix(in srgb, var(--bp-navy) 25%, transparent);
background: var(--bp-paper);
color: var(--bp-navy);
font-family: inherit;
}
.wizard-form-input:disabled { opacity: 0.7; }
+56 -40
View File
@@ -263,51 +263,53 @@ export default function Wizard() {
</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">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.nodes.length > 0 && (
const renderGraphPreview = () => (
<div className="wizard-preview-section">
<div className="wizard-preview-label">Steps · {draft.nodes.length}</div>
<ol className="wizard-preview-chain">
<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) => (
<li key={n._key || i} className="wizard-preview-chain-item">
<span className="wizard-preview-chain-idx">{i + 1}</span>
<span className="wizard-preview-chain-name">{n.display_name || "Untitled step"}</span>
<span className={`wizard-preview-chain-kind kind-${n.dispatch_kind || "human"}`}>{({ human: "Manual review", agent: "Assistant", system: "System" } as Record<string, string>)[n.dispatch_kind || "human"] || "Manual review"}</span>
</li>
))}
</ol>
<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>
)}
{draft.fields.length > 0 && (
<div className="wizard-preview-section">
<div className="wizard-preview-label">Form · {draft.fields.length} field{draft.fields.length === 1 ? "" : "s"}</div>
<ul className="wizard-preview-fields">
{draft.fields.filter((f) => f.name).map((f, i) => (
<li key={i}>
<span className="wizard-preview-field-name">{f.name}</span>
<span className="wizard-preview-field-type">· {f.type}</span>
</li>
))}
{draft.fields.filter((f) => f.name).length === 0 && (
<li className="wizard-preview-empty">No fields named yet</li>
)}
</ul>
<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>
);
{draft.rules.length > 0 && (
const renderFormPreview = () => (
<div className="wizard-preview-section">
<div className="wizard-preview-label">Rules · {draft.rules.length}</div>
<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 && (
@@ -315,7 +317,21 @@ export default function Wizard() {
)}
</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>
);
};