- 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>
791 lines
35 KiB
TypeScript
791 lines
35 KiB
TypeScript
'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>
|
||
)
|
||
}
|