'use client'; import { useCallback, useMemo, useState } from 'react'; import { ReactFlow, Background, Controls, MiniMap, ReactFlowProvider, useNodesState, useEdgesState, type Node, type Edge, type NodeMouseHandler } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { competencyStacks, allCompetencies } from '@nextcraft/mock-data'; import type { CompetencyStack, Competency } from '@nextcraft/types'; import { colors } from '@nextcraft/ui'; // Stack color → hex used for node fills + minimap. Sourced from the design // tokens (colors.*) rather than inline literals. const STACK_HEX: Record = { indigo: colors.primary[500], rose: colors.rose[500], violet: colors.violet[500], emerald: colors.accent[500], cyan: colors.cyan[500], }; const STACK_BG: Record = { indigo: colors.primary[50], rose: '#fff1f2', violet: '#f5f3ff', emerald: colors.accent[50], cyan: '#ecfeff', }; interface StackNodeData { label: string; kind: 'stack'; stackId: string; description: string; competencyCount: number; targetRoles: string; stackColor: string; [key: string]: unknown; } interface CompetencyNodeData { label: string; kind: 'competency'; stackId: string; description: string; stackColor: string; prerequisites: string[]; learnersMastering: number; [key: string]: unknown; } type GraphData = StackNodeData | CompetencyNodeData; type GraphNode = Node; function buildGraph(): { nodes: GraphNode[]; edges: Edge[] } { const nodes: GraphNode[] = []; const edges: Edge[] = []; // Place stacks in a horizontal row at the top. const stackCount = competencyStacks.length; const stackGap = 280; const stackY = 0; const startX = -((stackCount - 1) * stackGap) / 2; competencyStacks.forEach((stack: CompetencyStack, sIdx) => { const x = startX + sIdx * stackGap; const color = STACK_HEX[stack.color] ?? colors.primary[500]; nodes.push({ id: stack.id, type: 'default', position: { x, y: stackY }, data: { label: stack.name, kind: 'stack', stackId: stack.id, description: stack.description, competencyCount: stack.competencies.length, targetRoles: stack.targetRoles, stackColor: color, }, style: { background: STACK_BG[stack.color] ?? colors.primary[50], border: `2px solid ${color}`, color: colors.neutral[900], borderRadius: 12, padding: 10, fontWeight: 600, fontSize: 13, width: 220, textAlign: 'center', }, }); }); // Place competencies below their stack in a vertical column. competencyStacks.forEach((stack, sIdx) => { const stackX = startX + sIdx * stackGap; const color = STACK_HEX[stack.color] ?? colors.primary[500]; stack.competencies.forEach((comp: Competency, cIdx) => { // Show up to 3 competencies per stack in the graph (10-15 total). if (cIdx >= 3) return; const compId = comp.id; const y = 160 + cIdx * 90; nodes.push({ id: compId, type: 'default', position: { x: stackX + 30, y }, data: { label: comp.name, kind: 'competency', stackId: stack.id, description: comp.description, stackColor: color, prerequisites: comp.prerequisites, learnersMastering: 12 + ((cIdx + sIdx) % 40), }, style: { background: '#ffffff', border: `1.5px solid ${color}`, color: colors.neutral[700], borderRadius: 8, padding: 8, fontSize: 11, width: 200, }, }); // Edge: competency → parent stack. edges.push({ id: `e-${compId}-${stack.id}`, source: compId, target: stack.id, style: { stroke: color, strokeWidth: 1.5 }, type: 'smoothstep', }); }); }); // A few cross-stack dependency edges between competencies. const crossDeps: Array<{ from: string; to: string }> = [ { from: 'stack-orchestration-c001', to: 'stack-safety-c001' }, { from: 'stack-orchestration-c005', to: 'stack-science-c001' }, { from: 'stack-designer-c001', to: 'stack-orchestration-c001' }, ]; crossDeps.forEach((d, i) => { const fromComp = allCompetencies.find((c) => c.id === d.from); const toComp = allCompetencies.find((c) => c.id === d.to); if (!fromComp || !toComp) return; if (!nodes.find((n) => n.id === d.from) || !nodes.find((n) => n.id === d.to)) return; edges.push({ id: `cross-${i}`, source: d.from, target: d.to, style: { stroke: colors.neutral[400], strokeWidth: 1.5, strokeDasharray: '4 3' }, type: 'smoothstep', animated: true, }); }); return { nodes, edges }; } interface SelectedStackInfo { kind: 'stack'; label: string; description: string; competencyCount: number; targetRoles: string; stackColor: string; } interface SelectedCompetencyInfo { kind: 'competency'; label: string; description: string; stackId: string; stackName: string; stackColor: string; prerequisites: string[]; learnersMastering: number; } type SelectedInfo = SelectedStackInfo | SelectedCompetencyInfo | null; function GraphInner() { const initial = useMemo(() => buildGraph(), []); const [nodes, , onNodesChange] = useNodesState(initial.nodes); const [edges, , onEdgesChange] = useEdgesState(initial.edges); const [selected, setSelected] = useState(null); const handleNodeClick: NodeMouseHandler = useCallback((_evt, node) => { const data = node.data as StackNodeData | CompetencyNodeData; if (data.kind === 'stack') { setSelected({ kind: 'stack', label: data.label, description: data.description, competencyCount: data.competencyCount, targetRoles: data.targetRoles, stackColor: data.stackColor, }); } else { const stackName = competencyStacks.find((s) => s.id === data.stackId)?.name ?? data.stackId; setSelected({ kind: 'competency', label: data.label, description: data.description, stackId: data.stackId, stackName, stackColor: data.stackColor, prerequisites: data.prerequisites, learnersMastering: data.learnersMastering, }); } }, []); return (
{ const d = n.data as StackNodeData | CompetencyNodeData; return d.stackColor ?? colors.neutral[400]; }} maskColor="rgba(241, 245, 249, 0.7)" />
{/* Detail panel */}
); } export function CompetencyGraph() { return ( ); }