Files
breakpilot-lehrer/website/app/admin/screen-flow/page.tsx
Benjamin Admin b9c3c47a37 refactor: LLM Compare komplett entfernt, Video/Voice/Alerts Sidebar hinzugefuegt
- LLM Compare Seiten, Configs und alle Referenzen geloescht
- Kommunikation-Kategorie in Sidebar mit Video & Chat, Voice Service, Alerts
- Compliance SDK Kategorie aus Sidebar entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:34:54 +01:00

791 lines
35 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* Screen Flow Visualization
*
* Visualisiert alle Screens aus:
* - Studio (Port 8000): Lehrer-Oberfläche
* - Admin (Port 3000): Admin Panel
*/
import { useCallback, useState, useMemo, useEffect } from 'react'
import ReactFlow, {
Node,
Edge,
Controls,
Background,
MiniMap,
useNodesState,
useEdgesState,
Connection,
BackgroundVariant,
MarkerType,
Panel,
} from 'reactflow'
import 'reactflow/dist/style.css'
import AdminLayout from '@/components/admin/AdminLayout'
// ============================================
// TYPES
// ============================================
interface ScreenDefinition {
id: string
name: string
description: string
category: string
icon: string
url?: string
}
interface ConnectionDef {
source: string
target: string
label?: string
}
type FlowType = 'studio' | 'admin'
// ============================================
// STUDIO SCREENS (Port 8000)
// ============================================
const STUDIO_SCREENS: ScreenDefinition[] = [
{ id: 'lehrer-dashboard', name: 'Mein Dashboard', description: 'Hauptübersicht mit Widgets', category: 'navigation', icon: '🏠', url: '/app#lehrer-dashboard' },
{ id: 'lehrer-onboarding', name: 'Erste Schritte', description: 'Onboarding & Schnellstart', category: 'navigation', icon: '🚀', url: '/app#lehrer-onboarding' },
{ id: 'hilfe', name: 'Dokumentation', description: 'Hilfe & Anleitungen', category: 'navigation', icon: '📚', url: '/app#hilfe' },
{ id: 'worksheets', name: 'Arbeitsblätter Studio', description: 'Lernmaterialien erstellen', category: 'content', icon: '📝', url: '/app#worksheets' },
{ id: 'content-creator', name: 'Content Creator', description: 'Inhalte erstellen', category: 'content', icon: '✨', url: '/app#content-creator' },
{ id: 'content-feed', name: 'Content Feed', description: 'Inhalte durchsuchen', category: 'content', icon: '📰', url: '/app#content-feed' },
{ id: 'unit-creator', name: 'Unit Creator', description: 'Lerneinheiten erstellen', category: 'content', icon: '📦', url: '/app#unit-creator' },
{ id: 'letters', name: 'Briefe & Vorlagen', description: 'Brief-Generator', category: 'content', icon: '✉️', url: '/app#letters' },
{ id: 'correction', name: 'Korrektur', description: 'Arbeiten korrigieren', category: 'content', icon: '✏️', url: '/app#correction' },
{ id: 'klausur-korrektur', name: 'Abiturklausuren', description: 'KI-gestützte Klausurkorrektur', category: 'content', icon: '📋', url: '/app#klausur-korrektur' },
{ id: 'jitsi', name: 'Videokonferenz', description: 'Jitsi Meet Integration', category: 'communication', icon: '🎥', url: '/app#jitsi' },
{ id: 'messenger', name: 'Messenger', description: 'Matrix E2EE Chat', category: 'communication', icon: '💬', url: '/app#messenger' },
{ id: 'mail', name: 'Unified Inbox', description: 'E-Mail Verwaltung', category: 'communication', icon: '📧', url: '/app#mail' },
{ id: 'school-classes', name: 'Klassen', description: 'Klassenverwaltung', category: 'school', icon: '👥', url: '/app#school-classes' },
{ id: 'school-exams', name: 'Prüfungen', description: 'Prüfungsverwaltung', category: 'school', icon: '📝', url: '/app#school-exams' },
{ id: 'school-grades', name: 'Noten', description: 'Notenverwaltung', category: 'school', icon: '📊', url: '/app#school-grades' },
{ id: 'school-gradebook', name: 'Notenbuch', description: 'Digitales Notenbuch', category: 'school', icon: '📖', url: '/app#school-gradebook' },
{ id: 'school-certificates', name: 'Zeugnisse', description: 'Zeugniserstellung', category: 'school', icon: '🎓', url: '/app#school-certificates' },
{ id: 'companion', name: 'Begleiter & Stunde', description: 'KI-Unterrichtsassistent', category: 'ai', icon: '🤖', url: '/app#companion' },
{ id: 'alerts', name: 'Alerts', description: 'News & Benachrichtigungen', category: 'ai', icon: '🔔', url: '/app#alerts' },
{ id: 'admin', name: 'Einstellungen', description: 'Systemeinstellungen', category: 'admin', icon: '⚙️', url: '/app#admin' },
{ id: 'rbac-admin', name: 'Rollen & Rechte', description: 'Berechtigungsverwaltung', category: 'admin', icon: '🔐', url: '/app#rbac-admin' },
{ id: 'abitur-docs-admin', name: 'Abitur Dokumente', description: 'Erwartungshorizonte', category: 'admin', icon: '📄', url: '/app#abitur-docs-admin' },
{ id: 'system-info', name: 'System Info', description: 'Systeminformationen', category: 'admin', icon: '💻', url: '/app#system-info' },
{ id: 'workflow', name: 'Workflow', description: 'Automatisierungen', category: 'admin', icon: '⚡', url: '/app#workflow' },
]
const STUDIO_CONNECTIONS: ConnectionDef[] = [
{ source: 'lehrer-onboarding', target: 'worksheets', label: 'Arbeitsblätter' },
{ source: 'lehrer-onboarding', target: 'klausur-korrektur', label: 'Abiturklausuren' },
{ source: 'lehrer-onboarding', target: 'correction', label: 'Korrektur' },
{ source: 'lehrer-onboarding', target: 'letters', label: 'Briefe' },
{ source: 'lehrer-onboarding', target: 'school-classes', label: 'Klassen' },
{ source: 'lehrer-onboarding', target: 'jitsi', label: 'Meet' },
{ source: 'lehrer-onboarding', target: 'hilfe', label: 'Doku' },
{ source: 'lehrer-onboarding', target: 'admin', label: 'Settings' },
{ source: 'lehrer-dashboard', target: 'worksheets' },
{ source: 'lehrer-dashboard', target: 'correction' },
{ source: 'lehrer-dashboard', target: 'jitsi' },
{ source: 'lehrer-dashboard', target: 'letters' },
{ source: 'lehrer-dashboard', target: 'messenger' },
{ source: 'lehrer-dashboard', target: 'klausur-korrektur' },
{ source: 'lehrer-dashboard', target: 'companion' },
{ source: 'lehrer-dashboard', target: 'alerts' },
{ source: 'lehrer-dashboard', target: 'mail' },
{ source: 'lehrer-dashboard', target: 'school-classes' },
{ source: 'lehrer-dashboard', target: 'lehrer-onboarding', label: 'Sidebar' },
{ source: 'school-classes', target: 'school-exams' },
{ source: 'school-classes', target: 'school-grades' },
{ source: 'school-grades', target: 'school-gradebook' },
{ source: 'school-gradebook', target: 'school-certificates' },
{ source: 'worksheets', target: 'content-creator' },
{ source: 'worksheets', target: 'unit-creator' },
{ source: 'content-creator', target: 'content-feed' },
{ source: 'klausur-korrektur', target: 'abitur-docs-admin' },
{ source: 'admin', target: 'rbac-admin' },
{ source: 'admin', target: 'system-info' },
{ source: 'admin', target: 'workflow' },
]
// ============================================
// ADMIN SCREENS (Port 3000)
// ============================================
const ADMIN_SCREENS: ScreenDefinition[] = [
{ id: 'admin-dashboard', name: 'Dashboard', description: 'Übersicht & Statistiken', category: 'overview', icon: '🏠', url: '/admin' },
{ id: 'admin-onboarding', name: 'Onboarding', description: 'Lern-Wizards für alle Module', category: 'overview', icon: '📖', url: '/admin/onboarding' },
{ id: 'admin-gpu', name: 'GPU Infrastruktur', description: 'vast.ai GPU Management', category: 'infrastructure', icon: '🖥️', url: '/admin/gpu' },
{ id: 'admin-middleware', name: 'Middleware', description: 'Middleware Stack & Test', category: 'infrastructure', icon: '🔧', url: '/admin/middleware' },
{ id: 'admin-mac-mini', name: 'Mac Mini', description: 'Headless Mac Mini Control', category: 'infrastructure', icon: '🍎', url: '/admin/mac-mini' },
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente', category: 'compliance', icon: '📄', url: '/admin/consent' },
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'compliance', icon: '🔒', url: '/admin/dsr' },
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management', category: 'compliance', icon: '🛡️', url: '/admin/dsms' },
{ id: 'admin-compliance', name: 'Compliance', description: 'GRC & Audit', category: 'compliance', icon: '✅', url: '/admin/compliance' },
{ id: 'admin-docs-audit', name: 'DSGVO-Audit', description: 'Audit-Dokumentation', category: 'compliance', icon: '📋', url: '/admin/docs/audit' },
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/admin/rag' },
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '🏷️', url: '/admin/ocr-labeling' },
{ id: 'admin-magic-help', name: 'Magic Help (TrOCR)', description: 'Handschrift-OCR', category: 'ai', icon: '✨', url: '/admin/magic-help' },
{ id: 'admin-companion', name: 'Companion Dev', description: 'Lesson-Modus Entwicklung', category: 'ai', icon: '📚', url: '/admin/companion' },
{ id: 'admin-communication', name: 'Kommunikation', description: 'Matrix & Jitsi Monitoring', category: 'communication', icon: '💬', url: '/admin/communication' },
{ id: 'admin-alerts', name: 'Alerts Monitoring', description: 'Google Alerts & Feeds', category: 'communication', icon: '🔔', url: '/admin/alerts' },
{ id: 'admin-mail', name: 'Unified Inbox', description: 'E-Mail & KI-Analyse', category: 'communication', icon: '📧', url: '/admin/mail' },
{ id: 'admin-security', name: 'Security', description: 'DevSecOps Dashboard', category: 'security', icon: '🔐', url: '/admin/security' },
{ id: 'admin-sbom', name: 'SBOM', description: 'Software Bill of Materials', category: 'security', icon: '📦', url: '/admin/sbom' },
{ id: 'admin-screen-flow', name: 'Screen Flow', description: 'UI Verbindungen', category: 'security', icon: '🔀', url: '/admin/screen-flow' },
{ id: 'admin-content', name: 'Übersetzungen', description: 'Website Content', category: 'content', icon: '🌍', url: '/admin/content' },
{ id: 'admin-edu-search', name: 'Education Search', description: 'Bildungsquellen & Crawler', category: 'content', icon: '🔍', url: '/admin/edu-search' },
{ id: 'admin-staff-search', name: 'Personensuche', description: 'Uni-Mitarbeiter', category: 'content', icon: '👤', url: '/admin/staff-search' },
{ id: 'admin-uni-crawler', name: 'Uni-Crawler', description: 'Universitäts-Crawling', category: 'content', icon: '🕷️', url: '/admin/uni-crawler' },
{ id: 'admin-game', name: 'Breakpilot Drive', description: 'Lernspiel Klasse 2-6', category: 'game', icon: '🎮', url: '/admin/game' },
{ id: 'admin-unity-bridge', name: 'Unity Bridge', description: 'Unity Editor Steuerung', category: 'game', icon: '⚡', url: '/admin/unity-bridge' },
{ id: 'admin-backlog', name: 'Production Backlog', description: 'Go-Live Checkliste', category: 'misc', icon: '📝', url: '/admin/backlog' },
{ id: 'admin-brandbook', name: 'Brandbook', description: 'Corporate Design', category: 'misc', icon: '🎨', url: '/admin/brandbook' },
{ id: 'admin-docs', name: 'Developer Docs', description: 'API & Architektur', category: 'misc', icon: '📖', url: '/admin/docs' },
{ id: 'admin-pca-platform', name: 'PCA Platform', description: 'Bot-Erkennung', category: 'misc', icon: '💰', url: '/admin/pca-platform' },
]
const ADMIN_CONNECTIONS: ConnectionDef[] = [
{ source: 'admin-dashboard', target: 'admin-onboarding' },
{ source: 'admin-dashboard', target: 'admin-security' },
{ source: 'admin-dashboard', target: 'admin-compliance' },
{ source: 'admin-onboarding', target: 'admin-gpu' },
{ source: 'admin-onboarding', target: 'admin-consent' },
{ source: 'admin-consent', target: 'admin-dsr' },
{ source: 'admin-dsr', target: 'admin-dsms' },
{ source: 'admin-dsms', target: 'admin-compliance' },
{ source: 'admin-compliance', target: 'admin-docs-audit' },
{ source: 'admin-rag', target: 'admin-ocr-labeling' },
{ source: 'admin-ocr-labeling', target: 'admin-magic-help' },
{ source: 'admin-magic-help', target: 'admin-companion' },
{ source: 'admin-security', target: 'admin-sbom' },
{ source: 'admin-sbom', target: 'admin-screen-flow' },
{ source: 'admin-communication', target: 'admin-alerts' },
{ source: 'admin-alerts', target: 'admin-mail' },
{ source: 'admin-gpu', target: 'admin-middleware' },
{ source: 'admin-middleware', target: 'admin-mac-mini' },
{ source: 'admin-game', target: 'admin-unity-bridge' },
{ source: 'admin-edu-search', target: 'admin-staff-search' },
{ source: 'admin-staff-search', target: 'admin-uni-crawler' },
]
// ============================================
// CATEGORY COLORS
// ============================================
const STUDIO_COLORS: Record<string, { bg: string; border: string; text: string }> = {
navigation: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
content: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
communication: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
school: { bg: '#fce7f3', border: '#ec4899', text: '#9d174d' },
admin: { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
ai: { bg: '#cffafe', border: '#06b6d4', text: '#0e7490' },
}
const ADMIN_COLORS: Record<string, { bg: string; border: string; text: string }> = {
overview: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
infrastructure: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
compliance: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
ai: { bg: '#cffafe', border: '#06b6d4', text: '#0e7490' },
communication: { bg: '#fce7f3', border: '#ec4899', text: '#9d174d' },
security: { bg: '#fee2e2', border: '#ef4444', text: '#991b1b' },
content: { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
game: { bg: '#fef9c3', border: '#eab308', text: '#713f12' },
misc: { bg: '#f1f5f9', border: '#64748b', text: '#334155' },
}
const STUDIO_LABELS: Record<string, string> = {
navigation: 'Navigation',
content: 'Content & Tools',
communication: 'Kommunikation',
school: 'Schulverwaltung',
admin: 'Administration',
ai: 'KI & Assistent',
}
const ADMIN_LABELS: Record<string, string> = {
overview: 'Übersicht',
infrastructure: 'Infrastruktur',
compliance: 'DSGVO & Compliance',
ai: 'KI & LLM',
communication: 'Kommunikation',
security: 'Security & DevOps',
content: 'Content & Suche',
game: 'Game & Unity',
misc: 'Sonstiges',
}
// ============================================
// HELPER: Find all connected nodes (recursive)
// ============================================
function findConnectedNodes(
startNodeId: string,
connections: ConnectionDef[],
direction: 'children' | 'parents' | 'both' = 'children'
): Set<string> {
const connected = new Set<string>()
connected.add(startNodeId)
const queue = [startNodeId]
while (queue.length > 0) {
const current = queue.shift()!
connections.forEach(conn => {
if ((direction === 'children' || direction === 'both') && conn.source === current) {
if (!connected.has(conn.target)) {
connected.add(conn.target)
queue.push(conn.target)
}
}
if ((direction === 'parents' || direction === 'both') && conn.target === current) {
if (!connected.has(conn.source)) {
connected.add(conn.source)
queue.push(conn.source)
}
}
})
}
return connected
}
// ============================================
// HELPER: Construct embed URL
// ============================================
function constructEmbedUrl(baseUrl: string, url: string | undefined): string | null {
if (!url) return null
const hashIndex = url.indexOf('#')
if (hashIndex !== -1) {
const basePart = url.substring(0, hashIndex)
const hashPart = url.substring(hashIndex)
const separator = basePart.includes('?') ? '&' : '?'
return `${baseUrl}${basePart}${separator}embed=true${hashPart}`
} else {
const separator = url.includes('?') ? '&' : '?'
return `${baseUrl}${url}${separator}embed=true`
}
}
// ============================================
// LAYOUT HELPERS
// ============================================
const getNodePosition = (
id: string,
category: string,
screens: ScreenDefinition[],
flowType: FlowType
) => {
const studioPositions: Record<string, { x: number; y: number }> = {
navigation: { x: 400, y: 50 },
content: { x: 50, y: 250 },
communication: { x: 750, y: 250 },
school: { x: 50, y: 500 },
admin: { x: 750, y: 500 },
ai: { x: 400, y: 380 },
}
const adminPositions: Record<string, { x: number; y: number }> = {
overview: { x: 400, y: 30 },
infrastructure: { x: 50, y: 150 },
compliance: { x: 700, y: 150 },
ai: { x: 50, y: 350 },
communication: { x: 400, y: 350 },
security: { x: 700, y: 350 },
content: { x: 50, y: 550 },
game: { x: 400, y: 550 },
misc: { x: 700, y: 550 },
}
const positions = flowType === 'studio' ? studioPositions : adminPositions
const base = positions[category] || { x: 400, y: 300 }
const categoryScreens = screens.filter(s => s.category === category)
const categoryIndex = categoryScreens.findIndex(s => s.id === id)
const cols = Math.ceil(Math.sqrt(categoryScreens.length + 1))
const row = Math.floor(categoryIndex / cols)
const col = categoryIndex % cols
return {
x: base.x + col * 160,
y: base.y + row * 90,
}
}
// ============================================
// MAIN COMPONENT
// ============================================
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)
// Get data based on flow type
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'
// Calculate connected nodes
const connectedNodes = useMemo(() => {
if (!selectedNode) return new Set<string>()
return findConnectedNodes(selectedNode, connections, 'children')
}, [selectedNode, connections])
// Create nodes with useMemo
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)
// Determine opacity
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])
// Create edges with useMemo
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([])
// Update nodes/edges when dependencies change
useEffect(() => {
setNodes(initialNodes)
setEdges(initialEdges)
}, [initialNodes, initialEdges])
// Reset when flow type changes
const handleFlowTypeChange = useCallback((newType: FlowType) => {
setFlowType(newType)
setSelectedNode(null)
setSelectedCategory(null)
setPreviewUrl(null)
setPreviewScreen(null)
}, [])
// Handle node click
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const screen = screens.find(s => s.id === node.id)
if (selectedNode === node.id) {
// Double-click: open in new tab
if (screen?.url) {
window.open(`${baseUrl}${screen.url}`, '_blank')
}
return
}
setSelectedNode(node.id)
setSelectedCategory(null)
if (screen?.url) {
const embedUrl = constructEmbedUrl(baseUrl, screen.url)
setPreviewUrl(embedUrl)
setPreviewScreen(screen)
}
}, [screens, baseUrl, selectedNode])
// Handle background click - deselect
const onPaneClick = useCallback(() => {
setSelectedNode(null)
setPreviewUrl(null)
setPreviewScreen(null)
}, [])
// Close preview
const closePreview = useCallback(() => {
setPreviewUrl(null)
setPreviewScreen(null)
setSelectedNode(null)
}, [])
// Stats
const stats = {
totalScreens: screens.length,
totalConnections: connections.length,
connectedCount: connectedNodes.size,
}
const categories = Object.keys(labels)
// Connected screens list
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">
<button
onClick={() => handleFlowTypeChange('studio')}
className={`p-6 rounded-xl border-2 transition-all ${
flowType === 'studio'
? 'border-green-500 bg-green-50 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 ${
flowType === 'studio' ? 'bg-green-500 text-white' : 'bg-slate-100'
}`}>
🎓
</div>
<div className="text-left">
<div className="font-bold text-lg">Studio (Port 8000)</div>
<div className="text-sm text-slate-500">Lehrer-Oberfläche</div>
<div className="text-xs text-slate-400 mt-1">{STUDIO_SCREENS.length} Screens</div>
</div>
</div>
</button>
<button
onClick={() => handleFlowTypeChange('admin')}
className={`p-6 rounded-xl border-2 transition-all ${
flowType === 'admin'
? 'border-purple-500 bg-purple-50 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 ${
flowType === 'admin' ? 'bg-purple-500 text-white' : 'bg-slate-100'
}`}>
</div>
<div className="text-left">
<div className="font-bold text-lg">Admin (Port 3000)</div>
<div className="text-sm text-slate-500">Admin Panel</div>
<div className="text-xs text-slate-400 mt-1">{ADMIN_SCREENS.length} Screens</div>
</div>
</div>
</button>
</div>
{/* Stats & Selection Info */}
<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">{stats.totalScreens}</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">{stats.totalConnections}</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">
{stats.connectedCount} verbundene Screen{stats.connectedCount !== 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"
>
Zurücksetzen
</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={() => {
const embedUrl = constructEmbedUrl(baseUrl, screen.url)
setPreviewUrl(embedUrl)
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 screen = screens.find(s => s.id === node.id)
const catColors = screen ? colors[screen.category] : null
return catColors?.border || '#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 = Öffnen
</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>
Öffnen
</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>
Schließen
</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 (when no 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">
<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)
const embedUrl = constructEmbedUrl(baseUrl, screen.url)
setPreviewUrl(embedUrl)
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>
)
}