This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/web/src/components/MapNode.tsx
Adam Moussa 8530074551 ci(agent-team-web): wire Prettier + ESLint + Playwright and enable the CI steps (#65)
- Prettier 3.6.2: .prettierrc.json + .prettierignore + format/format:check
  scripts; formatted the web/ tree.
- ESLint 9 flat config (@eslint/js + typescript-eslint + react-hooks +
  react-refresh, eslint-config-prettier last so Prettier owns formatting);
  lint script; src/components/ui/** opts out of react-refresh (shadcn primitives
  co-export their cva variants). 0 problems.
- Playwright 1.61.1: playwright.config.ts (build+preview webServer) + an e2e
  Board-renders smoke against a mocked /api; test:e2e script. Reusable workflow
  installs chromium itself.
- ci-web.yaml: required-scripts -> format:check,lint,build,test,test:e2e;
  run-format-check/lint/e2e -> true; e2e-browser chromium.

All five scripts verified green locally.
2026-06-25 10:56:48 -04:00

91 lines
3.1 KiB
TypeScript

import { memo } from "react";
import { Handle, Position, type NodeProps } from "reactflow";
import { Lock, RotateCcw } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
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, loopBackTo } = data;
const canLoop = loopBackTo.length > 0;
return (
<div
className={cn(
// Base
"relative flex w-[184px] h-[72px] flex-col items-center justify-center gap-0.5 rounded-lg border bg-card px-3 py-2 text-center",
// State-driven border + tint
state === "active" && "border-status-active bg-status-active-bg",
state === "awaiting_human" &&
"border-status-waiting bg-status-waiting-bg",
state === "parked" && "border-status-parked bg-status-parked-bg",
state === "idle" && "border-border bg-card",
// Path highlight
onPath && "ring-2 ring-primary shadow-md shadow-primary/20",
// Dimmed when a task is selected and this node is not on its path
dimmed && "opacity-30",
)}
role="group"
aria-label={`${meta.label} stage, ${state}, ${count} task${count === 1 ? "" : "s"}`}
tabIndex={0}
>
<Handle type="target" position={Position.Left} />
{/* Count badge — top-right corner */}
{count > 0 && (
<span
className="absolute -top-2 -right-2 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-primary-foreground"
aria-hidden
>
{count}
</span>
)}
{/* Label row */}
<div className="flex items-center gap-1 font-semibold text-sm leading-tight text-foreground">
{meta.label}
{meta.kind === "gate" && (
<span className="inline-flex" title="human gate">
<Lock
className="h-3 w-3 shrink-0 text-muted-foreground"
aria-hidden
/>
</span>
)}
{canLoop && (
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center">
<RotateCcw
className="h-3 w-3 shrink-0 text-muted-foreground"
aria-label={`can send work back to ${loopBackTo.join(", ")}`}
/>
</span>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
can send work back to {loopBackTo.join(", ")}
</TooltipContent>
</Tooltip>
)}
</div>
{/* Agent name */}
<div className="text-xs text-muted-foreground leading-tight truncate max-w-full px-1">
{meta.agent}
</div>
<Handle type="source" position={Position.Right} />
</div>
);
}
export const MapNode = memo(MapNodeImpl);