'use client' import { useCallback, useMemo } from 'react' import { useParams } from 'next/navigation' import { ReactFlow, Background, Controls, MiniMap, useNodesState, useEdgesState, type Node, type Edge, MarkerType, } from '@xyflow/react' import '@xyflow/react/dist/style.css' import { useKnowledgeGraph } from './_hooks/useKnowledgeGraph' const TYPE_STYLES: Record = { component: { bg: '#EEF2FF', border: '#6366F1' }, hazard: { bg: '#FEF2F2', border: '#EF4444' }, mitigation: { bg: '#ECFDF5', border: '#10B981' }, } const TYPE_LABELS: Record = { component: 'Komponente', hazard: 'Gefaehrdung', mitigation: 'Massnahme', } export default function KnowledgeGraphPage() { const { projectId } = useParams<{ projectId: string }>() const { nodes: graphNodes, edges: graphEdges, loading, stats } = useKnowledgeGraph(projectId) // Convert to React Flow nodes with layout const rfNodes = useMemo((): Node[] => { const compNodes = graphNodes.filter((n) => n.type === 'component') const hazNodes = graphNodes.filter((n) => n.type === 'hazard') const mitNodes = graphNodes.filter((n) => n.type === 'mitigation') const nodes: Node[] = [] const colWidth = 300 const rowHeight = 80 // Column 1: Components compNodes.forEach((n, i) => { nodes.push({ id: n.id, position: { x: 0, y: i * rowHeight }, data: { label: n.label, subLabel: n.subLabel, nodeType: n.type }, style: { background: TYPE_STYLES.component.bg, border: `2px solid ${TYPE_STYLES.component.border}`, borderRadius: '12px', padding: '8px 12px', fontSize: '12px', fontWeight: 500, width: 200, }, }) }) // Column 2: Hazards hazNodes.forEach((n, i) => { nodes.push({ id: n.id, position: { x: colWidth, y: i * rowHeight }, data: { label: n.label, subLabel: n.subLabel, nodeType: n.type }, style: { background: TYPE_STYLES.hazard.bg, border: `2px solid ${TYPE_STYLES.hazard.border}`, borderRadius: '12px', padding: '8px 12px', fontSize: '12px', fontWeight: 500, width: 220, }, }) }) // Column 3: Mitigations mitNodes.forEach((n, i) => { nodes.push({ id: n.id, position: { x: colWidth * 2, y: i * rowHeight }, data: { label: n.label, subLabel: n.subLabel, nodeType: n.type }, style: { background: TYPE_STYLES.mitigation.bg, border: `2px solid ${TYPE_STYLES.mitigation.border}`, borderRadius: '12px', padding: '8px 12px', fontSize: '12px', fontWeight: 500, width: 220, }, }) }) return nodes }, [graphNodes]) const rfEdges = useMemo((): Edge[] => { return graphEdges.map((e) => ({ id: e.id, source: e.source, target: e.target, label: e.label, type: 'smoothstep', animated: true, style: { stroke: '#94A3B8', strokeWidth: 1.5 }, labelStyle: { fontSize: 10, fill: '#64748B' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#94A3B8' }, })) }, [graphEdges]) const [nodes, setNodes, onNodesChange] = useNodesState(rfNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(rfEdges) // Update when data loads const onInit = useCallback(() => { if (rfNodes.length > 0) { setNodes(rfNodes) setEdges(rfEdges) } }, [rfNodes, rfEdges, setNodes, setEdges]) if (loading) { return (
) } return (
{/* Header */}

Safety Knowledge Graph

Interaktive Visualisierung: Komponente → Gefaehrdung → Massnahme

{/* Legend + Stats */}
{(['component', 'hazard', 'mitigation'] as const).map((t) => (
{TYPE_LABELS[t]} ({ t === 'component' ? stats.components : t === 'hazard' ? stats.hazards : stats.mitigations })
))}
{/* Graph */} {graphNodes.length === 0 ? (
Keine Daten — bitte zuerst Projekt initialisieren.
) : (
{ const t = (node.data as { nodeType?: string })?.nodeType || 'component' return TYPE_STYLES[t]?.border || '#94A3B8' }} maskColor="rgba(0,0,0,0.05)" />
)}
) }