fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,329 @@
'use client'
import { useEffect, useRef, useState, useCallback } from 'react'
import { LearningNode } from '@/app/geo-lernwelt/types'
interface UnityViewerProps {
aoiId: string
manifestUrl?: string
learningNodes: LearningNode[]
geoServiceUrl: string
}
interface UnityInstance {
SendMessage: (objectName: string, methodName: string, value?: string | number) => void
}
export default function UnityViewer({
aoiId,
manifestUrl,
learningNodes,
geoServiceUrl,
}: UnityViewerProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [unityInstance, setUnityInstance] = useState<UnityInstance | null>(null)
const [loadingProgress, setLoadingProgress] = useState(0)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedNode, setSelectedNode] = useState<LearningNode | null>(null)
const [showNodePanel, setShowNodePanel] = useState(false)
// Placeholder mode when Unity build is not available
const [placeholderMode, setPlaceholderMode] = useState(true)
useEffect(() => {
// Check if Unity build exists
// For now, always use placeholder mode since Unity build needs to be created separately
setPlaceholderMode(true)
setIsLoading(false)
}, [])
// Unity message handler (for when Unity build exists)
useEffect(() => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).handleUnityMessage = (message: string) => {
try {
const data = JSON.parse(message)
switch (data.type) {
case 'nodeSelected':
const node = learningNodes.find((n) => n.id === data.nodeId)
if (node) {
setSelectedNode(node)
setShowNodePanel(true)
}
break
case 'terrainLoaded':
console.log('Unity terrain loaded')
break
case 'error':
setError(data.message)
break
}
} catch (e) {
console.error('Error parsing Unity message:', e)
}
}
}
return () => {
if (typeof window !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (window as any).handleUnityMessage
}
}
}, [learningNodes])
const sendToUnity = useCallback(
(objectName: string, methodName: string, value?: string | number) => {
if (unityInstance) {
unityInstance.SendMessage(objectName, methodName, value)
}
},
[unityInstance]
)
// Send AOI data to Unity when ready
useEffect(() => {
if (unityInstance && manifestUrl) {
sendToUnity('TerrainManager', 'LoadManifest', manifestUrl)
// Send learning nodes
const nodesJson = JSON.stringify(learningNodes)
sendToUnity('LearningNodeManager', 'LoadNodes', nodesJson)
}
}, [unityInstance, manifestUrl, learningNodes, sendToUnity])
const handleNodeClick = (node: LearningNode) => {
setSelectedNode(node)
setShowNodePanel(true)
if (unityInstance) {
sendToUnity('CameraController', 'FocusOnNode', node.id)
}
}
const handleCloseNodePanel = () => {
setShowNodePanel(false)
setSelectedNode(null)
}
// Placeholder 3D view (when Unity is not available)
const PlaceholderView = () => (
<div className="w-full h-full bg-gradient-to-b from-sky-400 to-sky-200 relative overflow-hidden">
{/* Sky */}
<div className="absolute inset-0 bg-gradient-to-b from-sky-500 via-sky-400 to-sky-300" />
{/* Sun */}
<div className="absolute top-8 right-12 w-16 h-16 bg-yellow-300 rounded-full shadow-lg shadow-yellow-400/50" />
{/* Clouds */}
<div className="absolute top-12 left-8 w-24 h-8 bg-white/80 rounded-full blur-sm" />
<div className="absolute top-20 left-20 w-16 h-6 bg-white/70 rounded-full blur-sm" />
<div className="absolute top-16 right-32 w-20 h-7 bg-white/75 rounded-full blur-sm" />
{/* Mountains */}
<svg
className="absolute bottom-0 left-0 w-full h-1/2"
viewBox="0 0 1200 400"
preserveAspectRatio="none"
>
{/* Background mountains */}
<path d="M0,400 L200,150 L400,300 L600,100 L800,250 L1000,80 L1200,200 L1200,400 Z" fill="#4ade80" fillOpacity="0.5" />
{/* Foreground mountains */}
<path d="M0,400 L150,200 L300,350 L500,150 L700,300 L900,120 L1100,280 L1200,180 L1200,400 Z" fill="#22c55e" />
{/* Ground */}
<path d="M0,400 L0,350 Q300,320 600,350 Q900,380 1200,340 L1200,400 Z" fill="#15803d" />
</svg>
{/* Learning Nodes as Markers */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative w-full h-full">
{learningNodes.map((node, idx) => {
// Position nodes across the view
const positions = [
{ left: '20%', top: '55%' },
{ left: '40%', top: '45%' },
{ left: '60%', top: '50%' },
{ left: '75%', top: '55%' },
{ left: '30%', top: '60%' },
]
const pos = positions[idx % positions.length]
return (
<button
key={node.id}
onClick={() => handleNodeClick(node)}
style={{ left: pos.left, top: pos.top }}
className="absolute transform -translate-x-1/2 -translate-y-1/2 group"
>
<div className="relative">
{/* Marker pin */}
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white font-bold shadow-lg group-hover:bg-blue-600 transition-colors animate-bounce">
{idx + 1}
</div>
{/* Pulse effect */}
<div className="absolute inset-0 w-8 h-8 bg-blue-500 rounded-full animate-ping opacity-25" />
{/* Label */}
<div className="absolute top-10 left-1/2 transform -translate-x-1/2 bg-black/70 text-white text-xs px-2 py-1 rounded whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity">
{node.title}
</div>
</div>
</button>
)
})}
</div>
</div>
{/* 3D View Notice */}
<div className="absolute bottom-4 left-4 bg-black/50 text-white px-4 py-2 rounded-lg text-sm">
<p className="font-medium">Vorschau-Modus</p>
<p className="text-white/70 text-xs">
Unity WebGL-Build wird fuer die volle 3D-Ansicht benoetigt
</p>
</div>
{/* Controls hint */}
<div className="absolute bottom-4 right-4 bg-black/50 text-white px-4 py-2 rounded-lg text-sm">
<p className="text-white/70">Klicke auf die Marker um Lernstationen anzuzeigen</p>
</div>
</div>
)
return (
<div className="relative w-full h-full bg-slate-900">
{/* Loading overlay */}
{isLoading && (
<div className="absolute inset-0 bg-slate-900 flex flex-col items-center justify-center z-10">
<div className="w-64 mb-4">
<div className="h-2 bg-slate-700 rounded-full overflow-hidden">
<div
className="h-full bg-blue-500 transition-all duration-300"
style={{ width: `${loadingProgress}%` }}
/>
</div>
</div>
<p className="text-white/60 text-sm">Lade 3D-Lernwelt... {loadingProgress}%</p>
</div>
)}
{/* Error display */}
{error && (
<div className="absolute inset-0 bg-slate-900 flex items-center justify-center z-10">
<div className="text-center p-8">
<div className="text-red-400 text-6xl mb-4"></div>
<p className="text-white text-lg mb-2">Fehler beim Laden</p>
<p className="text-white/60 text-sm max-w-md">{error}</p>
<button
onClick={() => {
setError(null)
setIsLoading(true)
setLoadingProgress(0)
}}
className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
>
Erneut versuchen
</button>
</div>
</div>
)}
{/* Unity Canvas or Placeholder */}
{placeholderMode ? (
<PlaceholderView />
) : (
<canvas
ref={canvasRef}
id="unity-canvas"
className="w-full h-full"
tabIndex={-1}
/>
)}
{/* Learning Node Panel */}
{showNodePanel && selectedNode && (
<div className="absolute top-4 right-4 w-80 bg-white/95 dark:bg-slate-800/95 backdrop-blur-lg rounded-2xl shadow-2xl overflow-hidden z-20">
{/* Header */}
<div className="bg-gradient-to-r from-blue-500 to-purple-500 p-4 text-white">
<div className="flex items-center justify-between">
<span className="text-sm opacity-80">Station {learningNodes.indexOf(selectedNode) + 1}</span>
<button
onClick={handleCloseNodePanel}
className="w-6 h-6 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center"
>
</button>
</div>
<h3 className="font-semibold text-lg mt-1">{selectedNode.title}</h3>
</div>
{/* Content */}
<div className="p-4 space-y-4">
{/* Question */}
<div>
<h4 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Aufgabe</h4>
<p className="text-slate-800 dark:text-white">{selectedNode.question}</p>
</div>
{/* Hints */}
{selectedNode.hints.length > 0 && (
<div>
<h4 className="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">Hinweise</h4>
<ul className="list-disc list-inside text-sm text-slate-600 dark:text-slate-300 space-y-1">
{selectedNode.hints.map((hint, idx) => (
<li key={idx}>{hint}</li>
))}
</ul>
</div>
)}
{/* Answer (collapsible) */}
<details className="bg-green-50 dark:bg-green-900/20 rounded-lg">
<summary className="p-3 cursor-pointer text-green-700 dark:text-green-400 font-medium text-sm">
Loesung anzeigen
</summary>
<div className="px-3 pb-3">
<p className="text-green-800 dark:text-green-300 text-sm">{selectedNode.answer}</p>
{selectedNode.explanation && (
<p className="text-green-600 dark:text-green-400 text-xs mt-2 italic">
{selectedNode.explanation}
</p>
)}
</div>
</details>
{/* Points */}
<div className="flex items-center justify-between pt-2 border-t border-slate-200 dark:border-slate-700">
<span className="text-sm text-slate-500 dark:text-slate-400">Punkte</span>
<span className="font-bold text-blue-500">{selectedNode.points}</span>
</div>
</div>
</div>
)}
{/* Node List (minimized) */}
{!showNodePanel && learningNodes.length > 0 && (
<div className="absolute top-4 right-4 bg-black/70 rounded-xl p-3 z-20 max-h-64 overflow-y-auto">
<h4 className="text-white text-sm font-medium mb-2">Lernstationen</h4>
<div className="space-y-1">
{learningNodes.map((node, idx) => (
<button
key={node.id}
onClick={() => handleNodeClick(node)}
className="w-full text-left px-3 py-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors text-white text-sm flex items-center gap-2"
>
<span className="w-5 h-5 bg-blue-500 rounded-full flex items-center justify-center text-xs">
{idx + 1}
</span>
<span className="truncate">{node.title}</span>
</button>
))}
</div>
</div>
)}
</div>
)
}