Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay
141 lines
5.9 KiB
TypeScript
141 lines
5.9 KiB
TypeScript
// Command palette v2 — scenarios, steps, tour, scenes, recents.
|
|
import { useEffect, useMemo } from "react";
|
|
import { Command } from "cmdk";
|
|
import { useApp, scenarioById } from "../state/store";
|
|
import { Play, Branch, Bot, Check, Sparkles, Home, History as HistoryIcon, Layers, Search, Refresh } from "./icons";
|
|
|
|
export default function CommandBar() {
|
|
const open = useApp((s) => s.cmdOpen);
|
|
const setOpen = useApp((s) => s.setCmdOpen);
|
|
const setScenarioId = useApp((s) => s.setScenarioId);
|
|
const setSelectedStepId = useApp((s) => s.setSelectedStepId);
|
|
const startTour = useApp((s) => s.startTour);
|
|
const setScene = useApp((s) => s.setScene);
|
|
const recents = useApp((s) => s.recents);
|
|
const scenarioId = useApp((s) => s.scenarioId);
|
|
const pushRecent = useApp((s) => s.pushRecent);
|
|
const scenarios = useApp((s) => s.scenarios);
|
|
const mode = useApp((s) => s.mode);
|
|
const setMode = useApp((s) => s.setMode);
|
|
const pushToast = useApp((s) => s.pushToast);
|
|
|
|
const sc = scenarioById(scenarioId);
|
|
|
|
const previewAction = (label: string) => {
|
|
pushToast("info", `${label} is preview-only in this demo. Wire to /api/runtime/transactions to make it real.`);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
|
e.preventDefault();
|
|
setOpen(!open);
|
|
}
|
|
if (e.key === "Escape" && open) setOpen(false);
|
|
};
|
|
document.addEventListener("keydown", onKey);
|
|
return () => document.removeEventListener("keydown", onKey);
|
|
}, [open, setOpen]);
|
|
|
|
const stepItems = useMemo(() => sc?.steps ?? [], [sc]);
|
|
|
|
if (!open) return null;
|
|
|
|
const close = () => setOpen(false);
|
|
|
|
return (
|
|
<div className="cmd-overlay" onClick={close}>
|
|
<Command className="cmd" onClick={(e) => e.stopPropagation()} label="Command bar">
|
|
<div className="cmd-input-row">
|
|
<Search size={14} />
|
|
<Command.Input autoFocus placeholder="Switch scenario · jump to step · start tour…" />
|
|
<kbd className="kbd-hint">esc</kbd>
|
|
</div>
|
|
<Command.List>
|
|
<Command.Empty>No matches.</Command.Empty>
|
|
|
|
{recents.length > 0 && (
|
|
<Command.Group heading="Recent">
|
|
{recents.map((r, i) => (
|
|
<Command.Item key={`r-${i}-${r}`} onSelect={() => { pushRecent(r); close(); }}>
|
|
<HistoryIcon size={13} /> {r}
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
)}
|
|
|
|
<Command.Group heading="Scenes">
|
|
<Command.Item onSelect={() => { setScene("landing"); close(); }}>
|
|
<Home size={13} /> Landing
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("mission"); close(); }}>
|
|
<Layers size={13} /> Mission Control
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setScene("history"); close(); }}>
|
|
<HistoryIcon size={13} /> Run History
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Tour & demo">
|
|
<Command.Item onSelect={() => { startTour(); close(); }}>
|
|
<Sparkles size={13} /> Start guided tour <span className="cmd-hint">presenter mode</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Scenarios">
|
|
{scenarios.map((s) => (
|
|
<Command.Item
|
|
key={s.id}
|
|
value={`scenario ${s.id} ${s.family.label} ${s.defName}`}
|
|
onSelect={() => { setScenarioId(s.id); setScene("mission"); close(); }}
|
|
>
|
|
<Branch size={13} style={{ color: s.family.accent }} />
|
|
{s.family.label}
|
|
<span className="cmd-hint">{s.live ? "live" : "blueprint"} · {s.defName}</span>
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
|
|
{sc && (
|
|
<Command.Group heading="Jump to step">
|
|
{stepItems.map((st) => (
|
|
<Command.Item
|
|
key={st.id}
|
|
value={`step ${st.id} ${st.name} ${st.kind}`}
|
|
onSelect={() => { setSelectedStepId(st.id); setScene("mission"); close(); }}
|
|
>
|
|
<span className={`dot dot-${st.state}`} aria-hidden /> {st.name}
|
|
<span className="cmd-hint mono">{st.kind}</span>
|
|
</Command.Item>
|
|
))}
|
|
</Command.Group>
|
|
)}
|
|
|
|
<Command.Group heading="Data mode">
|
|
<Command.Item onSelect={() => { setMode("live"); close(); }}>
|
|
<Refresh size={13} /> {mode === "live" ? "Refresh live scenarios" : "Switch to LIVE mode (fetch demo.flow-master.ai)"}
|
|
<span className="cmd-hint">{mode === "live" ? "re-fetch" : "in-browser"}</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { setMode("snapshot"); close(); }}>
|
|
<Layers size={13} /> Switch to SNAPSHOT mode
|
|
<span className="cmd-hint">bundled JSON</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
|
|
<Command.Group heading="Actions (preview-only)">
|
|
<Command.Item onSelect={() => { previewAction("Start runtime instance"); close(); }}>
|
|
<Play size={13} /> Start runtime instance <span className="cmd-hint">preview</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { previewAction("Dispatch sidekick agent"); close(); }}>
|
|
<Bot size={13} /> Dispatch sidekick agent <span className="cmd-hint">preview</span>
|
|
</Command.Item>
|
|
<Command.Item onSelect={() => { previewAction("Confirm awaiting agent runs"); close(); }}>
|
|
<Check size={13} /> Confirm awaiting agent runs <span className="cmd-hint">{sc?.agentRuns.length ?? 0} pending · preview</span>
|
|
</Command.Item>
|
|
</Command.Group>
|
|
</Command.List>
|
|
</Command>
|
|
</div>
|
|
);
|
|
}
|