196 lines
6.9 KiB
TypeScript
196 lines
6.9 KiB
TypeScript
import type { DemoEvent } from "../demo/timeline/models.js";
|
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
|
import {
|
|
demoBeatLensForBeat,
|
|
graphExecutionForBeat,
|
|
projectInterruptContract,
|
|
projectOperationPresentation,
|
|
} from "./demo-workflow-model.js";
|
|
import { projectDemoRunFacts } from "./demo-run-facts.js";
|
|
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
|
import { demoSurfaceForBeat } from "./presentation-coherence.js";
|
|
import { DemoContinuityRail } from "./DemoContinuityRail.js";
|
|
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
|
|
import { GuidedProductMoment } from "./GuidedProductMoment.js";
|
|
import { InterruptContractPreview } from "./InterruptContractPreview.js";
|
|
import { NodeSpotlight } from "./NodeSpotlight.js";
|
|
import { OperationBlock } from "./OperationBlock.js";
|
|
import { RunInputFileBrowser } from "./RunInputFileBrowser.js";
|
|
import { StageCaption } from "./StageCaption.js";
|
|
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
|
|
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
|
|
|
type DemoWorkflowSceneProps = {
|
|
readonly scene: SceneDefinition;
|
|
readonly beat: SceneBeatDefinition;
|
|
readonly demo: DemoTimelineController;
|
|
readonly selectedNodeId: string | null;
|
|
readonly selectNode: (nodeId: string | null) => void;
|
|
readonly openEvidence: () => void;
|
|
readonly approvalActions?: DemoApprovalActions | undefined;
|
|
};
|
|
|
|
type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "evidence";
|
|
|
|
const layoutForBeat = (beatId: string): DemoWorkflowLayout => {
|
|
if (beatId === "operation" || beatId === "resume") return "operation";
|
|
if (beatId === "interrupt") return "interrupt";
|
|
if (beatId === "approval") return "approval";
|
|
if (beatId === "trace" || beatId === "output") return "evidence";
|
|
return "graph";
|
|
};
|
|
|
|
const operationStageByBeat: Readonly<Record<string, DemoEvent["stage"] | undefined>> = {
|
|
operation: "run_start",
|
|
resume: "run_resume",
|
|
trace: "trace_read",
|
|
};
|
|
|
|
const findEvent = (
|
|
demo: DemoTimelineController,
|
|
stage: DemoEvent["stage"],
|
|
): DemoEvent | null => demo.state.events.find((event) => event.stage === stage) ?? null;
|
|
|
|
export const DemoWorkflowScene = ({
|
|
scene,
|
|
beat,
|
|
demo,
|
|
selectedNodeId,
|
|
selectNode,
|
|
openEvidence,
|
|
approvalActions,
|
|
}: DemoWorkflowSceneProps) => {
|
|
const runStart = findEvent(demo, "run_start");
|
|
const runResume = findEvent(demo, "run_resume");
|
|
const currentStage = operationStageByBeat[beat.id];
|
|
const currentEvent = currentStage ? findEvent(demo, currentStage) : null;
|
|
const contract = runStart ? projectInterruptContract(runStart, runResume) : null;
|
|
const execution = graphExecutionForBeat(beat.id);
|
|
const layout = layoutForBeat(beat.id);
|
|
|
|
// Presentation-only proof labels keep the graph tied to the recorded run
|
|
// without changing the canonical replay event payload.
|
|
const graphVariant = beat.id === "interrupt" || beat.id === "approval" ? "compact" : "full";
|
|
|
|
const runProof = {
|
|
runId: runStart?.resultingIds.runId ?? null,
|
|
evidenceLabel: "JSON-RPC evidence",
|
|
};
|
|
|
|
const lens = demoBeatLensForBeat(beat.id);
|
|
const currentOperation = currentEvent ? projectOperationPresentation(currentEvent) : null;
|
|
const facts = projectDemoRunFacts(demo);
|
|
const surface = demoSurfaceForBeat(scene.id, beat.id);
|
|
const showOutcomePanel = beat.id === "resume" || beat.id === "output" || beat.id === "trace";
|
|
|
|
const showExpandedOperation = beat.id === "operation" || beat.id === "resume" || beat.id === "trace";
|
|
const showGraph = beat.id === "graph" || beat.id === "interrupt" || beat.id === "approval" || beat.id === "output";
|
|
const showReceipt = showGraph;
|
|
const contractMode = beat.id === "approval"
|
|
? "approval"
|
|
: beat.id === "interrupt"
|
|
? "preview"
|
|
: null;
|
|
|
|
const isGuidedDemoMoment = scene.id === "typed-human-boundary"
|
|
|| scene.id === "resume-output-evidence";
|
|
|
|
return (
|
|
<>
|
|
<StageCaption eyebrow="Live system walkthrough" title={scene.title}>
|
|
<p>{beat.caption}</p>
|
|
</StageCaption>
|
|
|
|
<DemoContinuityRail lens={lens} />
|
|
|
|
<div
|
|
className="demo-workflow-stage"
|
|
data-beat={beat.id}
|
|
data-demo-layout={layout}
|
|
data-graph-layout={beat.id === "graph" ? "horizontal" : undefined}
|
|
data-primary-surface={surface.primarySurface}
|
|
data-support-surface={surface.supportSurface}
|
|
aria-label="demo workflow stage"
|
|
>
|
|
{isGuidedDemoMoment ? (
|
|
<GuidedProductMoment
|
|
beat={beat}
|
|
demo={demo}
|
|
contract={contract}
|
|
operation={currentOperation}
|
|
approvalActions={approvalActions}
|
|
openEvidence={openEvidence}
|
|
/>
|
|
) : (
|
|
<>
|
|
{showExpandedOperation && currentEvent && (
|
|
<OperationBlock
|
|
event={currentEvent}
|
|
variant="expanded"
|
|
openEvidence={openEvidence}
|
|
/>
|
|
)}
|
|
|
|
{showReceipt && runStart && (
|
|
<OperationBlock
|
|
event={runStart}
|
|
variant="receipt"
|
|
openEvidence={openEvidence}
|
|
/>
|
|
)}
|
|
|
|
{beat.id === "input" && (
|
|
<div className="demo-workflow-stage__input-facts">
|
|
<RunInputFileBrowser
|
|
selectedDocuments={facts.input.selectedDocuments}
|
|
boardPath={facts.input.boardPath}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{showGraph && (
|
|
<div className="demo-workflow-stage__graph">
|
|
<WorkflowGraphStage
|
|
execution={execution}
|
|
selectedNodeId={selectedNodeId}
|
|
selectNode={selectNode}
|
|
proof={runProof}
|
|
variant={graphVariant}
|
|
/>
|
|
{contractMode && contract && (
|
|
<InterruptContractPreview
|
|
contract={contract}
|
|
mode={contractMode}
|
|
hero={layout === "approval"}
|
|
approvalActions={approvalActions}
|
|
interrupt={facts.interrupt}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{showOutcomePanel && (
|
|
<DemoOutcomePanel
|
|
beatId={beat.id}
|
|
lens={lens}
|
|
operation={currentOperation}
|
|
contract={contract}
|
|
/>
|
|
)}
|
|
|
|
{showExpandedOperation && !currentEvent && (
|
|
<div className="demo-workflow-stage__pending" role="status">
|
|
Replay operation is not available yet.
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{selectedNodeId && (
|
|
<NodeSpotlight nodeId={selectedNodeId} close={() => selectNode(null)} />
|
|
)}
|
|
</>
|
|
);
|
|
};
|