Files
breakpilot-lehrer/website/app/admin/screen-flow/page.tsx
Benjamin Admin 34da9f4cda [split-required] Split 700-870 LOC files across all services
backend-lehrer (11 files):
- llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6)
- messenger_api.py (840 → 5), print_generator.py (824 → 5)
- unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4)
- llm_gateway/routes/edu_search_seeds.py (710 → 4)

klausur-service (12 files):
- ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4)
- legal_corpus_api.py (790 → 4), page_crop.py (758 → 3)
- mail/ai_service.py (747 → 4), github_crawler.py (767 → 3)
- trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4)
- dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4)

website (6 pages):
- audit-checklist (867 → 8), content (806 → 6)
- screen-flow (790 → 4), scraper (789 → 5)
- zeugnisse (776 → 5), modules (745 → 4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:01:18 +02:00

228 lines
15 KiB
TypeScript

'use client'
/**
* Screen Flow Visualization
*
* Visualisiert alle Screens aus:
* - Studio (Port 8000): Lehrer-Oberflaeche
* - Admin (Port 3000): Admin Panel
*/
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 AdminLayout from '@/components/admin/AdminLayout'
import { ScreenDefinition, FlowType } from './_components/types'
import {
STUDIO_SCREENS, STUDIO_CONNECTIONS,
ADMIN_SCREENS, ADMIN_CONNECTIONS,
STUDIO_COLORS, ADMIN_COLORS,
STUDIO_LABELS, ADMIN_LABELS,
} from './_components/screen-data'
import { findConnectedNodes, constructEmbedUrl, getNodePosition } from './_components/flow-helpers'
export default function ScreenFlowPage() {
const [flowType, setFlowType] = useState<FlowType>('studio')
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [selectedNode, setSelectedNode] = useState<string | null>(null)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [previewScreen, setPreviewScreen] = useState<ScreenDefinition | null>(null)
const screens = flowType === 'studio' ? STUDIO_SCREENS : ADMIN_SCREENS
const connections = flowType === 'studio' ? STUDIO_CONNECTIONS : ADMIN_CONNECTIONS
const colors = flowType === 'studio' ? STUDIO_COLORS : ADMIN_COLORS
const labels = flowType === 'studio' ? STUDIO_LABELS : ADMIN_LABELS
const baseUrl = flowType === 'studio' ? 'http://macmini:8000' : 'http://macmini:3000'
const connectedNodes = useMemo(() => {
if (!selectedNode) return new Set<string>()
return findConnectedNodes(selectedNode, connections, 'children')
}, [selectedNode, connections])
const initialNodes = useMemo((): Node[] => {
return screens.map((screen) => {
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
const position = getNodePosition(screen.id, screen.category, screens, flowType)
let opacity = 1
if (selectedNode) {
opacity = connectedNodes.has(screen.id) ? 1 : 0.2
} else if (selectedCategory) {
opacity = screen.category === selectedCategory ? 1 : 0.2
}
const isSelected = selectedNode === screen.id
return {
id: screen.id, type: 'default', position,
data: { label: (<div className="text-center p-1"><div className="text-lg mb-1">{screen.icon}</div><div className="font-medium text-xs leading-tight">{screen.name}</div></div>) },
style: { background: isSelected ? catColors.border : catColors.bg, color: isSelected ? 'white' : catColors.text, border: `2px solid ${catColors.border}`, borderRadius: '12px', padding: '6px', minWidth: '110px', opacity, cursor: 'pointer', boxShadow: isSelected ? `0 0 20px ${catColors.border}` : 'none' },
}
})
}, [screens, colors, flowType, selectedCategory, selectedNode, connectedNodes])
const initialEdges = useMemo((): Edge[] => {
return connections.map((conn, index) => {
const isHighlighted = selectedNode && (conn.source === selectedNode || conn.target === selectedNode)
const isInSubtree = selectedNode && connectedNodes.has(conn.source) && connectedNodes.has(conn.target)
return {
id: `e-${conn.source}-${conn.target}-${index}`, source: conn.source, target: conn.target, label: conn.label, type: 'smoothstep', animated: isHighlighted || false,
style: { stroke: isHighlighted ? '#3b82f6' : (isInSubtree ? '#94a3b8' : '#e2e8f0'), strokeWidth: isHighlighted ? 3 : 1.5, opacity: selectedNode ? (isInSubtree ? 1 : 0.15) : 1 },
labelStyle: { fontSize: 9, fill: '#64748b' }, labelBgStyle: { fill: '#f8fafc' },
markerEnd: { type: MarkerType.ArrowClosed, color: isHighlighted ? '#3b82f6' : '#94a3b8', width: 15, height: 15 },
}
})
}, [connections, selectedNode, connectedNodes])
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
useEffect(() => { setNodes(initialNodes); setEdges(initialEdges) }, [initialNodes, initialEdges])
const handleFlowTypeChange = useCallback((newType: FlowType) => {
setFlowType(newType); setSelectedNode(null); setSelectedCategory(null); setPreviewUrl(null); setPreviewScreen(null)
}, [])
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const screen = screens.find(s => s.id === node.id)
if (selectedNode === node.id) { if (screen?.url) window.open(`${baseUrl}${screen.url}`, '_blank'); return }
setSelectedNode(node.id); setSelectedCategory(null)
if (screen?.url) { setPreviewUrl(constructEmbedUrl(baseUrl, screen.url)); setPreviewScreen(screen) }
}, [screens, baseUrl, selectedNode])
const onPaneClick = useCallback(() => { setSelectedNode(null); setPreviewUrl(null); setPreviewScreen(null) }, [])
const closePreview = useCallback(() => { setPreviewUrl(null); setPreviewScreen(null); setSelectedNode(null) }, [])
const categories = Object.keys(labels)
const connectedScreens = selectedNode ? screens.filter(s => connectedNodes.has(s.id)) : []
return (
<AdminLayout title="Screen Flow" description="Visualisierung aller UI-Screens und ihrer Verbindungen">
{/* Flow Type Selector */}
<div className="grid grid-cols-2 gap-4 mb-6">
{(['studio', 'admin'] as const).map(type => {
const isActive = flowType === type
const cfg = type === 'studio'
? { icon: '🎓', label: 'Studio (Port 8000)', sub: 'Lehrer-Oberflaeche', count: STUDIO_SCREENS.length, activeColor: 'border-green-500 bg-green-50', iconBg: 'bg-green-500 text-white' }
: { icon: '⚙️', label: 'Admin (Port 3000)', sub: 'Admin Panel', count: ADMIN_SCREENS.length, activeColor: 'border-purple-500 bg-purple-50', iconBg: 'bg-purple-500 text-white' }
return (
<button key={type} onClick={() => handleFlowTypeChange(type)} className={`p-6 rounded-xl border-2 transition-all ${isActive ? `${cfg.activeColor} shadow-lg` : 'border-slate-200 bg-white hover:border-slate-300'}`}>
<div className="flex items-center gap-4">
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${isActive ? cfg.iconBg : 'bg-slate-100'}`}>{cfg.icon}</div>
<div className="text-left">
<div className="font-bold text-lg">{cfg.label}</div>
<div className="text-sm text-slate-500">{cfg.sub}</div>
<div className="text-xs text-slate-400 mt-1">{cfg.count} Screens</div>
</div>
</div>
</button>
)
})}
</div>
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-lg shadow p-4"><div className="text-3xl font-bold text-slate-800">{screens.length}</div><div className="text-sm text-slate-500">Screens</div></div>
<div className="bg-white rounded-lg shadow p-4"><div className="text-3xl font-bold text-blue-600">{connections.length}</div><div className="text-sm text-slate-500">Verbindungen</div></div>
<div className="bg-white rounded-lg shadow p-4 col-span-2">
{selectedNode ? (
<div className="flex items-center gap-3">
<div className="text-3xl">{previewScreen?.icon}</div>
<div><div className="font-bold text-slate-800">{previewScreen?.name}</div><div className="text-sm text-slate-500">{connectedNodes.size} verbundene Screen{connectedNodes.size !== 1 ? 's' : ''}</div></div>
<button onClick={closePreview} className="ml-auto px-3 py-1 text-sm bg-slate-100 hover:bg-slate-200 rounded-lg">Zuruecksetzen</button>
</div>
) : (<div className="text-slate-500 text-sm">Klicke auf einen Screen um den Subtree und die Vorschau zu sehen</div>)}
</div>
</div>
{/* Category Filter */}
<div className="bg-white rounded-lg shadow p-4 mb-6">
<div className="flex flex-wrap gap-2">
<button onClick={() => { setSelectedCategory(null); setSelectedNode(null); setPreviewUrl(null); setPreviewScreen(null) }} className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${selectedCategory === null && !selectedNode ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}>Alle ({screens.length})</button>
{categories.map((key) => {
const count = screens.filter(s => s.category === key).length
const catColors = colors[key] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
return (
<button key={key} onClick={() => { setSelectedCategory(selectedCategory === key ? null : key); setSelectedNode(null); setPreviewUrl(null); setPreviewScreen(null) }} className="px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2" style={{ background: selectedCategory === key ? catColors.border : catColors.bg, color: selectedCategory === key ? 'white' : catColors.text }}>
<span className="w-3 h-3 rounded-full" style={{ background: catColors.border }} />{labels[key]} ({count})
</button>
)
})}
</div>
</div>
{/* Connected Screens List */}
{selectedNode && connectedScreens.length > 1 && (
<div className="bg-white rounded-lg shadow p-4 mb-6">
<div className="text-sm font-medium text-slate-700 mb-3">Verbundene Screens:</div>
<div className="flex flex-wrap gap-2">
{connectedScreens.map((screen) => {
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
const isCurrentNode = screen.id === selectedNode
return (
<button key={screen.id} onClick={() => { setPreviewUrl(constructEmbedUrl(baseUrl, screen.url)); setPreviewScreen(screen) }} className={`px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${previewScreen?.id === screen.id ? 'ring-2 ring-blue-500' : ''}`} style={{ background: isCurrentNode ? catColors.border : catColors.bg, color: isCurrentNode ? 'white' : catColors.text }}>
<span>{screen.icon}</span>{screen.name}
</button>
)
})}
</div>
</div>
)}
{/* Flow Diagram */}
<div className="bg-white rounded-lg shadow overflow-hidden" style={{ height: previewUrl ? '350px' : '500px' }}>
<ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} onNodeClick={onNodeClick} onPaneClick={onPaneClick} fitView fitViewOptions={{ padding: 0.2 }} attributionPosition="bottom-left">
<Controls /><MiniMap nodeColor={(node) => { const s = screens.find(s => s.id === node.id); return s ? (colors[s.category]?.border || '#94a3b8') : '#94a3b8' }} maskColor="rgba(0, 0, 0, 0.1)" /><Background variant={BackgroundVariant.Dots} gap={12} size={1} />
<Panel position="top-left" className="bg-white/95 p-3 rounded-lg shadow-lg text-xs">
<div className="font-medium text-slate-700 mb-2">{flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin'}</div>
<div className="space-y-1">{categories.slice(0, 4).map((key) => { const catColors = colors[key] || { bg: '#f1f5f9', border: '#94a3b8' }; return (<div key={key} className="flex items-center gap-2"><span className="w-3 h-3 rounded" style={{ background: catColors.bg, border: `1px solid ${catColors.border}` }} /><span className="text-slate-600">{labels[key]}</span></div>) })}</div>
<div className="mt-2 pt-2 border-t text-slate-400">Klick = Subtree + Preview<br/>Doppelklick = Oeffnen</div>
</Panel>
</ReactFlow>
</div>
{/* Iframe Preview */}
{previewUrl && (
<div className="mt-6 bg-white rounded-lg shadow overflow-hidden">
<div className="px-4 py-3 bg-slate-50 border-b flex items-center justify-between">
<div className="flex items-center gap-3"><span className="text-xl">{previewScreen?.icon}</span><div><h3 className="font-medium text-slate-700">{previewScreen?.name}</h3><p className="text-xs text-slate-500">{previewScreen?.description}</p></div></div>
<div className="flex items-center gap-2">
<span className="text-xs text-slate-400 font-mono">{previewScreen?.url}</span>
<a href={`${baseUrl}${previewScreen?.url}`} target="_blank" rel="noopener noreferrer" className="px-3 py-1 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center gap-1"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>Oeffnen</a>
<button onClick={closePreview} className="px-3 py-1 text-sm bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 flex items-center gap-1"><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>Schliessen</button>
</div>
</div>
<div className="relative" style={{ height: '600px' }}><iframe src={previewUrl} className="w-full h-full border-0" title={`Preview: ${previewScreen?.name}`} /></div>
</div>
)}
{/* Screen List */}
{!previewUrl && (
<div className="mt-6 bg-white rounded-lg shadow overflow-hidden">
<div className="px-4 py-3 bg-slate-50 border-b flex items-center justify-between"><h3 className="font-medium text-slate-700">Alle Screens ({screens.length})</h3><span className="text-xs text-slate-400">{baseUrl}</span></div>
<div className="divide-y max-h-80 overflow-y-auto">
{screens.filter(s => !selectedCategory || s.category === selectedCategory).map((screen) => {
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
return (
<button key={screen.id} onClick={() => { setSelectedNode(screen.id); setSelectedCategory(null); setPreviewUrl(constructEmbedUrl(baseUrl, screen.url)); setPreviewScreen(screen) }} className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 transition-colors text-left">
<span className="w-9 h-9 rounded-lg flex items-center justify-center text-lg" style={{ background: catColors.bg }}>{screen.icon}</span>
<div className="flex-1 min-w-0"><div className="font-medium text-slate-800 text-sm">{screen.name}</div><div className="text-xs text-slate-500 truncate">{screen.description}</div></div>
<span className="px-2 py-1 rounded text-xs font-medium shrink-0" style={{ background: catColors.bg, color: catColors.text }}>{labels[screen.category]}</span>
</button>
)
})}
</div>
</div>
)}
</AdminLayout>
)
}