Files
breakpilot-compliance/admin-compliance/app/(admin)/development/sdk-flow/page.tsx
Benjamin Admin d9f819e5be
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 27s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 16s
docs+tests: Phase 2 RAG audit — missing tests, dev docs, SDK flow page
- Add rag-query.test.ts (7 Jest tests for shared queryRAG utility)
- Add test_routes_legal_context.py (3 tests for ?include_legal_context param)
- Update ARCHITECTURE.md with multi-collection RAG section (3.3)
- Update DEVELOPER.md with RAG usage examples, collection table, error tolerance
- Add SDK flow page with updated requirements + DSFA RAG descriptions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 09:26:57 +01:00

848 lines
28 KiB
TypeScript

'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<string, number> = {
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 (
<div className="w-80 bg-white border-l border-slate-200 overflow-y-auto">
<div className="sticky top-0 bg-white z-10 border-b border-slate-200">
<div className="flex items-center justify-between p-4">
<h3 className="font-bold text-slate-900">{step.name}</h3>
<button
onClick={onClose}
className="text-slate-400 hover:text-slate-600 text-lg leading-none"
>
x
</button>
</div>
<div className="px-4 pb-3 flex items-center gap-2">
<span
className="px-2 py-0.5 rounded text-xs font-medium"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{pkg.icon} {pkg.name}
</span>
<span className="text-xs text-slate-400">seq {step.seq}</span>
{step.isOptional && (
<span className="px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-500">
Optional
</span>
)}
</div>
</div>
<div className="p-4 space-y-4">
{/* Beschreibung */}
<div>
<p className="text-sm text-slate-700 leading-relaxed">{step.description}</p>
<p className="text-xs text-slate-500 leading-relaxed mt-2">{step.descriptionLong}</p>
{step.legalBasis && (
<div className="mt-2 px-2 py-1.5 bg-blue-50 rounded text-xs text-blue-700">
<span className="font-medium">Rechtsgrundlage:</span> {step.legalBasis}
</div>
)}
</div>
{/* Inputs */}
{step.inputs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Inputs (SDKState)
</h4>
<div className="space-y-1">
{step.inputs.map(input => {
const producer = findProducerStep(input)
return (
<div
key={input}
className="flex items-center justify-between text-sm bg-blue-50 rounded px-2 py-1"
>
<code className="text-blue-700 text-xs">{input}</code>
{producer && (
<span className="text-xs text-slate-400">
{producer.nameShort}
</span>
)}
</div>
)
})}
</div>
</div>
)}
{/* Outputs */}
{step.outputs.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Outputs (SDKState)
</h4>
<div className="space-y-1">
{step.outputs.map(output => (
<div
key={output}
className="text-sm bg-emerald-50 rounded px-2 py-1"
>
<code className="text-emerald-700 text-xs">{output}</code>
</div>
))}
</div>
</div>
)}
{/* DB Tables */}
{step.dbTables.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
DB-Tabellen
</h4>
<div className="space-y-1">
{step.dbTables.map(table => (
<div
key={table}
className="flex items-center justify-between text-sm bg-slate-100 rounded px-2 py-1"
>
<code className="text-slate-700 text-xs">{table}</code>
<span className="text-xs text-slate-400">{step.dbMode}</span>
</div>
))}
</div>
</div>
)}
{/* RAG Collections */}
{step.ragCollections.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
RAG-Collections
</h4>
<div className="space-y-1">
{step.ragCollections.map(rag => (
<div
key={rag}
className="text-sm bg-green-50 rounded px-2 py-1"
>
<code className="text-green-700 text-xs">{rag}</code>
</div>
))}
{step.ragPurpose && (
<p className="text-xs text-slate-500 mt-1">{step.ragPurpose}</p>
)}
</div>
</div>
)}
{/* Checkpoint */}
{step.checkpointId && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Checkpoint
</h4>
<div className="bg-amber-50 rounded p-2 space-y-1">
<div className="flex items-center gap-2">
<code className="text-xs font-bold text-amber-800">
{step.checkpointId}
</code>
<span
className={`px-1.5 py-0.5 rounded text-xs font-medium ${
step.checkpointType === 'REQUIRED'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
}`}
>
{step.checkpointType}
</span>
</div>
{step.checkpointReviewer && step.checkpointReviewer !== 'NONE' && (
<div className="text-xs text-slate-600">
Reviewer: <span className="font-medium">{step.checkpointReviewer}</span>
</div>
)}
</div>
</div>
)}
{/* Generated Docs */}
{step.generates && step.generates.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Generierte Dokumente
</h4>
<div className="space-y-1">
{step.generates.map(doc => (
<div
key={doc}
className="text-sm bg-violet-50 rounded px-2 py-1 text-violet-700"
>
{doc}
</div>
))}
</div>
</div>
)}
{/* Prerequisites */}
{step.prerequisiteSteps.length > 0 && (
<div>
<h4 className="text-xs font-semibold text-slate-500 uppercase mb-1.5">
Voraussetzungen
</h4>
<div className="space-y-1">
{step.prerequisiteSteps.map(preId => {
const preStep = SDK_FLOW_STEPS.find(s => s.id === preId)
return (
<div
key={preId}
className="text-sm text-slate-600 bg-slate-50 rounded px-2 py-1"
>
{preStep?.name || preId}
</div>
)
})}
</div>
</div>
)}
{/* Open in SDK */}
<button
onClick={() => window.open(`${baseUrl}${step.url}`, '_blank')}
className="w-full mt-2 px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 transition-colors"
>
Im SDK oeffnen
</button>
</div>
</div>
)
}
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export default function SDKFlowPage() {
const [selectedStep, setSelectedStep] = useState<SDKFlowStep | null>(null)
const [packageFilter, setPackageFilter] = useState<PackageFilter>('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: (
<div className="text-center px-1">
<div className="font-medium text-xs leading-tight">
{step.nameShort}
</div>
<div className="text-[10px] opacity-70 mt-0.5">
{step.checkpointId || ''}
{badge}
</div>
</div>
),
},
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<string>()
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: (
<div className="text-center">
<div className="text-sm mb-0.5">DB</div>
<div className="font-medium text-[10px] leading-tight">{table}</div>
</div>
),
},
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<string>()
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: (
<div className="text-center">
<div className="text-sm mb-0.5">RAG</div>
<div className="font-medium text-[10px] leading-tight">
{collection.replace('bp_', '')}
</div>
</div>
),
},
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 (
<div className="space-y-4">
{/* Header */}
<div className="bg-white rounded-xl border border-slate-200 p-5 shadow-sm">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-purple-600 flex items-center justify-center text-xl text-white">
SDK
</div>
<div>
<h2 className="text-xl font-bold text-slate-900">
SDK Flow Visualization
</h2>
<p className="text-sm text-slate-500">
{stats.total} Steps in 5 Packages | {stats.checkpoints} Checkpoints |{' '}
{stats.dbTables} DB-Tabellen | {stats.ragCollections} RAG-Collections
</p>
</div>
</div>
</div>
{/* Toolbar */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex flex-wrap items-center gap-2">
{/* Package Filter */}
<button
onClick={() => {
setPackageFilter('alle')
setSelectedStep(null)
}}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
packageFilter === 'alle'
? 'bg-slate-800 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
>
Alle ({SDK_FLOW_STEPS.length})
</button>
{PACKAGE_ORDER.map(pkgId => {
const pkg = FLOW_PACKAGES[pkgId]
const count = SDK_FLOW_STEPS.filter(s => s.package === pkgId).length
return (
<button
key={pkgId}
onClick={() => {
setPackageFilter(packageFilter === pkgId ? 'alle' : pkgId)
setSelectedStep(null)
}}
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-all flex items-center gap-1.5"
style={{
background:
packageFilter === pkgId ? pkg.color.border : pkg.color.bg,
color: packageFilter === pkgId ? 'white' : pkg.color.text,
}}
>
<span
className="w-2.5 h-2.5 rounded-full"
style={{ background: pkg.color.border }}
/>
{pkg.nameShort} ({count})
</button>
)
})}
{/* Separator */}
<div className="w-px h-6 bg-slate-200 mx-1" />
{/* Toggles */}
<button
onClick={() => setShowDb(!showDb)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
showDb
? 'bg-slate-700 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
DB-Tabellen
</button>
<button
onClick={() => setShowRag(!showRag)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
showRag
? 'bg-green-600 text-white'
: 'bg-green-50 text-green-700 hover:bg-green-100'
}`}
>
RAG-Collections
</button>
</div>
</div>
{/* Flow Canvas + Detail Panel */}
<div className="flex bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '700px' }}>
{/* Canvas */}
<div className="flex-1 relative">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
fitView
fitViewOptions={{ padding: 0.15 }}
attributionPosition="bottom-left"
>
<Controls />
<MiniMap
nodeColor={node => {
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)"
/>
<Background variant={BackgroundVariant.Dots} gap={16} size={1} />
{/* Legende */}
<Panel
position="bottom-right"
className="bg-white/95 p-3 rounded-lg shadow-lg text-xs"
>
<div className="font-medium text-slate-700 mb-2">Legende</div>
<div className="space-y-1">
{PACKAGE_ORDER.map(pkgId => {
const pkg = FLOW_PACKAGES[pkgId]
return (
<div key={pkgId} className="flex items-center gap-2">
<span
className="w-3 h-3 rounded"
style={{
background: pkg.color.bg,
border: `1px solid ${pkg.color.border}`,
}}
/>
<span className="text-slate-600">{pkg.name}</span>
</div>
)
})}
<div className="border-t border-slate-200 my-1.5 pt-1.5">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-slate-200 border border-slate-400" />
<span className="text-slate-500">DB-Tabelle</span>
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="w-3 h-3 rounded-full bg-green-100 border border-green-500" />
<span className="text-slate-500">RAG-Collection</span>
</div>
</div>
<div className="border-t border-slate-200 my-1.5 pt-1.5 text-slate-400">
<div>* = REQUIRED</div>
<div>~ = RECOMMENDED</div>
<div>--- = gestrichelte Border: Optional</div>
</div>
</div>
</Panel>
{/* Package Headers */}
{packageFilter === 'alle' && (
<Panel position="top-center" className="flex gap-2 pointer-events-none">
{PACKAGE_ORDER.map((pkgId) => {
const pkg = FLOW_PACKAGES[pkgId]
return (
<div
key={pkgId}
className="px-3 py-1 rounded-lg text-xs font-medium opacity-60"
style={{
background: pkg.color.bg,
color: pkg.color.text,
border: `1px solid ${pkg.color.border}`,
minWidth: '200px',
textAlign: 'center',
}}
>
{pkg.icon} {pkg.name}
</div>
)
})}
</Panel>
)}
</ReactFlow>
</div>
{/* Detail Panel */}
{selectedStep && (
<DetailPanel
step={selectedStep}
onClose={() => setSelectedStep(null)}
/>
)}
</div>
{/* Step Table (below flow) */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="px-4 py-3 bg-slate-50 border-b">
<h3 className="font-medium text-slate-700">
Alle Steps ({stats.visible}
{packageFilter !== 'alle' ? ` / ${stats.total}` : ''})
</h3>
</div>
<div className="divide-y max-h-96 overflow-y-auto">
{SDK_FLOW_STEPS.filter(
s => packageFilter === 'alle' || s.package === packageFilter
).map(step => {
const pkg = FLOW_PACKAGES[step.package]
return (
<button
key={step.id}
onClick={() => setSelectedStep(step)}
className={`w-full flex items-center gap-3 p-3 text-left transition-colors ${
selectedStep?.id === step.id
? 'bg-purple-50'
: 'hover:bg-slate-50'
}`}
>
<span
className="w-8 h-8 rounded-lg flex items-center justify-center text-xs font-bold shrink-0"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{step.seq}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium text-slate-800 text-sm">
{step.name}
</div>
<div className="text-xs text-slate-500 flex items-center gap-2 mt-0.5">
{step.checkpointId && (
<span
className={`px-1 py-0 rounded text-[10px] font-medium ${
step.checkpointType === 'REQUIRED'
? 'bg-red-100 text-red-600'
: 'bg-yellow-100 text-yellow-600'
}`}
>
{step.checkpointId}
</span>
)}
{step.dbTables.length > 0 && (
<span className="text-slate-400">
DB: {step.dbTables.join(', ')}
</span>
)}
{step.ragCollections.length > 0 && (
<span className="text-green-600">
RAG: {step.ragCollections.length}
</span>
)}
{step.isOptional && (
<span className="text-slate-400 italic">optional</span>
)}
</div>
</div>
<span
className="px-2 py-0.5 rounded text-[10px] font-medium shrink-0"
style={{ background: pkg.color.bg, color: pkg.color.text }}
>
{pkg.nameShort}
</span>
</button>
)
})}
</div>
</div>
</div>
)
}