New web/ SPA (React 18 + Vite 5 + TypeScript + React Flow + dagre, all pinned, no CDN). Dark, three-pane layout: summary top bar, filterable task list, center auto-laid pipeline map, and a right drawer showing a selected task's history through each node (timeline with timestamps/duration/cost + Q&A/verdicts/plan). Map nodes color by live state, loop-backs render dashed, clicking a node filters the list, selecting a task highlights its path. Polls /api/state every 4s; topology fetched once. dist/ is gitignored (built on the Mac, rsynced).
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
import { memo } from "react";
|
|
import { Handle, Position, type NodeProps } from "reactflow";
|
|
import type { RFData } from "../layout";
|
|
|
|
// One pipeline stage on the map. Color encodes live state; a badge shows the
|
|
// live task count; gated (P3/inert) nodes render dimmed with a tag. ARIA role +
|
|
// label keep the map reachable for screen readers / keyboard users.
|
|
function MapNodeImpl({ data }: NodeProps<RFData>) {
|
|
const { meta, state, count, dimmed, onPath } = data;
|
|
const cls = [
|
|
"node",
|
|
`state-${state}`,
|
|
meta.gated ? "gated" : "",
|
|
dimmed ? "dimmed" : "",
|
|
onPath ? "on-path" : "",
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ");
|
|
|
|
return (
|
|
<div
|
|
className={cls}
|
|
role="group"
|
|
aria-label={`${meta.label} stage, ${state}, ${count} task${count === 1 ? "" : "s"}`}
|
|
tabIndex={0}
|
|
>
|
|
<Handle type="target" position={Position.Left} />
|
|
<div className="node-label">
|
|
{meta.label}
|
|
{meta.kind === "gate" && (
|
|
<span className="gate-pip" title="human gate" aria-hidden>
|
|
⛋
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="node-agent">{meta.agent}</div>
|
|
{count > 0 && (
|
|
<span className="node-count" aria-hidden>
|
|
{count}
|
|
</span>
|
|
)}
|
|
{meta.gated && <span className="node-gated-tag">inert</span>}
|
|
<Handle type="source" position={Position.Right} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export const MapNode = memo(MapNodeImpl);
|