'use client' /** * SDK Flow Visualization * * Interaktive React Flow Darstellung aller SDK-Steps: * - Farblich nach Package gruppiert (5 Packages) * - Prerequisite-Edges zeigen den sequenziellen Flow * - DB-Tabellen und RAG-Collections als optionale Nodes * - Detail-Panel bei Klick auf einen Step */ import { useCallback, useState, useMemo, useEffect } from 'react' import ReactFlow, { Node, Edge, Controls, Background, MiniMap, useNodesState, useEdgesState, BackgroundVariant, MarkerType, Panel, } from 'reactflow' import 'reactflow/dist/style.css' import { SDK_FLOW_STEPS, FLOW_PACKAGES, findProducerStep, getAllDbTables, getAllRagCollections, type SDKFlowStep, } from './flow-data' // ============================================================================= // TYPES // ============================================================================= type PackageFilter = 'alle' | SDKFlowStep['package'] // ============================================================================= // LAYOUT // ============================================================================= const PACKAGE_ORDER: SDKFlowStep['package'][] = [ 'vorbereitung', 'analyse', 'dokumentation', 'rechtliche-texte', 'betrieb', ] const PACKAGE_X_OFFSET: Record = { vorbereitung: 0, analyse: 320, dokumentation: 640, 'rechtliche-texte': 960, betrieb: 1280, } const NODE_WIDTH = 200 const NODE_HEIGHT = 70 const STEP_Y_SPACING = 100 const STEP_Y_START = 100 function getStepPosition(step: SDKFlowStep): { x: number; y: number } { const packageSteps = SDK_FLOW_STEPS .filter(s => s.package === step.package) .sort((a, b) => a.seq - b.seq) const idx = packageSteps.findIndex(s => s.id === step.id) return { x: PACKAGE_X_OFFSET[step.package] + 40, y: STEP_Y_START + idx * STEP_Y_SPACING, } } // ============================================================================= // DETAIL PANEL // ============================================================================= function DetailPanel({ step, onClose, }: { step: SDKFlowStep onClose: () => void }) { const pkg = FLOW_PACKAGES[step.package] const baseUrl = 'https://macmini:3007' return (

{step.name}

{pkg.icon} {pkg.name} seq {step.seq} {step.isOptional && ( Optional )}
{/* Beschreibung */}

{step.description}

{step.descriptionLong}

{step.legalBasis && (
Rechtsgrundlage: {step.legalBasis}
)}
{/* Inputs */} {step.inputs.length > 0 && (

Inputs (SDKState)

{step.inputs.map(input => { const producer = findProducerStep(input) return (
{input} {producer && ( ← {producer.nameShort} )}
) })}
)} {/* Outputs */} {step.outputs.length > 0 && (

Outputs (SDKState)

{step.outputs.map(output => (
{output}
))}
)} {/* DB Tables */} {step.dbTables.length > 0 && (

DB-Tabellen

{step.dbTables.map(table => (
{table} {step.dbMode}
))}
)} {/* RAG Collections */} {step.ragCollections.length > 0 && (

RAG-Collections

{step.ragCollections.map(rag => (
{rag}
))} {step.ragPurpose && (

{step.ragPurpose}

)}
)} {/* Checkpoint */} {step.checkpointId && (

Checkpoint

{step.checkpointId} {step.checkpointType}
{step.checkpointReviewer && step.checkpointReviewer !== 'NONE' && (
Reviewer: {step.checkpointReviewer}
)}
)} {/* Generated Docs */} {step.generates && step.generates.length > 0 && (

Generierte Dokumente

{step.generates.map(doc => (
{doc}
))}
)} {/* Prerequisites */} {step.prerequisiteSteps.length > 0 && (

Voraussetzungen

{step.prerequisiteSteps.map(preId => { const preStep = SDK_FLOW_STEPS.find(s => s.id === preId) return (
{preStep?.name || preId}
) })}
)} {/* Open in SDK */}
) } // ============================================================================= // MAIN COMPONENT // ============================================================================= export default function SDKFlowPage() { const [selectedStep, setSelectedStep] = useState(null) const [packageFilter, setPackageFilter] = useState('alle') const [showDb, setShowDb] = useState(false) const [showRag, setShowRag] = useState(false) const allDbTables = useMemo(() => getAllDbTables(), []) const allRagCollections = useMemo(() => getAllRagCollections(), []) // ========================================================================= // Build Nodes // ========================================================================= const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { const nodes: Node[] = [] const edges: Edge[] = [] const visibleSteps = packageFilter === 'alle' ? SDK_FLOW_STEPS : SDK_FLOW_STEPS.filter(s => s.package === packageFilter) const visibleStepIds = new Set(visibleSteps.map(s => s.id)) // Step Nodes visibleSteps.forEach(step => { const pkg = FLOW_PACKAGES[step.package] const pos = getStepPosition(step) const isSelected = selectedStep?.id === step.id // Checkpoint badge text let badge = '' if (step.checkpointId) { badge = step.checkpointType === 'REQUIRED' ? ' *' : ' ~' if (step.checkpointReviewer && step.checkpointReviewer !== 'NONE') { badge += ` [${step.checkpointReviewer}]` } } nodes.push({ id: step.id, type: 'default', position: pos, data: { label: (
{step.nameShort}
{step.checkpointId || ''} {badge}
), }, style: { background: isSelected ? pkg.color.border : pkg.color.bg, color: isSelected ? 'white' : pkg.color.text, border: `2px solid ${pkg.color.border}`, borderRadius: '10px', padding: '8px 4px', minWidth: `${NODE_WIDTH}px`, maxWidth: `${NODE_WIDTH}px`, cursor: 'pointer', boxShadow: isSelected ? `0 0 16px ${pkg.color.border}` : '0 1px 3px rgba(0,0,0,0.08)', opacity: step.isOptional ? 0.85 : 1, borderStyle: step.isOptional ? 'dashed' : 'solid', }, }) }) // Prerequisite Edges visibleSteps.forEach(step => { step.prerequisiteSteps.forEach(preId => { if (visibleStepIds.has(preId)) { edges.push({ id: `e-${preId}-${step.id}`, source: preId, target: step.id, type: 'smoothstep', animated: selectedStep?.id === preId || selectedStep?.id === step.id, style: { stroke: selectedStep?.id === preId || selectedStep?.id === step.id ? '#7c3aed' : '#94a3b8', strokeWidth: selectedStep?.id === preId || selectedStep?.id === step.id ? 2.5 : 1.5, }, markerEnd: { type: MarkerType.ArrowClosed, color: selectedStep?.id === preId || selectedStep?.id === step.id ? '#7c3aed' : '#94a3b8', width: 14, height: 14, }, }) } }) }) // DB Table Nodes if (showDb) { const dbTablesInUse = new Set() visibleSteps.forEach(s => s.dbTables.forEach(t => dbTablesInUse.add(t))) let dbIdx = 0 dbTablesInUse.forEach(table => { const nodeId = `db-${table}` nodes.push({ id: nodeId, type: 'default', position: { x: -280, y: STEP_Y_START + dbIdx * 90 }, data: { label: (
DB
{table}
), }, style: { background: '#f1f5f9', color: '#475569', border: '2px solid #94a3b8', borderRadius: '50%', width: '90px', height: '90px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '4px', }, }) // Connect steps to this DB table visibleSteps .filter(s => s.dbTables.includes(table)) .forEach(step => { edges.push({ id: `e-db-${table}-${step.id}`, source: nodeId, target: step.id, type: 'straight', style: { stroke: '#94a3b8', strokeWidth: 1, strokeDasharray: '6 3', }, labelStyle: { fontSize: 9, fill: '#94a3b8' }, label: step.dbMode !== 'none' ? step.dbMode : undefined, }) }) dbIdx++ }) } // RAG Collection Nodes if (showRag) { const ragInUse = new Set() visibleSteps.forEach(s => s.ragCollections.forEach(r => ragInUse.add(r))) let ragIdx = 0 ragInUse.forEach(collection => { const nodeId = `rag-${collection}` // Place RAG nodes to the right of all packages const rightX = (packageFilter === 'alle' ? 1280 : PACKAGE_X_OFFSET[packageFilter] || 0) + NODE_WIDTH + 180 nodes.push({ id: nodeId, type: 'default', position: { x: rightX, y: STEP_Y_START + ragIdx * 90 }, data: { label: (
RAG
{collection.replace('bp_', '')}
), }, style: { background: '#dcfce7', color: '#166534', border: '2px solid #22c55e', borderRadius: '50%', width: '90px', height: '90px', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '4px', }, }) // Connect steps to this RAG collection visibleSteps .filter(s => s.ragCollections.includes(collection)) .forEach(step => { edges.push({ id: `e-rag-${collection}-${step.id}`, source: nodeId, target: step.id, type: 'straight', style: { stroke: '#22c55e', strokeWidth: 1, strokeDasharray: '6 3', }, }) }) ragIdx++ }) } return { nodes, edges } }, [packageFilter, showDb, showRag, selectedStep]) // ========================================================================= // React Flow State // ========================================================================= const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) useEffect(() => { setNodes(initialNodes) setEdges(initialEdges) }, [initialNodes, initialEdges, setNodes, setEdges]) const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { const step = SDK_FLOW_STEPS.find(s => s.id === node.id) if (step) { setSelectedStep(prev => (prev?.id === step.id ? null : step)) } }, []) const onPaneClick = useCallback(() => { setSelectedStep(null) }, []) // ========================================================================= // Stats // ========================================================================= const stats = useMemo(() => { const visible = packageFilter === 'alle' ? SDK_FLOW_STEPS : SDK_FLOW_STEPS.filter(s => s.package === packageFilter) return { total: SDK_FLOW_STEPS.length, visible: visible.length, checkpoints: visible.filter(s => s.checkpointId).length, dbTables: allDbTables.length, ragCollections: allRagCollections.length, } }, [packageFilter, allDbTables, allRagCollections]) // ========================================================================= // Render // ========================================================================= return (
{/* Header */}
SDK

SDK Flow Visualization

{stats.total} Steps in 5 Packages | {stats.checkpoints} Checkpoints |{' '} {stats.dbTables} DB-Tabellen | {stats.ragCollections} RAG-Collections

{/* Toolbar */}
{/* Package Filter */} {PACKAGE_ORDER.map(pkgId => { const pkg = FLOW_PACKAGES[pkgId] const count = SDK_FLOW_STEPS.filter(s => s.package === pkgId).length return ( ) })} {/* Separator */}
{/* Toggles */}
{/* Flow Canvas + Detail Panel */}
{/* Canvas */}
{ if (node.id.startsWith('db-')) return '#94a3b8' if (node.id.startsWith('rag-')) return '#22c55e' const step = SDK_FLOW_STEPS.find(s => s.id === node.id) return step ? FLOW_PACKAGES[step.package].color.border : '#94a3b8' }} maskColor="rgba(0,0,0,0.08)" /> {/* Legende */}
Legende
{PACKAGE_ORDER.map(pkgId => { const pkg = FLOW_PACKAGES[pkgId] return (
{pkg.name}
) })}
DB-Tabelle
RAG-Collection
* = REQUIRED
~ = RECOMMENDED
--- = gestrichelte Border: Optional
{/* Package Headers */} {packageFilter === 'alle' && ( {PACKAGE_ORDER.map((pkgId) => { const pkg = FLOW_PACKAGES[pkgId] return (
{pkg.icon} {pkg.name}
) })}
)}
{/* Detail Panel */} {selectedStep && ( setSelectedStep(null)} /> )}
{/* Step Table (below flow) */}

Alle Steps ({stats.visible} {packageFilter !== 'alle' ? ` / ${stats.total}` : ''})

{SDK_FLOW_STEPS.filter( s => packageFilter === 'alle' || s.package === packageFilter ).map(step => { const pkg = FLOW_PACKAGES[step.package] return ( ) })}
) }