Submatter/apps/web/components/mission-control/convoy-dag.tsx

130 lines
3.7 KiB
TypeScript
Raw Normal View History

"use client";
import { BriefSummary } from "@tasks/shared/orchestrator";
interface ConvoyDagProps {
briefs: Array<{
id: string;
briefNumber: number;
sliceDependencies: number[];
status: string | null;
}>;
completedBriefIds: Set<string>;
}
export function ConvoyDag({ briefs, completedBriefIds }: ConvoyDagProps) {
if (briefs.length === 0) return null;
const briefByNum = new Map(briefs.map((b) => [b.briefNumber, b]));
const nodeSize = 60;
const spacing = 180;
const startX = 80;
const startY = 80;
const getPosition = (num: number) => ({
x: startX + ((num - 1) % 4) * spacing,
y: startY + Math.floor((num - 1) / 4) * 140,
});
const briefsSorted = [...briefs].sort((a, b) => a.briefNumber - b.briefNumber);
return (
<div className="w-full h-[300px] overflow-auto bg-muted/30 rounded-lg p-4 relative">
<svg
className="w-full h-full"
style={{ width: "100%", height: "100%" }}
viewBox="0 0 800 600"
>
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<path d="M0,0 L10,3.5 L0,7 Z" fill="currentColor" />
</marker>
</defs>
{briefsSorted.map((brief) => {
const pos = getPosition(brief.briefNumber);
const isComplete = completedBriefIds.has(brief.id);
const isEligible = !isComplete && brief.sliceDependencies.every((dep) => {
const depBrief = briefByNum.get(dep);
return depBrief && completedBriefIds.has(depBrief.id);
});
brief.sliceDependencies.forEach((depNum) => {
const dep = briefByNum.get(depNum);
if (dep) {
const depPos = getPosition(depNum);
return (
<line
key={`edge-${depNum}-${brief.briefNumber}`}
x1={depPos.x + nodeSize}
y1={depPos.y + nodeSize / 2}
x2={pos.x}
y2={pos.y + nodeSize / 2}
stroke="currentColor"
strokeWidth={2}
markerEnd="url(#arrowhead)"
opacity={0.4}
/>
);
}
});
return (
<g key={`node-${brief.briefNumber}`} transform={`translate(${pos.x}, ${pos.y})`}>
<rect
width={nodeSize}
height={nodeSize}
rx={8}
fill={isComplete ? "var(--green-3)" : isEligible ? "var(--blue-3)" : "var(--gray-4)"}
stroke={isComplete ? "var(--green-7)" : isEligible ? "var(--blue-7)" : "var(--gray-7)"}
strokeWidth={2}
/>
<text
x={nodeSize / 2}
y={nodeSize / 2 + 4}
textAnchor="middle"
dominantBaseline="middle"
fill="var(--white)"
fontSize={14}
fontWeight={600}
>
B{brief.briefNumber}
</text>
{isComplete && (
<circle
cx={nodeSize - 12}
cy={12}
r={10}
fill="var(--green-7)"
/>
)}
</g>
);
})}
</svg>
<p className="mt-4 text-xs text-muted-foreground">
Green = Complete | Blue = Ready | Gray = Blocked
</p>
</div>
);
}
function getPosition(num: number) {
const nodeSize = 60;
const spacing = 180;
const startX = 80;
const startY = 80;
return {
x: 80 + ((num - 1) % 4) * 180,
y: 80 + Math.floor((num - 1) / 4) * 140,
};
}