Mission Control demo v2
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
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Guided tour overlay. Reads the active scenario's tour script and
|
||||
// anchors a step-card to the named region.
|
||||
import { useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useApp, scenarioById } from "../state/store";
|
||||
import { Sparkles, ChevL, ChevR, Close } from "./icons";
|
||||
|
||||
export default function Tour() {
|
||||
const tour = useApp((s) => s.tour);
|
||||
const scenarioId = useApp((s) => s.scenarioId);
|
||||
const endTour = useApp((s) => s.endTour);
|
||||
const tourPrev = useApp((s) => s.tourPrev);
|
||||
const tourNext = useApp((s) => s.tourNext);
|
||||
|
||||
const sc = scenarioById(scenarioId);
|
||||
const step = sc?.tour[tour.index] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!tour.active) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowRight" || e.key === "Enter") { e.preventDefault(); tourNext(); }
|
||||
if (e.key === "ArrowLeft") { e.preventDefault(); tourPrev(); }
|
||||
if (e.key === "Escape") { e.preventDefault(); endTour(); }
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [tour.active, tourPrev, tourNext, endTour]);
|
||||
|
||||
if (!tour.active || !step || !sc) return null;
|
||||
|
||||
const last = tour.index === sc.tour.length - 1;
|
||||
const first = tour.index === 0;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
key={step.id}
|
||||
className={`tour-card tour-anchor-${step.anchor}`}
|
||||
initial={{ opacity: 0, y: 8, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -6 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
>
|
||||
<div className="tour-head">
|
||||
<Sparkles size={14} />
|
||||
<span className="tour-eyebrow">Guided tour · {tour.index + 1} / {sc.tour.length}</span>
|
||||
<button className="tour-close" onClick={endTour} aria-label="End tour"><Close size={13} /></button>
|
||||
</div>
|
||||
<h3 className="tour-title">{step.title}</h3>
|
||||
<p className="tour-body">{step.body}</p>
|
||||
<div className="tour-actions">
|
||||
<button className="btn btn-ghost" onClick={tourPrev} disabled={first}>
|
||||
<ChevL size={13} /> Back
|
||||
</button>
|
||||
{last ? (
|
||||
<button className="btn btn-primary" onClick={endTour}>Finish</button>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={tourNext}>
|
||||
Next <ChevR size={13} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user