'use client' import AdminLayout from '@/components/admin/AdminLayout' import SystemInfoSection, { SYSTEM_INFO_CONFIGS } from '@/components/admin/SystemInfoSection' import GameView from '@/components/admin/GameView' import Link from 'next/link' import { useState, useEffect, useCallback } from 'react' // Tab definitions type TabId = 'editor' | 'units' | 'sessions' | 'analytics' | 'content' interface Tab { id: TabId label: string icon: React.ReactNode } const tabs: Tab[] = [ { id: 'editor', label: 'Editor', icon: ( ), }, { id: 'units', label: 'Units', icon: ( ), }, { id: 'sessions', label: 'Sessions', icon: ( ), }, { id: 'analytics', label: 'Analytics', icon: ( ), }, { id: 'content', label: 'Content', icon: ( ), }, ] // Type definitions interface BridgeStatus { status: string unity_version: string project: string scene: string is_playing: boolean is_compiling: boolean errors: number warnings: number } // Unit System types interface UnitDefinition { unit_id: string template: string version: string locale: string[] grade_band: string[] duration_minutes: number difficulty: string subject?: string topic?: string learning_objectives?: string[] stops?: UnitStop[] } interface UnitStop { stop_id: string order: number label: { [key: string]: string } interaction?: { type: string params?: Record } } interface AnalyticsOverview { time_range: string total_sessions: number unique_students: number avg_completion_rate: number avg_learning_gain: number | null most_played_units: Array<{ unit_id: string; count: number }> struggling_concepts: Array<{ concept: string; count: number }> active_classes: number } interface GeneratedContent { unit_id: string locale: string generated_count?: number html?: string title?: string } interface LogEntry { time: string type: string message: string frame: number stack?: string } interface LogsResponse { count: number total_errors: number total_warnings: number total_info: number logs: LogEntry[] } interface DiagnosticEntry { category: string severity: 'ok' | 'warning' | 'error' message: string } interface DiagnoseResponse { diagnostics: DiagnosticEntry[] errors: number warnings: number } // Status Badge Component function StatusBadge({ status, error }: { status: BridgeStatus | null; error: string | null }) { if (error) { return (
Offline
) } if (!status) { return (
Verbinde...
) } if (status.is_compiling) { return (
Kompiliert...
) } return (
Online - Port 8090
) } // Stat Card Component function StatCard({ title, value, color = 'gray', icon, }: { title: string value: string | number color?: 'red' | 'yellow' | 'green' | 'blue' | 'gray' icon?: React.ReactNode }) { const colorClasses = { red: 'bg-red-50 border-red-200 text-red-700', yellow: 'bg-yellow-50 border-yellow-200 text-yellow-700', green: 'bg-green-50 border-green-200 text-green-700', blue: 'bg-blue-50 border-blue-200 text-blue-700', gray: 'bg-gray-50 border-gray-200 text-gray-700', } return (

{title}

{icon}

{value}

) } // Log Entry Component function LogEntryRow({ log }: { log: LogEntry }) { const [expanded, setExpanded] = useState(false) const typeColors = { error: 'bg-red-100 text-red-800', exception: 'bg-red-100 text-red-800', warning: 'bg-yellow-100 text-yellow-800', info: 'bg-blue-100 text-blue-800', } const typeColor = typeColors[log.type as keyof typeof typeColors] || 'bg-gray-100 text-gray-800' return (
setExpanded(!expanded)} >
{log.time} {log.type} {log.message.length > 150 && !expanded ? log.message.substring(0, 150) + '...' : log.message}
{expanded && log.stack && (
          {log.stack}
        
)}
) } // Console Log Panel Component function ConsoleLogPanel({ logs, onRefresh, onClear, isLoading, }: { logs: LogEntry[] onRefresh: () => void onClear: () => void isLoading: boolean }) { return (

Console Logs

{logs.length === 0 ? (

Keine Logs vorhanden

Logs erscheinen, wenn Unity Nachrichten generiert

) : ( logs.map((log, index) => ) )}
) } // Diagnostic Panel Component function DiagnosticPanel({ diagnostics, isLoading, onRun, }: { diagnostics: DiagnosticEntry[] isLoading: boolean onRun: () => void }) { const severityColors = { ok: 'text-green-600 bg-green-50', warning: 'text-yellow-600 bg-yellow-50', error: 'text-red-600 bg-red-50', } const severityIcons = { ok: '✓', warning: '⚠', error: '✕', } return (

Diagnostik

{diagnostics.length === 0 ? (

Klicke auf "Diagnose starten" um die Szene zu prüfen

) : (
{diagnostics.map((d, index) => (
{severityIcons[d.severity]} {d.category} {d.message}
))}
)}
) } export default function UnityBridgePage() { // Tab state const [activeTab, setActiveTab] = useState('editor') // Editor tab state const [status, setStatus] = useState(null) const [logs, setLogs] = useState([]) const [diagnostics, setDiagnostics] = useState([]) const [isLoading, setIsLoading] = useState(true) const [isLoadingLogs, setIsLoadingLogs] = useState(false) const [isLoadingDiagnose, setIsLoadingDiagnose] = useState(false) const [error, setError] = useState(null) // Units tab state const [units, setUnits] = useState([]) const [selectedUnit, setSelectedUnit] = useState(null) const [isLoadingUnits, setIsLoadingUnits] = useState(false) const [unitsError, setUnitsError] = useState(null) // Analytics tab state const [analyticsOverview, setAnalyticsOverview] = useState(null) const [isLoadingAnalytics, setIsLoadingAnalytics] = useState(false) // Content tab state const [generatedContent, setGeneratedContent] = useState(null) const [isGenerating, setIsGenerating] = useState(false) // Fetch status const fetchStatus = useCallback(async () => { try { const res = await fetch('/api/admin/unity-bridge?action=status') if (res.ok) { const data = await res.json() if (!data.offline) { setStatus(data) setError(null) } else { setError(data.error) setStatus(null) } } else { setError('Bridge nicht erreichbar') setStatus(null) } } catch { setError('Bridge offline - Server in Unity starten') setStatus(null) } setIsLoading(false) }, []) // Fetch logs const fetchLogs = useCallback(async () => { setIsLoadingLogs(true) try { const res = await fetch('/api/admin/unity-bridge?action=logs&limit=50') if (res.ok) { const data: LogsResponse = await res.json() if (data.logs) { setLogs(data.logs) } } } catch { // Ignore errors for logs } setIsLoadingLogs(false) }, []) // Clear logs const clearLogs = async () => { try { await fetch('/api/admin/unity-bridge?action=clear-logs', { method: 'POST' }) setLogs([]) } catch { // Ignore errors } } // Run diagnostics const runDiagnose = async () => { setIsLoadingDiagnose(true) try { const res = await fetch('/api/admin/unity-bridge?action=diagnose', { method: 'POST' }) if (res.ok) { const data: DiagnoseResponse = await res.json() if (data.diagnostics) { setDiagnostics(data.diagnostics) } } } catch { // Ignore errors } setIsLoadingDiagnose(false) } // Send command const sendCommand = async (command: string) => { try { await fetch(`/api/admin/unity-bridge?action=${command}`) // Refresh status after command setTimeout(fetchStatus, 500) } catch { // Ignore errors } } // ======================================== // Units Tab Functions // ======================================== const fetchUnits = useCallback(async () => { setIsLoadingUnits(true) setUnitsError(null) try { const res = await fetch('/api/admin/unity-bridge?action=units-list') if (res.ok) { const data = await res.json() if (Array.isArray(data)) { setUnits(data) } else if (data.offline) { setUnitsError(data.error) } } else { setUnitsError('Fehler beim Laden der Units') } } catch { setUnitsError('Backend nicht erreichbar') } setIsLoadingUnits(false) }, []) const fetchUnitDetails = async (unitId: string) => { try { const res = await fetch(`/api/admin/unity-bridge?action=units-get&unit_id=${unitId}`) if (res.ok) { const data = await res.json() setSelectedUnit(data.definition || data) } } catch { // Ignore errors } } // ======================================== // Analytics Tab Functions // ======================================== const fetchAnalytics = useCallback(async () => { setIsLoadingAnalytics(true) try { const res = await fetch('/api/admin/unity-bridge?action=analytics-overview') if (res.ok) { const data = await res.json() if (!data.offline) { setAnalyticsOverview(data) } } } catch { // Ignore errors } setIsLoadingAnalytics(false) }, []) // ======================================== // Content Tab Functions // ======================================== const generateH5P = async (unitId: string) => { setIsGenerating(true) try { const res = await fetch(`/api/admin/unity-bridge?action=content-h5p&unit_id=${unitId}`) if (res.ok) { const data = await res.json() setGeneratedContent(data) } } catch { // Ignore errors } setIsGenerating(false) } const generateWorksheet = async (unitId: string) => { setIsGenerating(true) try { const res = await fetch(`/api/admin/unity-bridge?action=content-worksheet&unit_id=${unitId}`) if (res.ok) { const data = await res.json() setGeneratedContent(data) } } catch { // Ignore errors } setIsGenerating(false) } const downloadPdf = (unitId: string) => { window.open(`/api/admin/unity-bridge?action=content-pdf&unit_id=${unitId}`, '_blank') } // Poll status every 5 seconds (Editor tab) useEffect(() => { fetchStatus() fetchLogs() const statusInterval = setInterval(fetchStatus, 5000) const logsInterval = setInterval(fetchLogs, 10000) return () => { clearInterval(statusInterval) clearInterval(logsInterval) } }, [fetchStatus, fetchLogs]) // Fetch data when tab changes useEffect(() => { if (activeTab === 'units' && units.length === 0) { fetchUnits() } if (activeTab === 'analytics' && !analyticsOverview) { fetchAnalytics() } }, [activeTab, units.length, analyticsOverview, fetchUnits, fetchAnalytics]) return ( {/* Header */}
{status && ( Unity {status.unity_version} - {status.project} )}
Wizard starten
{/* Offline Warning - only show on Editor tab */} {activeTab === 'editor' && error && (

{error}

Starte den Server in Unity: BreakpilotDrive → AI Bridge → Start Server

)} {/* Tab Navigation */}
{/* ======================================== EDITOR TAB ======================================== */} {activeTab === 'editor' && ( <> {/* Stats Grid */}
0 ? 'red' : 'green'} /> 0 ? 'yellow' : 'green'} />
{/* Quick Actions */}

Quick Actions

{/* Game View */}
{/* Two Column Layout */}
{/* Console Logs */} {/* Diagnostics */}
{/* API Info */}

API Endpoints

GET /status GET /logs/errors GET /scene POST /diagnose GET /play GET /stop GET /screenshot GET /stream/start GET /stream/frame

Basis-URL: http://localhost:8090 (Streaming-Endpoints blau markiert)

)} {/* ======================================== UNITS TAB ======================================== */} {activeTab === 'units' && (
{/* Units Header */}

Unit-Definitionen

{/* Units Error */} {unitsError && (

{unitsError}

Backend starten: cd backend && python main.py

)} {/* Units Grid */}
{units.map((unit) => (
fetchUnitDetails(unit.unit_id)} >

{unit.unit_id}

{unit.template}

{unit.topic || unit.subject || 'Keine Beschreibung'}

{unit.duration_minutes} min {unit.difficulty} {unit.grade_band?.join(', ') || '-'}
))}
{/* Empty State */} {!isLoadingUnits && units.length === 0 && !unitsError && (

Keine Units gefunden

Units werden unter backend/data/units/ gespeichert

)} {/* Selected Unit Details */} {selectedUnit && (

{selectedUnit.unit_id}

{/* Learning Objectives */} {selectedUnit.learning_objectives && selectedUnit.learning_objectives.length > 0 && (

Lernziele

    {selectedUnit.learning_objectives.map((obj, i) => (
  • {obj}
  • ))}
)} {/* Stops */} {selectedUnit.stops && selectedUnit.stops.length > 0 && (

Stops ({selectedUnit.stops.length})

{selectedUnit.stops.map((stop) => (
{stop.order + 1} {stop.label?.['de-DE'] || stop.stop_id} {stop.interaction && ( ({stop.interaction.type}) )}
))}
)} {/* JSON Preview */}
JSON anzeigen
                  {JSON.stringify(selectedUnit, null, 2)}
                
)}
)} {/* ======================================== SESSIONS TAB ======================================== */} {activeTab === 'sessions' && (

Session Monitor

Zeigt aktive Lern-Sessions in Echtzeit an

Sessions werden erstellt, wenn Spieler ein UnitGate im Spiel passieren.

)} {/* ======================================== ANALYTICS TAB ======================================== */} {activeTab === 'analytics' && (
{/* Analytics Header */}

Learning Analytics

{analyticsOverview ? ( <> {/* Stats Grid */}
0.7 ? 'green' : 'yellow'} /> 0.1 ? 'green' : 'gray'} />
{/* Most Played Units */} {analyticsOverview.most_played_units.length > 0 && (

Meistgespielte Units

{analyticsOverview.most_played_units.map((item, i) => (
{item.unit_id} {item.count} Sessions
))}
)} {/* Struggling Concepts */} {analyticsOverview.struggling_concepts.length > 0 && (

Schwierige Konzepte

{analyticsOverview.struggling_concepts.map((item, i) => (
{item.concept} {item.count} Fehler
))}
)} ) : (

{isLoadingAnalytics ? 'Lade Analytics...' : 'Keine Analytics-Daten verfügbar'}

Analytics werden nach abgeschlossenen Sessions verfügbar.

)}
)} {/* ======================================== CONTENT TAB ======================================== */} {activeTab === 'content' && (

Content Generator

{/* Unit Selector */} {units.length > 0 ? (

Unit auswählen

{units.map((unit) => ( ))}
) : ( )} {/* Generate Buttons */} {selectedUnit && (

Content generieren für: {selectedUnit.unit_id}

)} {/* Generated Content Preview */} {generatedContent && (

Generierter Content

{generatedContent.html ? (
) : (
                  {JSON.stringify(generatedContent, null, 2)}
                
)}
)}
)} {/* System Info Section - For Internal/External Audits */}
) }