feat(wizard): per-phase preview — graph in Analyze, form in Generate
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:
@@ -2260,3 +2260,71 @@ select.studio-input { background: var(--bp-paper); }
|
|||||||
}
|
}
|
||||||
.global-banner-warn { background: #fff6e0; color: #5a3a00; border-color: #b08a4a; }
|
.global-banner-warn { background: #fff6e0; color: #5a3a00; border-color: #b08a4a; }
|
||||||
[data-theme="dark"] .global-banner-warn { background: #3a2a0a; color: #ffd690; border-color: #6b4a14; }
|
[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; }
|
||||||
|
|||||||
+60
-44
@@ -263,59 +263,75 @@ export default function Wizard() {
|
|||||||
</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 = () => {
|
const renderPreviewPane = () => {
|
||||||
if (draft.nodes.length === 0 && draft.fields.length === 0 && draft.rules.length === 0) return null;
|
if (draft.nodes.length === 0 && draft.fields.length === 0 && draft.rules.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<aside className="wizard-preview">
|
<aside className="wizard-preview">
|
||||||
<div className="wizard-preview-head">
|
<div className="wizard-preview-head">
|
||||||
<span className="mc-hero-eyebrow">Preview</span>
|
<span className="mc-hero-eyebrow">Live preview</span>
|
||||||
<div className="wizard-preview-title">{draft.name || "Untitled process"}</div>
|
<div className="wizard-preview-title">{draft.name || "Untitled process"}</div>
|
||||||
{draft.description && <div className="wizard-preview-desc">{draft.description.slice(0, 140)}</div>}
|
{draft.description && <div className="wizard-preview-desc">{draft.description.slice(0, 140)}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{draft.nodes.length > 0 && (
|
{(draft.step === "Analyze" || draft.step === "Generate" || draft.step === "Validate" || draft.step === "Draft saved") && draft.nodes.length > 0 && renderGraphPreview()}
|
||||||
<div className="wizard-preview-section">
|
{(draft.step === "Generate" || draft.step === "Validate" || draft.step === "Draft saved") && renderFormPreview()}
|
||||||
<div className="wizard-preview-label">Steps · {draft.nodes.length}</div>
|
{(draft.step === "Validate" || draft.step === "Draft saved") && renderRulesPreview()}
|
||||||
<ol className="wizard-preview-chain">
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{draft.rules.length > 0 && (
|
|
||||||
<div className="wizard-preview-section">
|
|
||||||
<div className="wizard-preview-label">Rules · {draft.rules.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>
|
|
||||||
)}
|
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user