fix: address review findings for editorial presentation

This commit is contained in:
lda
2026-07-05 22:44:23 +07:00 Verified
parent 117a8ace83
commit 597370ffcd
8 changed files with 39 additions and 55 deletions
@@ -53,16 +53,6 @@ describe("PresentationRoute", () => {
expect(await screen.findByText(/workflow.runs.resume/i)).toBeInTheDocument(); expect(await screen.findByText(/workflow.runs.resume/i)).toBeInTheDocument();
}); });
it("runs the prepared agent and applies the interrupt node action", async () => {
render(<PresentationRoute />);
await userEvent.click(screen.getByRole("button", { name: /run prepared agent/i }));
expect(await screen.findByText(/prepared workflow recipe/i)).toBeInTheDocument();
expect(screen.getAllByText(/selectWorkflowNode/i).length).toBeGreaterThanOrEqual(2);
expect(await screen.findByRole("dialog", { name: /issue review/i })).toBeInTheDocument();
});
it("opens a positioning branch via hash and returns to the parent scene first beat", async () => { it("opens a positioning branch via hash and returns to the parent scene first beat", async () => {
window.location.hash = "#discuss/hosted-automation"; window.location.hash = "#discuss/hosted-automation";
render(<PresentationRoute />); render(<PresentationRoute />);
@@ -11,7 +11,6 @@ import {
presentationReducer, presentationReducer,
} from "./presentation-state.js"; } from "./presentation-state.js";
import { hashForLocation } from "./storyboard-navigation.js"; import { hashForLocation } from "./storyboard-navigation.js";
import { findScene } from "./storyboard.js";
import type { PresentationLocation } from "./storyboard.js"; import type { PresentationLocation } from "./storyboard.js";
import "./presentation.css"; import "./presentation.css";
import "./styles/demo-workflow.css"; import "./styles/demo-workflow.css";
@@ -153,22 +152,6 @@ export const PresentationRoute = () => {
[], [],
); );
const handleForceReplay = useCallback(() => {
demo.setMode("replay");
setEvidence(replayEvidence);
}, [demo, replayEvidence]);
const handleResetScene = useCallback(() => {
if (state.location.kind !== "main") return;
const scene = findScene(state.location.sceneId);
if (!scene || scene.beats.length === 0) return;
dispatch({ type: "jump", location: { kind: "main", sceneId: state.location.sceneId, beatId: scene.beats[0]!.id, focusPath: scene.beats[0]!.figure?.focusPath ?? [] } });
}, [state.location]);
const handleToggleMotion = useCallback(() => {
dispatch({ type: "toggle_motion" });
}, []);
return ( return (
<main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}> <main className="presentation-route" aria-label="lda.chat presentation" data-motion={state.motionDisabled ? "disabled" : "enabled"}>
<PresentationCanvas> <PresentationCanvas>
@@ -187,14 +170,6 @@ export const PresentationRoute = () => {
closeDiscussion={handleCloseDiscussion} closeDiscussion={handleCloseDiscussion}
/> />
</PresentationCanvas> </PresentationCanvas>
<button
type="button"
onClick={() => agent.startPreparedReplay()}
disabled={agent.phase === "running" || agent.phase === "awaiting-approval"}
className="presentation-route__agent-button"
>
Run prepared agent
</button>
</main> </main>
); );
}; };
@@ -74,7 +74,7 @@ export const PresentationStage = ({
jump({ ...state.location, focusPath: path }); jump({ ...state.location, focusPath: path });
} }
}} }}
motionDisabled={false} motionDisabled={state.motionDisabled}
/> />
)} )}
</section> </section>
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react"; import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from "react";
import { ReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react"; import { ReactFlow, ReactFlowProvider, useReactFlow, type Node, type Edge, type NodeTypes } from "@xyflow/react";
import "@xyflow/react/dist/style.css"; import "@xyflow/react/dist/style.css";
import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js"; import type { FigureCatalogDefinition, FigureNodeKind } from "./model.js";
import { layoutFigure, NODE_WIDTH, NODE_HEIGHT, type PositionedFigure } from "./layout.js"; import { layoutFigure, NODE_WIDTH, NODE_HEIGHT, type PositionedFigure } from "./layout.js";
@@ -45,6 +45,7 @@ const FigureFlowNode = ({ data }: { data: FigureNodeData }) => {
data-expandable={expandable} data-expandable={expandable}
data-testid={`figure-node-${data.nodeId}`} data-testid={`figure-node-${data.nodeId}`}
aria-label={accessibleName} aria-label={accessibleName}
tabIndex={-1}
onClick={() => { onClick={() => {
data.onActivate(data.nodeId); data.onActivate(data.nodeId);
if (expandable) data.onExpand(data.nodeId); if (expandable) data.onExpand(data.nodeId);
@@ -69,7 +70,15 @@ const nodeTypes: NodeTypes = {
figure: FigureFlowNode, figure: FigureFlowNode,
}; };
export const InteractiveFigure = ({ const FitViewOnLayoutChange = ({ layoutVersion }: { layoutVersion: number }) => {
const { fitView } = useReactFlow();
useEffect(() => {
void fitView({ padding: 0.15, duration: 0 });
}, [fitView, layoutVersion]);
return null;
};
const InteractiveFigureInner = ({
catalog, catalog,
focusPath, focusPath,
activeNodeId, activeNodeId,
@@ -82,7 +91,7 @@ export const InteractiveFigure = ({
activeNodeId ?? focus.figure.nodes[0]?.id ?? "", activeNodeId ?? focus.figure.nodes[0]?.id ?? "",
); );
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const prevFigureIdRef = useState(focus.figure.id); const [layoutVersion, setLayoutVersion] = useState(0);
useEffect(() => { useEffect(() => {
if (activeNodeId) setFocusedNodeId(activeNodeId); if (activeNodeId) setFocusedNodeId(activeNodeId);
@@ -91,6 +100,7 @@ export const InteractiveFigure = ({
useEffect(() => { useEffect(() => {
const firstNode = focus.figure.nodes[0]; const firstNode = focus.figure.nodes[0];
if (firstNode) setFocusedNodeId(firstNode.id); if (firstNode) setFocusedNodeId(firstNode.id);
setLayoutVersion((v) => v + 1);
}, [focus.figure.id]); }, [focus.figure.id]);
const handleExpand = useCallback( const handleExpand = useCallback(
@@ -177,6 +187,15 @@ export const InteractiveFigure = ({
[layout.edges], [layout.edges],
); );
const handleNodeClick = useCallback(
(_event: React.MouseEvent, node: Node) => {
const data = node.data as FigureNodeData;
setFocusedNodeId(data.nodeId);
if (data.isExpandable) handleExpand(data.nodeId);
},
[handleExpand],
);
return ( return (
<div <div
className="interactive-figure" className="interactive-figure"
@@ -205,8 +224,17 @@ export const InteractiveFigure = ({
zoomOnPinch={false} zoomOnPinch={false}
zoomOnDoubleClick={false} zoomOnDoubleClick={false}
preventScrolling={false} preventScrolling={false}
/> onNodeClick={handleNodeClick}
>
<FitViewOnLayoutChange layoutVersion={layoutVersion} />
</ReactFlow>
</div> </div>
</div> </div>
); );
}; };
export const InteractiveFigure = (props: InteractiveFigureProps) => (
<ReactFlowProvider>
<InteractiveFigureInner {...props} />
</ReactFlowProvider>
);
@@ -91,7 +91,7 @@ export const architectureCatalog: FigureCatalogDefinition = defineFigureCatalog(
label: "WorkflowServer", label: "WorkflowServer",
summary: "wf_server composition", summary: "wf_server composition",
kind: "runtime", kind: "runtime",
evidencePointer: "src/wf_server/server.py", evidencePointer: "src/wf_server/context.py",
}, },
{ {
id: "workflow-api", id: "workflow-api",
@@ -11,6 +11,10 @@
min-height: 0; min-height: 0;
} }
.interactive-figure .react-flow__pane {
pointer-events: none;
}
.interactive-figure .react-flow__node { .interactive-figure .react-flow__node {
cursor: default; cursor: default;
} }
@@ -417,19 +417,6 @@
font-weight: 600; font-weight: 600;
} }
.presentation-route__agent-button {
position: fixed;
bottom: 0.75rem;
right: 0.75rem;
padding: 0.45rem 0.75rem;
border: 1px solid oklch(0.4 0.05 250);
background: oklch(0.18 0.025 250);
color: inherit;
border-radius: 0.4rem;
font-size: 0.8rem;
z-index: 25;
}
.chat-tool-part { .chat-tool-part {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -140,7 +140,7 @@ export const mainScenes = defineScenes([
sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }), sceneBeat("client", "Client operations", "Human and agent clients use the same public lifecycle surface.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "client-operations" } }),
sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "application-lifecycle" } }), sceneBeat("api", "Transport and API", "JSON-RPC reaches WorkflowApi without owning domain behavior.", { figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "application-lifecycle" } }),
sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records.", { figure: { catalogId: "system-architecture", focusPath: ["runtime-providers"], activeNodeId: "configured-providers" } }), sceneBeat("runtime", "Runtime and providers", "The runtime resolves provider-neutral capabilities and stores lifecycle records.", { figure: { catalogId: "system-architecture", focusPath: ["runtime-providers"], activeNodeId: "configured-providers" } }),
sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek", figure: { catalogId: "system-architecture", focusPath: [], activeNodeId: "node-use" } }), sceneBeat("node-use", "NodeUse", "One callable node validates input, invokes a capability, and reduces output into state.", { evidenceMode: "peek", figure: { catalogId: "system-architecture", focusPath: ["node-use"], activeNodeId: "node-use" } }),
], ],
}, },
{ {