feat: connect presentation chat to timeline
This commit is contained in:
@@ -25,7 +25,15 @@ beforeAll(() => {
|
||||
} as unknown as typeof DOMRect;
|
||||
});
|
||||
|
||||
afterEach(() => cleanup());
|
||||
const setReplayMode = () => {
|
||||
window.sessionStorage.setItem("lda.workflowConsole.target", "file:///invalid");
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
window.sessionStorage.clear();
|
||||
window.location.hash = "";
|
||||
});
|
||||
|
||||
describe("PresentationRoute", () => {
|
||||
it("renders the presentation stage entry point", async () => {
|
||||
@@ -45,9 +53,10 @@ describe("PresentationRoute", () => {
|
||||
});
|
||||
|
||||
it("renders audience progress chrome without rail or mode label", async () => {
|
||||
setReplayMode();
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
expect(screen.getByText("Prepare the thesis readiness report.")).toBeInTheDocument();
|
||||
expect(screen.getByText(/replay fallback is active/i)).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText(/presentation scene rail/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText("scene position")).toBeInTheDocument();
|
||||
});
|
||||
@@ -62,6 +71,7 @@ describe("PresentationRoute", () => {
|
||||
});
|
||||
|
||||
it("can advance replay far enough to show a product operation block", async () => {
|
||||
setReplayMode();
|
||||
window.location.hash = "#scene/workflow-demo/operation";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
@@ -104,6 +114,7 @@ describe("PresentationRoute", () => {
|
||||
});
|
||||
|
||||
it("opens the inspector from an explicit operation action", async () => {
|
||||
setReplayMode();
|
||||
const user = userEvent.setup();
|
||||
window.location.hash = "#scene/workflow-demo/operation";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
@@ -113,6 +124,7 @@ describe("PresentationRoute", () => {
|
||||
});
|
||||
|
||||
it("closes the inspector from the explicit close action", async () => {
|
||||
setReplayMode();
|
||||
const user = userEvent.setup();
|
||||
window.location.hash = "#scene/workflow-demo/operation";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
@@ -124,6 +136,7 @@ describe("PresentationRoute", () => {
|
||||
});
|
||||
|
||||
it("returns to the receipt after closing the inspector on a receipt beat", async () => {
|
||||
setReplayMode();
|
||||
const user = userEvent.setup();
|
||||
window.location.hash = "#scene/interrupt-evidence/trace";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
@@ -135,6 +148,22 @@ describe("PresentationRoute", () => {
|
||||
expect(screen.getByRole("button", { name: /inspect evidence/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a chat run action on the presentation route", async () => {
|
||||
window.location.hash = "#scene/agent-handoff/request";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(await screen.findByRole("button", { name: /run prepared workflow|run replay walkthrough/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses stored target for live presentation mode", async () => {
|
||||
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(await screen.findByRole("button", { name: /run prepared workflow/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens Scene 10 approval from the canonical hash", async () => {
|
||||
window.location.hash = "#scene/interrupt-evidence/approval";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import { createPreparedRecipeDriver, assertNever } from "../demo/agent/preparedRecipeDriver.js";
|
||||
import { useDemoAgent } from "../demo/agent/useDemoAgent.js";
|
||||
import { resolvePresentationTarget } from "./live-target.js";
|
||||
import { useTimelineAgent } from "../demo/agent/timelineAgent.js";
|
||||
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
|
||||
import { useDemoTimeline } from "../demo/useDemoTimeline.js";
|
||||
import { PresentationCanvas } from "./PresentationCanvas.js";
|
||||
@@ -47,18 +47,13 @@ export const PresentationRoute = () => {
|
||||
const recordEvidence = useCallback((record: EvidenceRecord) => {
|
||||
setEvidence((records) => [...records, record]);
|
||||
}, []);
|
||||
const demo = useDemoTimeline(null, recordEvidence, recording);
|
||||
|
||||
const agentDriver = useMemo(() => createPreparedRecipeDriver(recording), [recording]);
|
||||
const agent = useDemoAgent(agentDriver);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
agent.submitApproval({ approved: true, comment: "Approved by operator." });
|
||||
}, [agent]);
|
||||
|
||||
const handleDeny = useCallback(() => {
|
||||
agent.submitApproval({ approved: false, comment: "Denied by operator." });
|
||||
}, [agent]);
|
||||
const presentationTarget = useMemo(() => resolvePresentationTarget(), []);
|
||||
const demo = useDemoTimeline(presentationTarget.target, recordEvidence, recording);
|
||||
const timelineAgent = useTimelineAgent(
|
||||
demo,
|
||||
presentationTarget.mode === "live" ? "live" : "replay",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = hashForLocation(state.location);
|
||||
@@ -96,39 +91,24 @@ export const PresentationRoute = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (demo.state.phase === "ready" && demo.state.mode !== "replay") {
|
||||
if (demo.state.phase === "ready" && presentationTarget.mode === "replay" && demo.state.mode !== "replay") {
|
||||
demo.setMode("replay");
|
||||
}
|
||||
}, [demo.state.phase, demo.state.mode, demo.setMode]);
|
||||
if (demo.state.phase === "ready" && presentationTarget.mode === "live" && demo.state.mode !== "live") {
|
||||
demo.setMode("live");
|
||||
}
|
||||
}, [demo.state.phase, demo.state.mode, demo.setMode, presentationTarget.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (demo.state.phase === "ready" && demo.state.mode === "replay") {
|
||||
if (presentationTarget.mode === "replay" && demo.state.phase === "ready" && demo.state.mode === "replay") {
|
||||
demo.start();
|
||||
}
|
||||
}, [demo.state.phase, demo.state.mode, demo.start]);
|
||||
}, [demo.state.phase, demo.state.mode, demo.start, presentationTarget.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: "set_playback_mode", mode: demo.state.mode });
|
||||
}, [demo.state.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
for (const action of agent.pendingActions) {
|
||||
switch (action.type) {
|
||||
case "selectWorkflowNode":
|
||||
dispatch({ type: "select_node", nodeId: action.nodeId });
|
||||
break;
|
||||
case "focusOperation":
|
||||
case "showTraceFrame":
|
||||
break;
|
||||
default:
|
||||
assertNever(action);
|
||||
}
|
||||
}
|
||||
if (agent.pendingActions.length > 0) {
|
||||
agent.clearPendingActions();
|
||||
}
|
||||
}, [agent.pendingActions, agent.clearPendingActions]);
|
||||
|
||||
const handleJump = useCallback(
|
||||
(location: MainLocation) => dispatch({ type: "jump", location }),
|
||||
[],
|
||||
@@ -151,9 +131,7 @@ export const PresentationRoute = () => {
|
||||
state={state}
|
||||
demo={demo}
|
||||
evidence={evidence}
|
||||
messages={agent.messages}
|
||||
onApprove={agent.phase === "awaiting-approval" ? handleApprove : undefined}
|
||||
onDeny={agent.phase === "awaiting-approval" ? handleDeny : undefined}
|
||||
timelineAgent={timelineAgent}
|
||||
jump={handleJump}
|
||||
selectNode={(nodeId) => dispatch({ type: "select_node", nodeId })}
|
||||
openEvidence={() => dispatch({ type: "set_evidence_presentation", presentation: "inspector" })}
|
||||
@@ -165,3 +143,4 @@ export const PresentationRoute = () => {
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { domAnimation, LayoutGroup, LazyMotion } from "motion/react";
|
||||
import type { EvidenceRecord } from "../app/state.js";
|
||||
import type { AgentMessage } from "../demo/agent/events.js";
|
||||
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
|
||||
import { SceneBody } from "./SceneBody.js";
|
||||
import { DiscussionPanel } from "./DiscussionPanel.js";
|
||||
import { EvidenceInspector } from "./evidence/EvidenceInspector.js";
|
||||
@@ -16,6 +17,7 @@ type PresentationStageProps = {
|
||||
readonly demo: DemoTimelineController;
|
||||
readonly evidence: readonly EvidenceRecord[];
|
||||
readonly messages?: ReadonlyArray<AgentMessage>;
|
||||
readonly timelineAgent?: TimelineAgentController | undefined;
|
||||
readonly onApprove?: (() => void) | undefined;
|
||||
readonly onDeny?: (() => void) | undefined;
|
||||
readonly jump: (location: MainLocation) => void;
|
||||
@@ -31,6 +33,7 @@ export const PresentationStage = ({
|
||||
demo,
|
||||
evidence,
|
||||
messages,
|
||||
timelineAgent,
|
||||
onApprove,
|
||||
onDeny,
|
||||
jump,
|
||||
@@ -57,7 +60,7 @@ export const PresentationStage = ({
|
||||
data-scene-view={activeSceneView}
|
||||
>
|
||||
<aside className="presentation-stage__chat" aria-label="agent chat region">
|
||||
<OperatorChat state={state} messages={messages} onApprove={onApprove} onDeny={onDeny} />
|
||||
<OperatorChat state={state} messages={messages} timelineAgent={timelineAgent} onApprove={onApprove} onDeny={onDeny} />
|
||||
</aside>
|
||||
<section className="presentation-stage__primary" aria-label="primary presentation region">
|
||||
{state.location.kind === "discussion" ? (
|
||||
@@ -96,4 +99,4 @@ export const PresentationStage = ({
|
||||
</LayoutGroup>
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user