This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/website/components/admin/GameView.tsx
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

317 lines
11 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback, useRef } from 'react'
interface StreamStatus {
is_streaming: boolean
frame_count: number
width: number
height: number
quality: number
uptime_seconds: string
}
interface StreamFrameResponse {
success: boolean
frame_count?: number
width?: number
height?: number
data?: string // Base64 encoded JPEG
message?: string
}
interface GameViewProps {
isUnityOnline: boolean
isPlaying?: boolean
}
export default function GameView({ isUnityOnline, isPlaying }: GameViewProps) {
const [isStreaming, setIsStreaming] = useState(false)
const [streamStatus, setStreamStatus] = useState<StreamStatus | null>(null)
const [frameData, setFrameData] = useState<string | null>(null)
const [frameCount, setFrameCount] = useState(0)
const [fps, setFps] = useState(0)
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const lastFrameTimeRef = useRef<number>(Date.now())
const frameCountRef = useRef<number>(0)
const fpsIntervalRef = useRef<NodeJS.Timeout | null>(null)
const streamIntervalRef = useRef<NodeJS.Timeout | null>(null)
// Fetch stream status
const fetchStreamStatus = useCallback(async () => {
if (!isUnityOnline) return
try {
const res = await fetch('/api/admin/unity-bridge?action=stream-status')
if (res.ok) {
const data: StreamStatus = await res.json()
setStreamStatus(data)
setIsStreaming(data.is_streaming)
}
} catch {
// Ignore errors
}
}, [isUnityOnline])
// Capture single screenshot
const captureScreenshot = async () => {
if (!isUnityOnline) return
setIsLoading(true)
setError(null)
try {
const res = await fetch('/api/admin/unity-bridge?action=screenshot')
if (res.ok) {
const blob = await res.blob()
const url = URL.createObjectURL(blob)
setFrameData(url)
setFrameCount(prev => prev + 1)
} else {
const errorData = await res.json()
setError(errorData.error || 'Screenshot fehlgeschlagen')
}
} catch (err) {
setError('Verbindung fehlgeschlagen')
}
setIsLoading(false)
}
// Start streaming
const startStreaming = async () => {
if (!isUnityOnline) return
setIsLoading(true)
setError(null)
try {
const res = await fetch('/api/admin/unity-bridge?action=stream-start')
if (res.ok) {
const data = await res.json()
if (data.success) {
setIsStreaming(true)
frameCountRef.current = 0
} else {
setError(data.message || 'Streaming konnte nicht gestartet werden')
}
}
} catch {
setError('Verbindung fehlgeschlagen')
}
setIsLoading(false)
}
// Stop streaming
const stopStreaming = async () => {
setIsLoading(true)
try {
const res = await fetch('/api/admin/unity-bridge?action=stream-stop')
if (res.ok) {
setIsStreaming(false)
}
} catch {
// Ignore errors
}
setIsLoading(false)
}
// Fetch frame during streaming
const fetchFrame = useCallback(async () => {
if (!isStreaming || !isUnityOnline) return
try {
const res = await fetch('/api/admin/unity-bridge?action=stream-frame')
if (res.ok) {
const data: StreamFrameResponse = await res.json()
if (data.success && data.data) {
setFrameData(`data:image/jpeg;base64,${data.data}`)
setFrameCount(data.frame_count || 0)
frameCountRef.current++
}
}
} catch {
// Ignore single frame failures
}
}, [isStreaming, isUnityOnline])
// Calculate FPS every second
useEffect(() => {
if (isStreaming) {
fpsIntervalRef.current = setInterval(() => {
setFps(frameCountRef.current)
frameCountRef.current = 0
}, 1000)
} else {
if (fpsIntervalRef.current) {
clearInterval(fpsIntervalRef.current)
fpsIntervalRef.current = null
}
setFps(0)
}
return () => {
if (fpsIntervalRef.current) {
clearInterval(fpsIntervalRef.current)
}
}
}, [isStreaming])
// Streaming loop - fetch frames at ~10 FPS
useEffect(() => {
if (isStreaming) {
// Initial fetch
fetchFrame()
// Setup interval for continuous fetching
streamIntervalRef.current = setInterval(fetchFrame, 100) // 10 FPS
} else {
if (streamIntervalRef.current) {
clearInterval(streamIntervalRef.current)
streamIntervalRef.current = null
}
}
return () => {
if (streamIntervalRef.current) {
clearInterval(streamIntervalRef.current)
}
}
}, [isStreaming, fetchFrame])
// Initial status fetch
useEffect(() => {
fetchStreamStatus()
}, [fetchStreamStatus])
return (
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 bg-gray-50">
<div className="flex items-center gap-3">
<h3 className="font-semibold text-gray-900">Game View</h3>
{isStreaming && (
<span className="flex items-center gap-1.5 px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs font-medium">
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
LIVE
</span>
)}
</div>
<div className="flex items-center gap-2">
{/* Screenshot Button */}
<button
onClick={captureScreenshot}
disabled={!isUnityOnline || isLoading || isStreaming}
className="px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-1.5"
title="Einzelner Screenshot"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
Screenshot
</button>
{/* Stream Toggle */}
{isStreaming ? (
<button
onClick={stopStreaming}
disabled={isLoading}
className="px-3 py-1.5 text-sm bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50 transition-colors flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<rect x="6" y="6" width="12" height="12" />
</svg>
Stop
</button>
) : (
<button
onClick={startStreaming}
disabled={!isUnityOnline || isLoading}
className="px-3 py-1.5 text-sm bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
Stream
</button>
)}
</div>
</div>
{/* Game View Area */}
<div className="relative bg-gray-900 aspect-video">
{error ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-white">
<svg className="w-12 h-12 mx-auto mb-3 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<p className="text-red-300">{error}</p>
</div>
</div>
) : !isUnityOnline ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-gray-400">
<svg className="w-16 h-16 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<p>Unity Bridge offline</p>
<p className="text-sm mt-1">Starte den Server in Unity</p>
</div>
</div>
) : !frameData ? (
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-gray-400">
<svg className="w-16 h-16 mx-auto mb-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p>Kein Bild verfügbar</p>
<p className="text-sm mt-1">
{isPlaying
? 'Klicke "Stream" um die Spielansicht zu sehen'
: 'Starte Play Mode und dann den Stream'}
</p>
</div>
</div>
) : (
// eslint-disable-next-line @next/next/no-img-element
<img
src={frameData}
alt="Unity Game View"
className="w-full h-full object-contain"
/>
)}
{/* Loading Overlay */}
{isLoading && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
{/* Status Bar */}
<div className="px-4 py-2 bg-gray-50 border-t border-gray-200 flex items-center justify-between text-xs text-gray-600">
<div className="flex items-center gap-4">
<span>
<span className="font-medium">Resolution:</span>{' '}
{streamStatus ? `${streamStatus.width}x${streamStatus.height}` : '1280x720'}
</span>
<span>
<span className="font-medium">Quality:</span>{' '}
{streamStatus ? `${streamStatus.quality}%` : '75%'}
</span>
</div>
<div className="flex items-center gap-4">
{isStreaming && (
<>
<span>
<span className="font-medium">FPS:</span> {fps}
</span>
<span>
<span className="font-medium">Frames:</span> {frameCount}
</span>
</>
)}
<span className={isUnityOnline ? 'text-green-600' : 'text-red-600'}>
{isUnityOnline ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
</div>
)
}