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

92 lines
3.1 KiB
TypeScript
Raw Normal View History

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);