fix: address review findings for editorial presentation
This commit is contained in:
@@ -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" } }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user