fix: restore readable presentation graph language
This commit is contained in:
@@ -13,6 +13,7 @@ export type WorkflowGraphNodeData = {
|
|||||||
readonly nodeId: string;
|
readonly nodeId: string;
|
||||||
readonly kind: WorkflowGraphNodeKind;
|
readonly kind: WorkflowGraphNodeKind;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
|
readonly detail?: string | null;
|
||||||
readonly nodeRef: string | null;
|
readonly nodeRef: string | null;
|
||||||
readonly raw: Readonly<Record<string, unknown>>;
|
readonly raw: Readonly<Record<string, unknown>>;
|
||||||
readonly onSelect?: (nodeId: string) => void;
|
readonly onSelect?: (nodeId: string) => void;
|
||||||
@@ -144,6 +145,7 @@ export const buildWorkflowGraph = (
|
|||||||
nodeId: id,
|
nodeId: id,
|
||||||
kind: mapNodeKind(node.type as string),
|
kind: mapNodeKind(node.type as string),
|
||||||
label: buildLabel(node, layout.label),
|
label: buildLabel(node, layout.label),
|
||||||
|
detail: typeof node.detail === "string" ? node.detail : null,
|
||||||
nodeRef: (node.node as string | null) ?? null,
|
nodeRef: (node.node as string | null) ?? null,
|
||||||
raw: node as Record<string, unknown>,
|
raw: node as Record<string, unknown>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ export const NodeSpotlight = ({ nodeId, close }: NodeSpotlightProps) => {
|
|||||||
const node = presentationNodes.find((candidate) => candidate.id === nodeId);
|
const node = presentationNodes.find((candidate) => candidate.id === nodeId);
|
||||||
const label = node && "label" in node
|
const label = node && "label" in node
|
||||||
? node.label
|
? node.label
|
||||||
: node?.id === "review_issues"
|
: nodeId === "review_issues"
|
||||||
? "Review issues"
|
? "Review issues"
|
||||||
: node?.id ?? nodeId;
|
: nodeId;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previouslyFocused = document.activeElement instanceof HTMLElement
|
const previouslyFocused = document.activeElement instanceof HTMLElement
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ describe("PresentationRoute", () => {
|
|||||||
window.location.hash = "#scene/run-from-deployment/graph";
|
window.location.hash = "#scene/run-from-deployment/graph";
|
||||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||||
render(<PresentationRoute />);
|
render(<PresentationRoute />);
|
||||||
fireEvent.click(graphNodeByLabel(/issue review/i));
|
fireEvent.click(graphNodeByLabel(/review issues/i));
|
||||||
expect(screen.getByRole("dialog", { name: /issue review/i })).toBeInTheDocument();
|
expect(screen.getByRole("dialog", { name: /review issues/i })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Workflow node")).toBeInTheDocument();
|
expect(screen.getByText("Workflow node")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -380,6 +380,24 @@ describe("SceneBody", () => {
|
|||||||
expect(within(rail).getByRole("button", { name: /Hosted automation future-work/i })).toBeInTheDocument();
|
expect(within(rail).getByRole("button", { name: /Hosted automation future-work/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps discussion rails out of workflow proof scenes", () => {
|
||||||
|
const location: PresentationLocation = { kind: "main", sceneId: "run-from-deployment", beatId: "graph", focusPath: [] };
|
||||||
|
render(
|
||||||
|
<SceneBody
|
||||||
|
location={location}
|
||||||
|
demo={demo}
|
||||||
|
selectedNodeId={null}
|
||||||
|
selectNode={noop}
|
||||||
|
openEvidence={noop}
|
||||||
|
openDiscussion={noop}
|
||||||
|
onFocusPathChange={noop}
|
||||||
|
motionDisabled={false}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText("defense discussion topics")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps discussion rail actions wired to branch ids", async () => {
|
it("keeps discussion rail actions wired to branch ids", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
|
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ type SceneBodyProps = {
|
|||||||
readonly onScene9Advance?: (() => void) | undefined;
|
readonly onScene9Advance?: (() => void) | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const workflowDemoSceneIds = new Set([
|
||||||
|
"prepared-lifecycle",
|
||||||
|
"run-from-deployment",
|
||||||
|
"typed-human-boundary",
|
||||||
|
"resume-output-evidence",
|
||||||
|
]);
|
||||||
|
|
||||||
const DiscussionLinks = ({
|
const DiscussionLinks = ({
|
||||||
sceneId,
|
sceneId,
|
||||||
openDiscussion,
|
openDiscussion,
|
||||||
@@ -315,9 +322,12 @@ export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selec
|
|||||||
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
||||||
const beat = findBeat(sceneId, beatId) ?? scene.beats[0]!;
|
const beat = findBeat(sceneId, beatId) ?? scene.beats[0]!;
|
||||||
|
|
||||||
// The questions beat renders its own grouped discussion index. Suppress the
|
// The questions beat owns the full discussion index. Workflow demo scenes
|
||||||
// generic per-scene rail there so the same actions do not appear twice.
|
// keep the product proof uncluttered; their discussion topics remain
|
||||||
const showDiscussionRail = !(scene.id === "conclusion" && beat.id === "questions");
|
// available from the global presenter controls.
|
||||||
|
const isWorkflowDemoScene = workflowDemoSceneIds.has(scene.id);
|
||||||
|
const showDiscussionRail = !(scene.id === "conclusion" && beat.id === "questions")
|
||||||
|
&& !isWorkflowDemoScene;
|
||||||
const discussionLinks = showDiscussionRail ? <DiscussionLinks sceneId={scene.id} openDiscussion={openDiscussion} /> : null;
|
const discussionLinks = showDiscussionRail ? <DiscussionLinks sceneId={scene.id} openDiscussion={openDiscussion} /> : null;
|
||||||
const content = (() => {
|
const content = (() => {
|
||||||
switch (scene.view) {
|
switch (scene.view) {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe("WorkflowGraphStage", () => {
|
|||||||
expect(graph).toHaveTextContent("Create issues");
|
expect(graph).toHaveTextContent("Create issues");
|
||||||
expect(graph).toHaveTextContent("Finalise");
|
expect(graph).toHaveTextContent("Finalise");
|
||||||
expect(graph).toHaveTextContent("Revision requested");
|
expect(graph).toHaveTextContent("Revision requested");
|
||||||
expect(graph).toHaveTextContent("completed");
|
expect(graph).toHaveTextContent("persisted run");
|
||||||
expect(graph).not.toHaveTextContent("end_cancelled");
|
expect(graph).not.toHaveTextContent("end_cancelled");
|
||||||
expect(document.querySelectorAll(".workflow-graph-stage__node")).toHaveLength(10);
|
expect(document.querySelectorAll(".workflow-graph-stage__node")).toHaveLength(10);
|
||||||
expect(presentationWorkflowNodeIds).toEqual([
|
expect(presentationWorkflowNodeIds).toEqual([
|
||||||
@@ -98,8 +98,9 @@ describe("WorkflowGraphStage", () => {
|
|||||||
expect(screen.queryByText("Current")).not.toBeInTheDocument();
|
expect(screen.queryByText("Current")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Current interrupt")).not.toBeInTheDocument();
|
expect(screen.queryByText("Current interrupt")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Queued")).not.toBeInTheDocument();
|
expect(screen.queryByText("Queued")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("Completed")).not.toBeInTheDocument();
|
expect(screen.getByLabelText("workflow graph node types")).toHaveTextContent("Action");
|
||||||
expect(screen.queryByText("Human boundary")).not.toBeInTheDocument();
|
expect(screen.getByLabelText("workflow graph node types")).toHaveTextContent("Human boundary");
|
||||||
|
expect(screen.getByLabelText("workflow graph node types")).toHaveTextContent("Outcome");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps raw plan facts and canonical labeled edge order", () => {
|
it("keeps raw plan facts and canonical labeled edge order", () => {
|
||||||
|
|||||||
@@ -47,14 +47,15 @@ const PresentationFlowNode = ({ data }: NodeProps<Node<PresentationNodeData>>) =
|
|||||||
data-kind={data.kind}
|
data-kind={data.kind}
|
||||||
data-selected={data.selected}
|
data-selected={data.selected}
|
||||||
aria-pressed={data.selected}
|
aria-pressed={data.selected}
|
||||||
aria-label={data.label}
|
aria-label={`${data.label}${data.detail ? `, ${data.detail}` : ""}`}
|
||||||
|
title={data.nodeRef ?? undefined}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
data.selectNode(data.nodeId);
|
data.selectNode(data.nodeId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<strong>{data.label}</strong>
|
<strong>{data.label}</strong>
|
||||||
{data.nodeRef && <small>{data.nodeRef}</small>}
|
<small>{data.detail ?? data.nodeRef ?? "workflow step"}</small>
|
||||||
</button>
|
</button>
|
||||||
<Handle type="source" position={Position.Right} />
|
<Handle type="source" position={Position.Right} />
|
||||||
</>
|
</>
|
||||||
@@ -72,10 +73,10 @@ const WorkflowGraphStageInner = ({
|
|||||||
const model = useMemo(
|
const model = useMemo(
|
||||||
() => buildWorkflowGraph(presentationWorkflowPlan, {
|
() => buildWorkflowGraph(presentationWorkflowPlan, {
|
||||||
direction: "LR",
|
direction: "LR",
|
||||||
nodeWidth: 190,
|
nodeWidth: 150,
|
||||||
nodeHeight: 72,
|
nodeHeight: 64,
|
||||||
nodesep: 55,
|
nodesep: 35,
|
||||||
ranksep: 100,
|
ranksep: 80,
|
||||||
label: (node) => {
|
label: (node) => {
|
||||||
if (typeof node.label === "string") return node.label;
|
if (typeof node.label === "string") return node.label;
|
||||||
if (node.id === "review_issues") return "Review issues";
|
if (node.id === "review_issues") return "Review issues";
|
||||||
@@ -131,6 +132,11 @@ const WorkflowGraphStageInner = ({
|
|||||||
data-graph-variant={variant}
|
data-graph-variant={variant}
|
||||||
data-graph-direction="horizontal"
|
data-graph-direction="horizontal"
|
||||||
>
|
>
|
||||||
|
<div className="workflow-graph-stage__legend" aria-label="workflow graph node types">
|
||||||
|
<span data-kind="use"><i aria-hidden="true" />Action</span>
|
||||||
|
<span data-kind="interrupt"><i aria-hidden="true" />Human boundary</span>
|
||||||
|
<span data-kind="end"><i aria-hidden="true" />Outcome</span>
|
||||||
|
</div>
|
||||||
{variant === "full" && proof && (
|
{variant === "full" && proof && (
|
||||||
<div className="workflow-graph-stage__proof" aria-label="workflow graph 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>Run</b><code>{proof.runId ?? "run unavailable"}</code></span>
|
||||||
@@ -143,7 +149,7 @@ const WorkflowGraphStageInner = ({
|
|||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{ padding: 0.12, minZoom: 0.45, maxZoom: 1 }}
|
fitViewOptions={{ padding: 0.02, minZoom: 0.5, maxZoom: 1 }}
|
||||||
minZoom={0.25}
|
minZoom={0.25}
|
||||||
maxZoom={1.5}
|
maxZoom={1.5}
|
||||||
nodesDraggable={false}
|
nodesDraggable={false}
|
||||||
|
|||||||
@@ -791,7 +791,7 @@
|
|||||||
|
|
||||||
.workflow-graph-stage {
|
.workflow-graph-stage {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 24rem;
|
min-height: 25rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1px solid oklch(0.3 0.035 250);
|
border: 1px solid oklch(0.3 0.035 250);
|
||||||
border-radius: 0.7rem;
|
border-radius: 0.7rem;
|
||||||
@@ -802,46 +802,108 @@
|
|||||||
.workflow-graph-stage__node {
|
.workflow-graph-stage__node {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 4.5rem;
|
min-height: 4.2rem;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
padding: 0.7rem 0.8rem;
|
align-content: center;
|
||||||
border: 1px solid oklch(0.45 0.04 250);
|
padding: 0.62rem 0.72rem;
|
||||||
border-radius: 0.45rem;
|
border: 1px solid oklch(0.48 0.04 250);
|
||||||
background: oklch(0.19 0.03 250);
|
border-radius: 0.55rem;
|
||||||
|
background: oklch(0.17 0.03 250);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 160ms ease, background 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage[data-graph-direction="horizontal"] .react-flow__node {
|
.workflow-graph-stage[data-graph-direction="horizontal"] .react-flow__node {
|
||||||
width: 11.875rem;
|
width: 9.375rem;
|
||||||
min-height: 4.5rem;
|
min-height: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__node[data-kind="use"] {
|
||||||
|
border-color: oklch(0.62 0.12 195 / 0.78);
|
||||||
|
background: oklch(0.16 0.035 220);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node[data-kind="interrupt"] {
|
.workflow-graph-stage__node[data-kind="interrupt"] {
|
||||||
border-color: var(--accent-amber);
|
border: 2px solid var(--accent-amber);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: oklch(0.19 0.04 70 / 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node[data-kind="end"] {
|
.workflow-graph-stage__node[data-kind="end"] {
|
||||||
border-color: oklch(0.5 0.025 250);
|
min-width: 9.5rem;
|
||||||
|
border-color: oklch(0.64 0.025 250);
|
||||||
|
border-radius: 999px;
|
||||||
background: oklch(0.16 0.02 250);
|
background: oklch(0.16 0.02 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node[data-selected="true"] {
|
.workflow-graph-stage__node[data-selected="true"] {
|
||||||
outline: 2px solid var(--accent-cyan);
|
outline: 2px solid var(--accent-cyan);
|
||||||
|
outline-offset: 2px;
|
||||||
|
box-shadow: 0 0 0 4px oklch(0.72 0.17 195 / 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__node strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 700 0.9rem/1.05 var(--font-display);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__node[data-kind="interrupt"] strong {
|
||||||
|
color: var(--accent-amber);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage__node small {
|
.workflow-graph-stage__node small {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font: 0.62rem/1.25 var(--font-evidence);
|
font: 600 0.64rem/1.25 var(--font-evidence);
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.7rem;
|
||||||
|
right: 0.8rem;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.85rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font: 700 0.62rem/1 var(--font-evidence);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend span {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend i {
|
||||||
|
width: 0.42rem;
|
||||||
|
height: 0.42rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: oklch(0.62 0.12 195);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend [data-kind="interrupt"] i {
|
||||||
|
background: var(--accent-amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage__legend [data-kind="end"] i {
|
||||||
|
background: oklch(0.68 0.025 250);
|
||||||
|
}
|
||||||
|
|
||||||
.workflow-graph-stage .react-flow__edge-text {
|
.workflow-graph-stage .react-flow__edge-text {
|
||||||
paint-order: stroke;
|
paint-order: stroke;
|
||||||
stroke: oklch(0.13 0.025 250);
|
stroke: oklch(0.13 0.025 250);
|
||||||
stroke-width: 0.4rem;
|
stroke-width: 0.4rem;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
fill: var(--text-primary);
|
fill: var(--text-primary);
|
||||||
font: 700 0.68rem/1 var(--font-evidence);
|
font: 700 0.78rem/1 var(--font-evidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph-stage .react-flow__edge-textbg {
|
||||||
|
fill: oklch(0.13 0.025 250);
|
||||||
|
stroke: oklch(0.13 0.025 250);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage .react-flow__edge-path {
|
.workflow-graph-stage .react-flow__edge-path {
|
||||||
@@ -1847,7 +1909,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.workflow-graph-stage {
|
.workflow-graph-stage {
|
||||||
min-height: 10rem;
|
min-height: 22rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
export const presentationWorkflowPlan = {
|
export const presentationWorkflowPlan = {
|
||||||
nodes: [
|
nodes: [
|
||||||
{ id: "reset_board", type: "node", node: "local.issue_board.reset_issue_board", label: "Reset issue board" },
|
{ id: "reset_board", type: "node", node: "local.issue_board.reset_issue_board", label: "Reset issue board", detail: "issue board source" },
|
||||||
{ id: "read_docs", type: "node", node: "local.lda_docs.read_documents", label: "Read documents" },
|
{ id: "read_docs", type: "node", node: "local.lda_docs.read_documents", label: "Read documents", detail: "document source" },
|
||||||
{ id: "analyze", type: "node", node: "local.lda_report.analyze_documents", label: "Analyze documents" },
|
{ id: "analyze", type: "node", node: "local.lda_report.analyze_documents", label: "Analyze documents", detail: "report source" },
|
||||||
{ id: "build_report", type: "node", node: "local.lda_report.build_report", label: "Build report" },
|
{ id: "build_report", type: "node", node: "local.lda_report.build_report", label: "Build report", detail: "Markdown output" },
|
||||||
{ id: "draft_issues", type: "node", node: "local.lda_report.create_issue_drafts", label: "Draft issues" },
|
{ id: "draft_issues", type: "node", node: "local.lda_report.create_issue_drafts", label: "Draft issues", detail: "issue proposals" },
|
||||||
{ id: "review_issues", type: "interrupt", kind: "issue_review" },
|
{ id: "review_issues", type: "interrupt", kind: "issue_review", label: "Review issues", detail: "typed human boundary" },
|
||||||
{ id: "create_issues", type: "node", node: "local.issue_board.create_issues", label: "Create issues" },
|
{ id: "create_issues", type: "node", node: "local.issue_board.create_issues", label: "Create issues", detail: "selected issues only" },
|
||||||
{ id: "finalise", type: "node", node: "local.lda_report.finalise_report", label: "Finalise report" },
|
{ id: "finalise", type: "node", node: "local.lda_report.finalise_report", label: "Finalise report", detail: "state output" },
|
||||||
{ id: "revision_requested", type: "node", node: "local.lda_report.record_revision_request", label: "Revision requested" },
|
{ id: "revision_requested", type: "node", node: "local.lda_report.record_revision_request", label: "Revision requested", detail: "operator branch" },
|
||||||
{ id: "end_completed", type: "end", outcome: "completed" },
|
{ id: "end_completed", type: "end", outcome: "completed", label: "Completed", detail: "persisted run" },
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
{ from: "reset_board", to: "read_docs", outcome: "ok" },
|
{ from: "reset_board", to: "read_docs", outcome: "ok" },
|
||||||
|
|||||||
Reference in New Issue
Block a user