[split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
92
website/app/admin/unity-bridge/_components/AnalyticsTab.tsx
Normal file
92
website/app/admin/unity-bridge/_components/AnalyticsTab.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
|
||||
import type { AnalyticsOverview } from './types'
|
||||
import { StatCard } from './StatCard'
|
||||
|
||||
export function AnalyticsTab({
|
||||
analyticsOverview,
|
||||
isLoadingAnalytics,
|
||||
onFetchAnalytics,
|
||||
}: {
|
||||
analyticsOverview: AnalyticsOverview | null
|
||||
isLoadingAnalytics: boolean
|
||||
onFetchAnalytics: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Analytics Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Learning Analytics</h2>
|
||||
<button
|
||||
onClick={onFetchAnalytics}
|
||||
disabled={isLoadingAnalytics}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isLoadingAnalytics ? 'Laden...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{analyticsOverview ? (
|
||||
<>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard title="Sessions" value={analyticsOverview.total_sessions} color="blue" />
|
||||
<StatCard title="Schüler" value={analyticsOverview.unique_students} color="green" />
|
||||
<StatCard
|
||||
title="Ø Abschluss"
|
||||
value={`${Math.round(analyticsOverview.avg_completion_rate * 100)}%`}
|
||||
color={analyticsOverview.avg_completion_rate > 0.7 ? 'green' : 'yellow'}
|
||||
/>
|
||||
<StatCard
|
||||
title="Ø Learning Gain"
|
||||
value={analyticsOverview.avg_learning_gain !== null
|
||||
? `${Math.round(analyticsOverview.avg_learning_gain * 100)}%`
|
||||
: '-'
|
||||
}
|
||||
color={analyticsOverview.avg_learning_gain && analyticsOverview.avg_learning_gain > 0.1 ? 'green' : 'gray'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Most Played Units */}
|
||||
{analyticsOverview.most_played_units.length > 0 && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Meistgespielte Units</h3>
|
||||
<div className="space-y-2">
|
||||
{analyticsOverview.most_played_units.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
||||
<span className="text-sm text-gray-900">{item.unit_id}</span>
|
||||
<span className="text-sm font-medium text-gray-600">{item.count} Sessions</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Struggling Concepts */}
|
||||
{analyticsOverview.struggling_concepts.length > 0 && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Schwierige Konzepte</h3>
|
||||
<div className="space-y-2">
|
||||
{analyticsOverview.struggling_concepts.map((item, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-2 bg-red-50 rounded">
|
||||
<span className="text-sm text-gray-900">{item.concept}</span>
|
||||
<span className="text-sm font-medium text-red-600">{item.count} Fehler</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-12 bg-gray-50 rounded-lg border border-gray-200">
|
||||
<p className="text-gray-500">
|
||||
{isLoadingAnalytics ? 'Lade Analytics...' : 'Keine Analytics-Daten verfügbar'}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400 mt-2">
|
||||
Analytics werden nach abgeschlossenen Sessions verfügbar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { LogEntry } from './types'
|
||||
|
||||
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 (
|
||||
<div
|
||||
className="border-b border-gray-100 py-2 px-3 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-xs text-gray-400 font-mono whitespace-nowrap">{log.time}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${typeColor} font-medium uppercase`}>
|
||||
{log.type}
|
||||
</span>
|
||||
<span className="text-sm text-gray-700 flex-1 break-all">
|
||||
{log.message.length > 150 && !expanded
|
||||
? log.message.substring(0, 150) + '...'
|
||||
: log.message}
|
||||
</span>
|
||||
</div>
|
||||
{expanded && log.stack && (
|
||||
<pre className="mt-2 text-xs bg-gray-900 text-gray-100 p-2 rounded overflow-x-auto">
|
||||
{log.stack}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConsoleLogPanel({
|
||||
logs,
|
||||
onRefresh,
|
||||
onClear,
|
||||
isLoading,
|
||||
}: {
|
||||
logs: LogEntry[]
|
||||
onRefresh: () => void
|
||||
onClear: () => void
|
||||
isLoading: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">Console Logs</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded transition-colors"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={isLoading}
|
||||
className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded hover:bg-primary-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isLoading ? 'Laden...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-96 overflow-y-auto">
|
||||
{logs.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
<svg className="w-12 h-12 mx-auto mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p>Keine Logs vorhanden</p>
|
||||
<p className="text-sm mt-1">Logs erscheinen, wenn Unity Nachrichten generiert</p>
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log, index) => <LogEntryRow key={index} log={log} />)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
133
website/app/admin/unity-bridge/_components/ContentTab.tsx
Normal file
133
website/app/admin/unity-bridge/_components/ContentTab.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import type { UnitDefinition, GeneratedContent } from './types'
|
||||
|
||||
export function ContentTab({
|
||||
units,
|
||||
selectedUnit,
|
||||
generatedContent,
|
||||
isGenerating,
|
||||
onSelectUnit,
|
||||
onFetchUnits,
|
||||
onGenerateH5P,
|
||||
onGenerateWorksheet,
|
||||
onDownloadPdf,
|
||||
onClearContent,
|
||||
}: {
|
||||
units: UnitDefinition[]
|
||||
selectedUnit: UnitDefinition | null
|
||||
generatedContent: GeneratedContent | null
|
||||
isGenerating: boolean
|
||||
onSelectUnit: (unit: UnitDefinition) => void
|
||||
onFetchUnits: () => void
|
||||
onGenerateH5P: (unitId: string) => void
|
||||
onGenerateWorksheet: (unitId: string) => void
|
||||
onDownloadPdf: (unitId: string) => void
|
||||
onClearContent: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Content Generator</h2>
|
||||
|
||||
{/* Unit Selector */}
|
||||
{units.length > 0 ? (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="font-medium text-gray-900 mb-3">Unit auswählen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{units.map((unit) => (
|
||||
<button
|
||||
key={unit.unit_id}
|
||||
onClick={() => onSelectUnit(unit)}
|
||||
className={`
|
||||
p-3 text-left rounded-lg border transition-colors
|
||||
${selectedUnit?.unit_id === unit.unit_id
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className="font-medium text-gray-900">{unit.unit_id}</span>
|
||||
<span className="text-xs text-gray-500 ml-2">{unit.template}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={onFetchUnits}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
>
|
||||
Units laden
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Generate Buttons */}
|
||||
{selectedUnit && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 className="font-medium text-gray-900 mb-3">
|
||||
Content generieren für: <span className="text-primary-600">{selectedUnit.unit_id}</span>
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => onGenerateH5P(selectedUnit.unit_id)}
|
||||
disabled={isGenerating}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
H5P generieren
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onGenerateWorksheet(selectedUnit.unit_id)}
|
||||
disabled={isGenerating}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Arbeitsblatt HTML
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDownloadPdf(selectedUnit.unit_id)}
|
||||
disabled={isGenerating}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
PDF Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generated Content Preview */}
|
||||
{generatedContent && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-medium text-gray-900">Generierter Content</h3>
|
||||
<button
|
||||
onClick={onClearContent}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{generatedContent.html ? (
|
||||
<div
|
||||
className="prose prose-sm max-w-none p-4 bg-gray-50 rounded-lg border"
|
||||
dangerouslySetInnerHTML={{ __html: generatedContent.html }}
|
||||
/>
|
||||
) : (
|
||||
<pre className="p-4 bg-gray-900 text-gray-100 rounded-lg text-xs overflow-x-auto max-h-96">
|
||||
{JSON.stringify(generatedContent, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import type { DiagnosticEntry } from './types'
|
||||
|
||||
export 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 (
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200">
|
||||
<h3 className="font-semibold text-gray-900">Diagnostik</h3>
|
||||
<button
|
||||
onClick={onRun}
|
||||
disabled={isLoading}
|
||||
className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded hover:bg-primary-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isLoading ? 'Prüfe...' : 'Diagnose starten'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
{diagnostics.length === 0 ? (
|
||||
<p className="text-gray-500 text-center py-4">
|
||||
Klicke auf "Diagnose starten" um die Szene zu prüfen
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{diagnostics.map((d, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex items-center gap-3 p-2 rounded ${severityColors[d.severity]}`}
|
||||
>
|
||||
<span className="font-bold">{severityIcons[d.severity]}</span>
|
||||
<span className="text-sm font-medium">{d.category}</span>
|
||||
<span className="text-sm flex-1">{d.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
132
website/app/admin/unity-bridge/_components/EditorTab.tsx
Normal file
132
website/app/admin/unity-bridge/_components/EditorTab.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
'use client'
|
||||
|
||||
import GameView from '@/components/admin/GameView'
|
||||
import type { BridgeStatus, LogEntry, DiagnosticEntry } from './types'
|
||||
import { StatCard } from './StatCard'
|
||||
import { ConsoleLogPanel } from './ConsoleLogPanel'
|
||||
import { DiagnosticPanel } from './DiagnosticPanel'
|
||||
|
||||
export function EditorTab({
|
||||
status,
|
||||
logs,
|
||||
diagnostics,
|
||||
isLoadingLogs,
|
||||
isLoadingDiagnose,
|
||||
error,
|
||||
onSendCommand,
|
||||
onFetchLogs,
|
||||
onClearLogs,
|
||||
onRunDiagnose,
|
||||
}: {
|
||||
status: BridgeStatus | null
|
||||
logs: LogEntry[]
|
||||
diagnostics: DiagnosticEntry[]
|
||||
isLoadingLogs: boolean
|
||||
isLoadingDiagnose: boolean
|
||||
error: string | null
|
||||
onSendCommand: (command: string) => void
|
||||
onFetchLogs: () => void
|
||||
onClearLogs: () => void
|
||||
onRunDiagnose: () => void
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<StatCard
|
||||
title="Fehler"
|
||||
value={status?.errors ?? '-'}
|
||||
color={status?.errors && status.errors > 0 ? 'red' : 'green'}
|
||||
/>
|
||||
<StatCard
|
||||
title="Warnungen"
|
||||
value={status?.warnings ?? '-'}
|
||||
color={status?.warnings && status.warnings > 0 ? 'yellow' : 'green'}
|
||||
/>
|
||||
<StatCard title="Szene" value={status?.scene ?? '-'} color="blue" />
|
||||
<StatCard
|
||||
title="Play Mode"
|
||||
value={status?.is_playing ? 'Aktiv' : 'Inaktiv'}
|
||||
color={status?.is_playing ? 'green' : 'gray'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-4 mb-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-3">Quick Actions</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => onSendCommand('play')}
|
||||
disabled={!status || status.is_playing}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSendCommand('stop')}
|
||||
disabled={!status || !status.is_playing}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
|
||||
>
|
||||
<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={() => onSendCommand('quicksetup')}
|
||||
disabled={!status || status.is_playing}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Quick Setup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Game View */}
|
||||
<div className="mb-6">
|
||||
<GameView
|
||||
isUnityOnline={!!status && !error}
|
||||
isPlaying={status?.is_playing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Two Column Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<ConsoleLogPanel
|
||||
logs={logs}
|
||||
onRefresh={onFetchLogs}
|
||||
onClear={onClearLogs}
|
||||
isLoading={isLoadingLogs}
|
||||
/>
|
||||
<DiagnosticPanel
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoadingDiagnose}
|
||||
onRun={onRunDiagnose}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Info */}
|
||||
<div className="mt-6 bg-slate-50 rounded-lg border border-slate-200 p-4">
|
||||
<h3 className="font-semibold text-slate-900 mb-2">API Endpoints</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-sm font-mono">
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">GET /status</code>
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">GET /logs/errors</code>
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">GET /scene</code>
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">POST /diagnose</code>
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">GET /play</code>
|
||||
<code className="bg-slate-100 px-2 py-1 rounded">GET /stop</code>
|
||||
<code className="bg-blue-100 text-blue-800 px-2 py-1 rounded">GET /screenshot</code>
|
||||
<code className="bg-blue-100 text-blue-800 px-2 py-1 rounded">GET /stream/start</code>
|
||||
<code className="bg-blue-100 text-blue-800 px-2 py-1 rounded">GET /stream/frame</code>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mt-3">
|
||||
Basis-URL: <code className="bg-slate-100 px-1 rounded">http://localhost:8090</code>
|
||||
<span className="text-blue-600 ml-2">(Streaming-Endpoints blau markiert)</span>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
31
website/app/admin/unity-bridge/_components/StatCard.tsx
Normal file
31
website/app/admin/unity-bridge/_components/StatCard.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
export 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 (
|
||||
<div className={`rounded-lg border p-4 ${colorClasses[color]}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium opacity-80">{title}</p>
|
||||
{icon}
|
||||
</div>
|
||||
<p className="text-2xl font-bold mt-1">{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
website/app/admin/unity-bridge/_components/StatusBadge.tsx
Normal file
39
website/app/admin/unity-bridge/_components/StatusBadge.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import type { BridgeStatus } from './types'
|
||||
|
||||
export function StatusBadge({ status, error }: { status: BridgeStatus | null; error: string | null }) {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-red-100 text-red-800 rounded-full text-sm font-medium">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
|
||||
Offline
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-gray-100 text-gray-600 rounded-full text-sm font-medium">
|
||||
<span className="w-2 h-2 bg-gray-400 rounded-full animate-pulse" />
|
||||
Verbinde...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status.is_compiling) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-yellow-100 text-yellow-800 rounded-full text-sm font-medium">
|
||||
<span className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse" />
|
||||
Kompiliert...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-green-100 text-green-800 rounded-full text-sm font-medium">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full" />
|
||||
Online - Port 8090
|
||||
</div>
|
||||
)
|
||||
}
|
||||
145
website/app/admin/unity-bridge/_components/UnitsTab.tsx
Normal file
145
website/app/admin/unity-bridge/_components/UnitsTab.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import type { UnitDefinition } from './types'
|
||||
|
||||
export function UnitsTab({
|
||||
units,
|
||||
selectedUnit,
|
||||
isLoadingUnits,
|
||||
unitsError,
|
||||
onFetchUnits,
|
||||
onFetchUnitDetails,
|
||||
onClearSelection,
|
||||
}: {
|
||||
units: UnitDefinition[]
|
||||
selectedUnit: UnitDefinition | null
|
||||
isLoadingUnits: boolean
|
||||
unitsError: string | null
|
||||
onFetchUnits: () => void
|
||||
onFetchUnitDetails: (unitId: string) => void
|
||||
onClearSelection: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Units Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Unit-Definitionen</h2>
|
||||
<button
|
||||
onClick={onFetchUnits}
|
||||
disabled={isLoadingUnits}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isLoadingUnits ? 'Laden...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Units Error */}
|
||||
{unitsError && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-800">{unitsError}</p>
|
||||
<p className="text-sm text-red-600 mt-1">
|
||||
Backend starten: <code className="bg-red-100 px-1 rounded">cd backend && python main.py</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Units Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{units.map((unit) => (
|
||||
<div
|
||||
key={unit.unit_id}
|
||||
className={`
|
||||
p-4 bg-white rounded-lg border-2 cursor-pointer transition-all
|
||||
${selectedUnit?.unit_id === unit.unit_id
|
||||
? 'border-primary-500 shadow-md'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}
|
||||
`}
|
||||
onClick={() => onFetchUnitDetails(unit.unit_id)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h3 className="font-semibold text-gray-900">{unit.unit_id}</h3>
|
||||
<span className={`
|
||||
px-2 py-0.5 text-xs rounded-full
|
||||
${unit.template === 'flight_path' ? 'bg-blue-100 text-blue-700' : 'bg-purple-100 text-purple-700'}
|
||||
`}>
|
||||
{unit.template}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mb-2">{unit.topic || unit.subject || 'Keine Beschreibung'}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<span>{unit.duration_minutes} min</span>
|
||||
<span>{unit.difficulty}</span>
|
||||
<span>{unit.grade_band?.join(', ') || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoadingUnits && units.length === 0 && !unitsError && (
|
||||
<div className="text-center py-12 text-gray-500">
|
||||
<p>Keine Units gefunden</p>
|
||||
<p className="text-sm mt-1">Units werden unter <code className="bg-gray-100 px-1 rounded">backend/data/units/</code> gespeichert</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selected Unit Details */}
|
||||
{selectedUnit && (
|
||||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">{selectedUnit.unit_id}</h3>
|
||||
<button
|
||||
onClick={onClearSelection}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Learning Objectives */}
|
||||
{selectedUnit.learning_objectives && selectedUnit.learning_objectives.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Lernziele</h4>
|
||||
<ul className="list-disc list-inside text-sm text-gray-600 space-y-1">
|
||||
{selectedUnit.learning_objectives.map((obj, i) => (
|
||||
<li key={i}>{obj}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stops */}
|
||||
{selectedUnit.stops && selectedUnit.stops.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">Stops ({selectedUnit.stops.length})</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{selectedUnit.stops.map((stop) => (
|
||||
<div key={stop.stop_id} className="flex items-center gap-2 p-2 bg-gray-50 rounded text-sm">
|
||||
<span className="w-6 h-6 flex items-center justify-center bg-primary-100 text-primary-700 rounded-full text-xs font-medium">
|
||||
{stop.order + 1}
|
||||
</span>
|
||||
<span className="text-gray-900">{stop.label?.['de-DE'] || stop.stop_id}</span>
|
||||
{stop.interaction && (
|
||||
<span className="text-xs text-gray-500">({stop.interaction.type})</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* JSON Preview */}
|
||||
<details className="mt-4">
|
||||
<summary className="text-sm font-medium text-gray-700 cursor-pointer">JSON anzeigen</summary>
|
||||
<pre className="mt-2 p-4 bg-gray-900 text-gray-100 rounded-lg text-xs overflow-x-auto max-h-96">
|
||||
{JSON.stringify(selectedUnit, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
49
website/app/admin/unity-bridge/_components/tabs.tsx
Normal file
49
website/app/admin/unity-bridge/_components/tabs.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Tab } from './types'
|
||||
|
||||
export const tabs: Tab[] = [
|
||||
{
|
||||
id: 'editor',
|
||||
label: 'Editor',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'units',
|
||||
label: 'Units',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: 'Sessions',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
label: 'Analytics',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
label: 'Content',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
]
|
||||
92
website/app/admin/unity-bridge/_components/types.ts
Normal file
92
website/app/admin/unity-bridge/_components/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
// Tab definitions
|
||||
export type TabId = 'editor' | 'units' | 'sessions' | 'analytics' | 'content'
|
||||
|
||||
export interface Tab {
|
||||
id: TabId
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
// Type definitions
|
||||
export interface BridgeStatus {
|
||||
status: string
|
||||
unity_version: string
|
||||
project: string
|
||||
scene: string
|
||||
is_playing: boolean
|
||||
is_compiling: boolean
|
||||
errors: number
|
||||
warnings: number
|
||||
}
|
||||
|
||||
// Unit System types
|
||||
export 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[]
|
||||
}
|
||||
|
||||
export interface UnitStop {
|
||||
stop_id: string
|
||||
order: number
|
||||
label: { [key: string]: string }
|
||||
interaction?: {
|
||||
type: string
|
||||
params?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export 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
|
||||
}
|
||||
|
||||
export interface GeneratedContent {
|
||||
unit_id: string
|
||||
locale: string
|
||||
generated_count?: number
|
||||
html?: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
time: string
|
||||
type: string
|
||||
message: string
|
||||
frame: number
|
||||
stack?: string
|
||||
}
|
||||
|
||||
export interface LogsResponse {
|
||||
count: number
|
||||
total_errors: number
|
||||
total_warnings: number
|
||||
total_info: number
|
||||
logs: LogEntry[]
|
||||
}
|
||||
|
||||
export interface DiagnosticEntry {
|
||||
category: string
|
||||
severity: 'ok' | 'warning' | 'error'
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DiagnoseResponse {
|
||||
diagnostics: DiagnosticEntry[]
|
||||
errors: number
|
||||
warnings: number
|
||||
}
|
||||
258
website/app/admin/unity-bridge/_components/useUnityBridge.ts
Normal file
258
website/app/admin/unity-bridge/_components/useUnityBridge.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type {
|
||||
BridgeStatus,
|
||||
LogEntry,
|
||||
DiagnosticEntry,
|
||||
UnitDefinition,
|
||||
AnalyticsOverview,
|
||||
GeneratedContent,
|
||||
LogsResponse,
|
||||
DiagnoseResponse,
|
||||
TabId,
|
||||
} from './types'
|
||||
|
||||
export function useUnityBridge() {
|
||||
// Tab state
|
||||
const [activeTab, setActiveTab] = useState<TabId>('editor')
|
||||
|
||||
// Editor tab state
|
||||
const [status, setStatus] = useState<BridgeStatus | null>(null)
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [diagnostics, setDiagnostics] = useState<DiagnosticEntry[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isLoadingLogs, setIsLoadingLogs] = useState(false)
|
||||
const [isLoadingDiagnose, setIsLoadingDiagnose] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Units tab state
|
||||
const [units, setUnits] = useState<UnitDefinition[]>([])
|
||||
const [selectedUnit, setSelectedUnit] = useState<UnitDefinition | null>(null)
|
||||
const [isLoadingUnits, setIsLoadingUnits] = useState(false)
|
||||
const [unitsError, setUnitsError] = useState<string | null>(null)
|
||||
|
||||
// Analytics tab state
|
||||
const [analyticsOverview, setAnalyticsOverview] = useState<AnalyticsOverview | null>(null)
|
||||
const [isLoadingAnalytics, setIsLoadingAnalytics] = useState(false)
|
||||
|
||||
// Content tab state
|
||||
const [generatedContent, setGeneratedContent] = useState<GeneratedContent | null>(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}`)
|
||||
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 {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
status,
|
||||
logs,
|
||||
diagnostics,
|
||||
isLoading,
|
||||
isLoadingLogs,
|
||||
isLoadingDiagnose,
|
||||
error,
|
||||
units,
|
||||
selectedUnit,
|
||||
setSelectedUnit,
|
||||
isLoadingUnits,
|
||||
unitsError,
|
||||
analyticsOverview,
|
||||
isLoadingAnalytics,
|
||||
generatedContent,
|
||||
setGeneratedContent,
|
||||
isGenerating,
|
||||
fetchLogs,
|
||||
clearLogs,
|
||||
runDiagnose,
|
||||
sendCommand,
|
||||
fetchUnits,
|
||||
fetchUnitDetails,
|
||||
fetchAnalytics,
|
||||
generateH5P,
|
||||
generateWorksheet,
|
||||
downloadPdf,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user