fix: keep demo workflow graph in frame

This commit is contained in:
lda
2026-07-09 03:28:23 +07:00 Verified
parent 368ecdf0b9
commit 9910d3f91f
3 changed files with 86 additions and 9 deletions
@@ -1,7 +1,7 @@
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
import { presentationNodes, WorkflowGraphStage } from "./WorkflowGraphStage.js";
afterEach(() => cleanup());
@@ -58,4 +58,28 @@ describe("WorkflowGraphStage", () => {
expect(connectors).toHaveLength(4);
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
});
it("keeps all graph nodes inside the visible percentage frame", () => {
for (const node of presentationNodes) {
expect(node.x).toBeGreaterThanOrEqual(14);
expect(node.x).toBeLessThanOrEqual(86);
expect(node.y).toBeGreaterThanOrEqual(28);
expect(node.y).toBeLessThanOrEqual(72);
}
});
it("renders compact run proof inside the graph", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
selectedNodeId={null}
selectNode={vi.fn()}
proof={{ runId: "run_recorded_lda_report", traceLabel: "5 nodes", evidenceLabel: "JSON-RPC captured" }}
/>,
);
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("5 nodes");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("JSON-RPC captured");
});
});
@@ -12,11 +12,11 @@ export type PresentationNode = {
};
export const presentationNodes: ReadonlyArray<PresentationNode> = [
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 9, y: 57 },
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 30, y: 35 },
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 57 },
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 74, y: 35 },
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 92, y: 57 },
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 14, y: 58 },
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 34, y: 36 },
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 58 },
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 70, y: 36 },
{ id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 86, y: 58 },
];
type PresentationEdge = readonly [from: string, to: string];
@@ -30,10 +30,17 @@ const presentationEdges: ReadonlyArray<PresentationEdge> = [
type NodeExecutionState = "completed" | "current" | "future";
export type WorkflowGraphProof = {
readonly runId: string | null;
readonly traceLabel: string;
readonly evidenceLabel: string;
};
type WorkflowGraphStageProps = {
readonly execution: GraphExecutionPresentation;
readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string) => void;
readonly proof?: WorkflowGraphProof;
};
const executionStateForNode = (
@@ -55,6 +62,7 @@ export const WorkflowGraphStage = ({
execution,
selectedNodeId,
selectNode,
proof,
}: WorkflowGraphStageProps) => {
const markerPrefix = useId().replaceAll(":", "");
const arrowMarkerId = `${markerPrefix}-workflow-arrow`;
@@ -68,6 +76,14 @@ export const WorkflowGraphStage = ({
<span><i data-state="interrupt" />Human boundary</span>
</div>
{proof && (
<div className="workflow-graph-stage__proof" aria-label="workflow graph proof">
<span><b>Run</b><code>{proof.runId ?? "run unavailable"}</code></span>
<span><b>Trace</b>{proof.traceLabel}</span>
<span><b>Evidence</b>{proof.evidenceLabel}</span>
</div>
)}
<svg className="workflow-graph-stage__connectors" aria-hidden="true">
<defs>
<marker className="workflow-graph-stage__arrow-marker" id={arrowMarkerId} markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
@@ -400,16 +400,53 @@
transform: translate(-50%, -50%);
}
.workflow-graph-stage__proof {
position: absolute;
z-index: 4;
left: 0.8rem;
bottom: 0.7rem;
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
max-width: calc(100% - 1.6rem);
color: var(--text-secondary);
font: 600 0.58rem/1.2 var(--font-mono, monospace);
}
.workflow-graph-stage__proof span {
display: inline-flex;
align-items: center;
gap: 0.3rem;
border: 1px solid oklch(0.36 0.04 250 / 0.7);
border-radius: 999px;
padding: 0.18rem 0.45rem;
background: oklch(0.09 0.018 250 / 0.78);
}
.workflow-graph-stage__proof b {
color: var(--accent-cyan);
font-weight: 700;
text-transform: uppercase;
}
.workflow-graph-stage__proof code {
max-width: 14rem;
overflow: hidden;
color: var(--text-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.presentation-route .workflow-graph-stage__node {
position: relative;
display: grid;
gap: 0.25rem;
width: clamp(7.2rem, 11vw, 9.5rem);
min-height: 4.35rem;
width: clamp(7rem, 10vw, 8.75rem);
min-height: 4.15rem;
transform: none;
border: 1px solid var(--stage-line);
border-radius: 0.7rem;
padding: 0.65rem 0.75rem;
padding: 0.58rem 0.68rem;
background: oklch(0.15 0.025 250 / 0.96);
color: var(--text-primary);
text-align: left;