[split-required] Split final batch of monoliths >1000 LOC
Python (6 files in klausur-service): - rbac.py (1,132 → 4), admin_api.py (1,012 → 4) - routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5) Python (2 files in backend-lehrer): - unit_api.py (1,226 → 6), game_api.py (1,129 → 5) Website (6 page files): - 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components in website/components/klausur-korrektur/ (17 shared files) - companion (1,057 → 10), magic-help (1,017 → 8) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
88
website/app/admin/companion/_components/BacklogTab.tsx
Normal file
88
website/app/admin/companion/_components/BacklogTab.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import type { Feature } from './types'
|
||||
import { priorityColors } from './types'
|
||||
|
||||
interface BacklogTabProps {
|
||||
features: Feature[]
|
||||
}
|
||||
|
||||
export default function BacklogTab({ features }: BacklogTabProps) {
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Todo Column */}
|
||||
<div className="bg-amber-50 rounded-xl p-4">
|
||||
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 bg-amber-200 rounded-full flex items-center justify-center text-sm">
|
||||
{features.filter(f => f.status === 'todo').length}
|
||||
</span>
|
||||
Todo
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{features.filter(f => f.status === 'todo').map(f => (
|
||||
<div key={f.id} className="bg-white p-3 rounded-lg shadow-sm">
|
||||
<div className="font-medium text-sm text-slate-900">{f.title}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{f.description}</div>
|
||||
<div className="flex gap-1 mt-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${priorityColors[f.priority]}`}>
|
||||
{f.priority}
|
||||
</span>
|
||||
<span className="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
|
||||
{f.effort}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* In Progress Column */}
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
<h3 className="font-semibold text-blue-800 mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 bg-blue-200 rounded-full flex items-center justify-center text-sm">
|
||||
{features.filter(f => f.status === 'in_progress').length}
|
||||
</span>
|
||||
In Arbeit
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{features.filter(f => f.status === 'in_progress').map(f => (
|
||||
<div key={f.id} className="bg-white p-3 rounded-lg shadow-sm">
|
||||
<div className="font-medium text-sm text-slate-900">{f.title}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{f.description}</div>
|
||||
<div className="flex gap-1 mt-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${priorityColors[f.priority]}`}>
|
||||
{f.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backlog Column */}
|
||||
<div className="bg-slate-100 rounded-xl p-4">
|
||||
<h3 className="font-semibold text-slate-700 mb-3 flex items-center gap-2">
|
||||
<span className="w-6 h-6 bg-slate-300 rounded-full flex items-center justify-center text-sm">
|
||||
{features.filter(f => f.status === 'backlog').length}
|
||||
</span>
|
||||
Backlog
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{features.filter(f => f.status === 'backlog').map(f => (
|
||||
<div key={f.id} className="bg-white p-3 rounded-lg shadow-sm">
|
||||
<div className="font-medium text-sm text-slate-900">{f.title}</div>
|
||||
<div className="text-xs text-slate-500 mt-1">{f.description}</div>
|
||||
<div className="flex gap-1 mt-2">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${priorityColors[f.priority]}`}>
|
||||
{f.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
website/app/admin/companion/_components/FeaturesTab.tsx
Normal file
76
website/app/admin/companion/_components/FeaturesTab.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import type { Feature } from './types'
|
||||
import { statusColors, priorityColors } from './types'
|
||||
import { roadmapPhases } from './data'
|
||||
|
||||
interface FeaturesTabProps {
|
||||
features: Feature[]
|
||||
selectedPhase: string | null
|
||||
setSelectedPhase: (phase: string | null) => void
|
||||
updateFeatureStatus: (id: string, status: Feature['status']) => void
|
||||
}
|
||||
|
||||
export default function FeaturesTab({ features, selectedPhase, setSelectedPhase, updateFeatureStatus }: FeaturesTabProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Phase Filter */}
|
||||
<div className="flex gap-2 mb-4 flex-wrap">
|
||||
<button
|
||||
onClick={() => setSelectedPhase(null)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
!selectedPhase ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
{roadmapPhases.map(phase => (
|
||||
<button
|
||||
key={phase.id}
|
||||
onClick={() => setSelectedPhase(phase.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedPhase === phase.id ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{phase.name.replace('Phase ', 'P')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Features List */}
|
||||
<div className="space-y-2">
|
||||
{features
|
||||
.filter(f => !selectedPhase || f.phase === selectedPhase)
|
||||
.map(feature => (
|
||||
<div key={feature.id} className="flex items-center gap-4 p-3 bg-slate-50 rounded-lg">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${priorityColors[feature.priority]}`}>
|
||||
{feature.priority}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-slate-900 truncate">{feature.title}</div>
|
||||
<div className="text-xs text-slate-500 truncate">{feature.description}</div>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${
|
||||
feature.effort === 'small' ? 'bg-green-100 text-green-700' :
|
||||
feature.effort === 'medium' ? 'bg-amber-100 text-amber-700' :
|
||||
feature.effort === 'large' ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{feature.effort}
|
||||
</span>
|
||||
<select
|
||||
value={feature.status}
|
||||
onChange={(e) => updateFeatureStatus(feature.id, e.target.value as Feature['status'])}
|
||||
className={`px-2 py-1 rounded text-xs font-medium border-0 cursor-pointer ${statusColors[feature.status]}`}
|
||||
>
|
||||
<option value="done">Fertig</option>
|
||||
<option value="in_progress">In Arbeit</option>
|
||||
<option value="todo">Todo</option>
|
||||
<option value="backlog">Backlog</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
110
website/app/admin/companion/_components/FeedbackTab.tsx
Normal file
110
website/app/admin/companion/_components/FeedbackTab.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
|
||||
import type { Feature, TeacherFeedback } from './types'
|
||||
import { priorityColors, feedbackTypeIcons } from './types'
|
||||
|
||||
interface FeedbackTabProps {
|
||||
features: Feature[]
|
||||
filteredFeedback: TeacherFeedback[]
|
||||
feedbackFilter: string
|
||||
setFeedbackFilter: (filter: string) => void
|
||||
updateFeedbackStatus: (id: string, status: TeacherFeedback['status']) => void
|
||||
}
|
||||
|
||||
export default function FeedbackTab({
|
||||
features,
|
||||
filteredFeedback,
|
||||
feedbackFilter,
|
||||
setFeedbackFilter,
|
||||
updateFeedbackStatus,
|
||||
}: FeedbackTabProps) {
|
||||
return (
|
||||
<div>
|
||||
{/* Filter */}
|
||||
<div className="flex gap-2 mb-4 flex-wrap">
|
||||
{['all', 'new', 'bug', 'feature_request', 'improvement'].map(filter => (
|
||||
<button
|
||||
key={filter}
|
||||
onClick={() => setFeedbackFilter(filter)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
feedbackFilter === filter ? 'bg-primary-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{filter === 'all' ? 'Alle' :
|
||||
filter === 'new' ? 'Neu' :
|
||||
filter === 'bug' ? 'Bugs' :
|
||||
filter === 'feature_request' ? 'Feature-Requests' : 'Verbesserungen'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Feedback List */}
|
||||
<div className="space-y-3">
|
||||
{filteredFeedback.map(fb => (
|
||||
<div key={fb.id} className="border border-slate-200 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
fb.type === 'bug' ? 'bg-red-100' :
|
||||
fb.type === 'feature_request' ? 'bg-blue-100' :
|
||||
fb.type === 'improvement' ? 'bg-amber-100' :
|
||||
fb.type === 'praise' ? 'bg-pink-100' : 'bg-purple-100'
|
||||
}`}>
|
||||
<svg className={`w-5 h-5 ${
|
||||
fb.type === 'bug' ? 'text-red-600' :
|
||||
fb.type === 'feature_request' ? 'text-blue-600' :
|
||||
fb.type === 'improvement' ? 'text-amber-600' :
|
||||
fb.type === 'praise' ? 'text-pink-600' : 'text-purple-600'
|
||||
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={feedbackTypeIcons[fb.type]} />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-slate-900">{fb.title}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${priorityColors[fb.priority]}`}>
|
||||
{fb.priority}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-2">{fb.description}</p>
|
||||
<div className="flex items-center gap-4 text-xs text-slate-400">
|
||||
<span>{fb.teacher}</span>
|
||||
<span>{fb.date}</span>
|
||||
{fb.relatedFeature && (
|
||||
<span className="text-primary-600">→ {features.find(f => f.id === fb.relatedFeature)?.title}</span>
|
||||
)}
|
||||
</div>
|
||||
{fb.response && (
|
||||
<div className="mt-2 p-2 bg-green-50 rounded text-sm text-green-800">
|
||||
<strong>Antwort:</strong> {fb.response}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<select
|
||||
value={fb.status}
|
||||
onChange={(e) => updateFeedbackStatus(fb.id, e.target.value as TeacherFeedback['status'])}
|
||||
className={`px-2 py-1 rounded text-xs font-medium border-0 cursor-pointer ${
|
||||
fb.status === 'new' ? 'bg-red-100 text-red-700' :
|
||||
fb.status === 'acknowledged' ? 'bg-blue-100 text-blue-700' :
|
||||
fb.status === 'planned' ? 'bg-amber-100 text-amber-700' :
|
||||
fb.status === 'implemented' ? 'bg-green-100 text-green-700' :
|
||||
'bg-slate-100 text-slate-600'
|
||||
}`}
|
||||
>
|
||||
<option value="new">Neu</option>
|
||||
<option value="acknowledged">Gesehen</option>
|
||||
<option value="planned">Geplant</option>
|
||||
<option value="implemented">Umgesetzt</option>
|
||||
<option value="declined">Abgelehnt</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Feedback Button */}
|
||||
<button className="mt-4 w-full py-3 border-2 border-dashed border-slate-300 rounded-xl text-slate-500 hover:border-primary-400 hover:text-primary-600 transition-colors">
|
||||
+ Neues Feedback hinzufuegen
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
84
website/app/admin/companion/_components/RoadmapTab.tsx
Normal file
84
website/app/admin/companion/_components/RoadmapTab.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { roadmapPhases } from './data'
|
||||
|
||||
export default function RoadmapTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{roadmapPhases.map((phase, index) => (
|
||||
<div
|
||||
key={phase.id}
|
||||
className={`border rounded-xl overflow-hidden ${
|
||||
phase.status === 'completed' ? 'border-green-200 bg-green-50/50' :
|
||||
phase.status === 'in_progress' ? 'border-blue-200 bg-blue-50/50' :
|
||||
phase.status === 'planned' ? 'border-amber-200 bg-amber-50/50' :
|
||||
'border-slate-200 bg-slate-50/50'
|
||||
}`}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold ${
|
||||
phase.status === 'completed' ? 'bg-green-500 text-white' :
|
||||
phase.status === 'in_progress' ? 'bg-blue-500 text-white' :
|
||||
phase.status === 'planned' ? 'bg-amber-500 text-white' :
|
||||
'bg-slate-300 text-slate-600'
|
||||
}`}>
|
||||
{phase.status === 'completed' ? '✓' : index + 1}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">{phase.name}</h3>
|
||||
<p className="text-sm text-slate-500">{phase.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
phase.status === 'completed' ? 'bg-green-100 text-green-800' :
|
||||
phase.status === 'in_progress' ? 'bg-blue-100 text-blue-800' :
|
||||
phase.status === 'planned' ? 'bg-amber-100 text-amber-800' :
|
||||
'bg-slate-100 text-slate-600'
|
||||
}`}>
|
||||
{phase.status === 'completed' ? 'Abgeschlossen' :
|
||||
phase.status === 'in_progress' ? 'In Arbeit' :
|
||||
phase.status === 'planned' ? 'Geplant' : 'Zukunft'}
|
||||
</span>
|
||||
{phase.startDate && (
|
||||
<div className="text-xs text-slate-400 mt-1">
|
||||
{phase.startDate} {phase.endDate ? `- ${phase.endDate}` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mt-3 mb-3">
|
||||
<div className="flex justify-between text-xs text-slate-500 mb-1">
|
||||
<span>Fortschritt</span>
|
||||
<span>{phase.progress}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-slate-200 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
phase.status === 'completed' ? 'bg-green-500' :
|
||||
phase.status === 'in_progress' ? 'bg-blue-500' :
|
||||
'bg-amber-500'
|
||||
}`}
|
||||
style={{ width: `${phase.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{phase.features.map((feature, i) => (
|
||||
<span key={i} className="px-2 py-1 bg-white border border-slate-200 rounded text-xs text-slate-600">
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
website/app/admin/companion/_components/StatsOverview.tsx
Normal file
34
website/app/admin/companion/_components/StatsOverview.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
|
||||
interface StatsOverviewProps {
|
||||
phaseStats: { completed: number; total: number; inProgress: number }
|
||||
featureStats: { percentage: number; done: number; total: number }
|
||||
feedbackStats: { newCount: number; total: number; bugs: number; requests: number }
|
||||
}
|
||||
|
||||
export default function StatsOverview({ phaseStats, featureStats, feedbackStats }: StatsOverviewProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500 mb-1">Roadmap-Phasen</div>
|
||||
<div className="text-2xl font-bold text-primary-600">{phaseStats.completed}/{phaseStats.total}</div>
|
||||
<div className="text-xs text-slate-400">{phaseStats.inProgress} in Arbeit</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500 mb-1">Features</div>
|
||||
<div className="text-2xl font-bold text-green-600">{featureStats.percentage}%</div>
|
||||
<div className="text-xs text-slate-400">{featureStats.done}/{featureStats.total} fertig</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500 mb-1">Neues Feedback</div>
|
||||
<div className="text-2xl font-bold text-amber-600">{feedbackStats.newCount}</div>
|
||||
<div className="text-xs text-slate-400">{feedbackStats.total} gesamt</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500 mb-1">Offene Bugs</div>
|
||||
<div className="text-2xl font-bold text-red-600">{feedbackStats.bugs}</div>
|
||||
<div className="text-xs text-slate-400">{feedbackStats.requests} Feature-Requests</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
404
website/app/admin/companion/_components/data.ts
Normal file
404
website/app/admin/companion/_components/data.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import type { RoadmapPhase, Feature, TeacherFeedback } from './types'
|
||||
|
||||
// ==================== ROADMAP DATA ====================
|
||||
|
||||
export const roadmapPhases: RoadmapPhase[] = [
|
||||
{
|
||||
id: 'phase-1',
|
||||
name: 'Phase 1: Core Engine',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-10',
|
||||
endDate: '2026-01-14',
|
||||
description: 'Grundlegende State Machine und API-Endpunkte',
|
||||
features: [
|
||||
'Finite State Machine (5 Phasen)',
|
||||
'Timer Service mit Countdown',
|
||||
'Phasenspezifische Suggestions',
|
||||
'REST API Endpoints',
|
||||
'In-Memory Session Storage',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-2',
|
||||
name: 'Phase 2: Frontend Integration',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-14',
|
||||
endDate: '2026-01-14',
|
||||
description: 'Integration in das Studio-Frontend',
|
||||
features: [
|
||||
'Lesson-Modus im Companion',
|
||||
'Timer-Anzeige mit Warning/Overtime',
|
||||
'Phasen-Timeline Visualisierung',
|
||||
'Suggestions pro Phase',
|
||||
'Session Start/End UI',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-2b',
|
||||
name: 'Phase 2b: Teacher UX Optimierung',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'Forschungsbasierte UX-Verbesserungen fuer intuitive Lehrer-Bedienung',
|
||||
features: [
|
||||
'Visual Pie Timer (Kreis statt Zahlen)',
|
||||
'Phasen-Farbschema (Blau→Orange→Gruen→Lila→Grau)',
|
||||
'Quick Actions Bar (+5min, Pause, Skip)',
|
||||
'Tablet-First Responsive Design',
|
||||
'Large Touch Targets (48x48px min)',
|
||||
'High Contrast fuer Beamer',
|
||||
'Audio Cues (sanfte Toene)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-3',
|
||||
name: 'Phase 3: Persistenz',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'Datenbank-Anbindung und Session-Persistenz',
|
||||
features: [
|
||||
'PostgreSQL Integration (done)',
|
||||
'SQLAlchemy Models (done)',
|
||||
'Session Repository (done)',
|
||||
'Alembic Migration Scripts (done)',
|
||||
'Session History API (done)',
|
||||
'Hybrid Storage (Memory+DB) (done)',
|
||||
'Lehrer-spezifische Settings (backlog)',
|
||||
'Keycloak Auth Integration (backlog)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-4',
|
||||
name: 'Phase 4: Content Integration',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'Verknuepfung mit Learning Units',
|
||||
features: [
|
||||
'Lesson Templates (done)',
|
||||
'Fachspezifische Unit-Vorschlaege (done)',
|
||||
'Hausaufgaben-Tracker (done)',
|
||||
'Material-Verknuepfung (done)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-5',
|
||||
name: 'Phase 5: Analytics',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'Unterrichtsanalyse und Optimierung (ohne wertende Metriken)',
|
||||
features: [
|
||||
'Phasen-Dauer Statistiken (done)',
|
||||
'Overtime-Analyse (done)',
|
||||
'Post-Lesson Reflection API (done)',
|
||||
'Lehrer-Dashboard UI (done)',
|
||||
'HTML/PDF Export (done)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-6',
|
||||
name: 'Phase 6: Real-time',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'WebSocket-basierte Echtzeit-Updates',
|
||||
features: [
|
||||
'WebSocket API Endpoint (done)',
|
||||
'Connection Manager mit Multi-Device Support (done)',
|
||||
'Timer Broadcast Loop (1-Sekunden-Genauigkeit) (done)',
|
||||
'Client-seitiger WebSocket Handler (done)',
|
||||
'Automatischer Reconnect mit Fallback zu Polling (done)',
|
||||
'Phase Change & Session End Notifications (done)',
|
||||
'Connection Status Indicator (done)',
|
||||
'WebSocket Tests (done)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-7',
|
||||
name: 'Phase 7: Erweiterungen',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
startDate: '2026-01-15',
|
||||
endDate: '2026-01-15',
|
||||
description: 'Lehrer-Feedback und Authentifizierung',
|
||||
features: [
|
||||
'Teacher Feedback API (done)',
|
||||
'Feedback Modal im Lehrer-Frontend (done)',
|
||||
'Keycloak Auth Integration (done)',
|
||||
'Optional Auth Dependency (done)',
|
||||
'Feedback DB Model & Migration (done)',
|
||||
'Feedback Repository (done)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-8',
|
||||
name: 'Phase 8: Schuljahres-Begleiter',
|
||||
status: 'in_progress',
|
||||
progress: 85,
|
||||
startDate: '2026-01-15',
|
||||
description: '2-Schichten-Modell: Makro-Phasen (Schuljahr) + Mikro-Engine (Events/Routinen)',
|
||||
features: [
|
||||
'Kontext-Datenmodell (TeacherContextDB, SchoolyearEventDB, RecurringRoutineDB) (done)',
|
||||
'Alembic Migration 007 (done)',
|
||||
'GET /v1/context Endpoint (done)',
|
||||
'Events & Routinen CRUD-APIs (done)',
|
||||
'Bundeslaender & Schularten Stammdaten (done)',
|
||||
'Antizipations-Engine mit 12 Regeln (done)',
|
||||
'GET /v1/suggestions Endpoint (done)',
|
||||
'Dynamische Sidebar /v1/sidebar (done)',
|
||||
'Schuljahres-Pfad /v1/path (done)',
|
||||
'Frontend ContextBar Component (done)',
|
||||
'Frontend Dynamic Sidebar (done)',
|
||||
'Frontend PathPanel Component (done)',
|
||||
'Main Content Actions Integration (done)',
|
||||
'Onboarding-Flow (geplant)',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'phase-9',
|
||||
name: 'Phase 9: Zukunft',
|
||||
status: 'future',
|
||||
progress: 0,
|
||||
description: 'Weitere geplante Features',
|
||||
features: [
|
||||
'Push Notifications',
|
||||
'Dark Mode',
|
||||
'Lesson Templates Library (erweitert)',
|
||||
'Multi-Language Support',
|
||||
'KI-Assistenz fuer Unterrichtsplanung',
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ==================== FEATURES DATA ====================
|
||||
|
||||
export const initialFeatures: Feature[] = [
|
||||
// Phase 1 - Done
|
||||
{ id: 'f1', title: 'LessonPhase Enum', description: '7 Zustaende: not_started, 5 Phasen, ended', priority: 'critical', status: 'done', phase: 'phase-1', effort: 'small' },
|
||||
{ id: 'f2', title: 'LessonSession Dataclass', description: 'Session-Datenmodell mit History', priority: 'critical', status: 'done', phase: 'phase-1', effort: 'medium' },
|
||||
{ id: 'f3', title: 'FSM Transitions', description: 'Erlaubte Phasen-Uebergaenge', priority: 'critical', status: 'done', phase: 'phase-1', effort: 'medium' },
|
||||
{ id: 'f4', title: 'PhaseTimer Service', description: 'Countdown, Warning, Overtime', priority: 'high', status: 'done', phase: 'phase-1', effort: 'medium' },
|
||||
{ id: 'f5', title: 'SuggestionEngine', description: 'Phasenspezifische Aktivitaets-Vorschlaege', priority: 'high', status: 'done', phase: 'phase-1', effort: 'large' },
|
||||
{ id: 'f6', title: 'REST API Endpoints', description: '10 Endpoints unter /api/classroom', priority: 'critical', status: 'done', phase: 'phase-1', effort: 'large' },
|
||||
|
||||
// Phase 2 - Done
|
||||
{ id: 'f7', title: 'Mode Toggle (3 Modi)', description: 'Begleiter, Stunde, Klassisch', priority: 'high', status: 'done', phase: 'phase-2', effort: 'small' },
|
||||
{ id: 'f8', title: 'Timer-Display', description: 'Grosser Countdown mit Styling', priority: 'high', status: 'done', phase: 'phase-2', effort: 'medium' },
|
||||
{ id: 'f9', title: 'Phasen-Timeline', description: 'Horizontale 5-Phasen-Anzeige', priority: 'high', status: 'done', phase: 'phase-2', effort: 'medium' },
|
||||
{ id: 'f10', title: 'Control Buttons', description: 'Naechste Phase, Beenden', priority: 'high', status: 'done', phase: 'phase-2', effort: 'small' },
|
||||
{ id: 'f11', title: 'Suggestions Cards', description: 'Aktivitaets-Vorschlaege UI', priority: 'medium', status: 'done', phase: 'phase-2', effort: 'medium' },
|
||||
{ id: 'f12', title: 'Session Start Form', description: 'Klasse, Fach, Thema auswaehlen', priority: 'high', status: 'done', phase: 'phase-2', effort: 'small' },
|
||||
|
||||
// Phase 3 - In Progress (Persistenz)
|
||||
{ id: 'f13', title: 'PostgreSQL Models', description: 'SQLAlchemy Models fuer Sessions (LessonSessionDB, PhaseHistoryDB, TeacherSettingsDB)', priority: 'critical', status: 'done', phase: 'phase-3', effort: 'medium' },
|
||||
{ id: 'f14', title: 'Session Repository', description: 'CRUD Operationen fuer Sessions (SessionRepository, TeacherSettingsRepository)', priority: 'critical', status: 'done', phase: 'phase-3', effort: 'medium' },
|
||||
{ id: 'f15', title: 'Migration Scripts', description: 'Alembic Migrationen fuer Classroom Tables', priority: 'high', status: 'done', phase: 'phase-3', effort: 'small' },
|
||||
{ id: 'f16', title: 'Teacher Settings', description: 'Individuelle Phasen-Dauern speichern (API + Settings Modal UI)', priority: 'medium', status: 'done', phase: 'phase-7', effort: 'medium' },
|
||||
{ id: 'f17', title: 'Session History API', description: 'GET /history/{teacher_id} mit Pagination', priority: 'medium', status: 'done', phase: 'phase-3', effort: 'small' },
|
||||
|
||||
// Phase 4 - In Progress (Content)
|
||||
{ id: 'f18', title: 'Unit-Vorschlaege', description: 'Fachspezifische Learning Units pro Phase (Mathe, Deutsch, Englisch, Bio, Physik, Informatik)', priority: 'high', status: 'done', phase: 'phase-4', effort: 'large' },
|
||||
{ id: 'f19', title: 'Material-Verknuepfung', description: 'Dokumente an Phasen anhaengen (PhaseMaterial Model, Repository, 8 API-Endpoints, Frontend-Integration)', priority: 'medium', status: 'done', phase: 'phase-4', effort: 'medium' },
|
||||
{ id: 'f20', title: 'Hausaufgaben-Tracker', description: 'CRUD API fuer Hausaufgaben mit Status und Faelligkeit', priority: 'medium', status: 'done', phase: 'phase-4', effort: 'medium' },
|
||||
|
||||
// ==================== NEUE UX FEATURES (aus Research) ====================
|
||||
|
||||
// P0 - KRITISCH (UX Research basiert)
|
||||
{ id: 'f21', title: 'Visual Pie Timer', description: 'Kreisfoermiger Countdown mit Farbverlauf (Gruen→Gelb→Rot) - reduziert Stress laut Forschung', priority: 'critical', status: 'done', phase: 'phase-2', effort: 'large' },
|
||||
{ id: 'f22', title: 'Database Persistence', description: 'PostgreSQL statt In-Memory - Sessions ueberleben Neustart (Hybrid Storage)', priority: 'critical', status: 'done', phase: 'phase-3', effort: 'large' },
|
||||
{ id: 'f23', title: 'Teacher Auth Integration', description: 'Keycloak-Anbindung mit optionalem Demo-User Fallback', priority: 'critical', status: 'done', phase: 'phase-7', effort: 'large' },
|
||||
{ id: 'f24', title: 'Tablet-First Responsive', description: 'Optimiert fuer 10" Touch-Screens, Einhand-Bedienung im Klassenraum', priority: 'critical', status: 'done', phase: 'phase-2b', effort: 'medium' },
|
||||
|
||||
// P1 - WICHTIG (UX Research basiert)
|
||||
{ id: 'f25', title: 'Phasen-Farbschema', description: 'Forschungsbasierte Farben: Blau(Einstieg), Orange(Erarbeitung), Gruen(Sicherung), Lila(Transfer), Grau(Reflexion)', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'medium' },
|
||||
{ id: 'f26', title: 'Quick Actions Bar', description: '+5min, Pause, Skip-Phase als One-Click Touch-Buttons (min 56px)', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'medium' },
|
||||
{ id: 'f27', title: 'Pause Timer API', description: 'POST /sessions/{id}/pause - Timer anhalten bei Stoerungen', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f28', title: 'Extend Phase API', description: 'POST /sessions/{id}/extend?minutes=5 - Phase verlaengern', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f29', title: 'Non-Intrusive Suggestions', description: 'Vorschlaege in dedizierter Sektion, nicht als stoerende Popups', priority: 'high', status: 'done', phase: 'phase-2', effort: 'medium' },
|
||||
{ id: 'f30', title: 'WebSocket Real-Time Timer', description: 'Sub-Sekunden Genauigkeit statt 5s Polling', priority: 'high', status: 'done', phase: 'phase-6', effort: 'large' },
|
||||
{ id: 'f31', title: 'Mobile Breakpoints', description: 'Responsive Design fuer 600px, 900px, 1200px Screens', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'medium' },
|
||||
{ id: 'f32', title: 'Large Touch Targets', description: 'Alle Buttons min 48x48px fuer sichere Touch-Bedienung', priority: 'high', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
|
||||
// P2 - NICE-TO-HAVE (UX Research basiert)
|
||||
{ id: 'f33', title: 'Audio Cues', description: 'Sanfte Toene bei Phasenwechsel und Warnungen (Taste A zum Toggle)', priority: 'medium', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f34', title: 'Keyboard Shortcuts', description: 'Space=Pause, N=Next Phase, E=Extend, H=High Contrast - fuer Desktop-Nutzung', priority: 'medium', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f35', title: 'Offline Timer Fallback', description: 'Client-seitige Timer-Berechnung bei Verbindungsverlust', priority: 'medium', status: 'done', phase: 'phase-2b', effort: 'medium' },
|
||||
{ id: 'f36', title: 'Post-Lesson Analytics', description: 'Phasen-Dauer Statistiken ohne wertende Metriken (SessionSummary, TeacherAnalytics, 4 API-Endpoints)', priority: 'medium', status: 'done', phase: 'phase-5', effort: 'large' },
|
||||
{ id: 'f37', title: 'Lesson Templates', description: '5 System-Templates + eigene Vorlagen erstellen/speichern', priority: 'medium', status: 'done', phase: 'phase-4', effort: 'medium' },
|
||||
{ id: 'f38', title: 'ARIA Labels', description: 'Screen-Reader Unterstuetzung fuer Barrierefreiheit', priority: 'medium', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f39', title: 'High Contrast Mode', description: 'Erhoehter Kontrast fuer Beamer/Projektor (Taste H)', priority: 'medium', status: 'done', phase: 'phase-2b', effort: 'small' },
|
||||
{ id: 'f40', title: 'Export to PDF', description: 'Stundenprotokoll als druckbares HTML mit Browser-PDF-Export (Strg+P)', priority: 'low', status: 'done', phase: 'phase-5', effort: 'medium' },
|
||||
{ id: 'f41', title: 'Overtime-Analyse', description: 'Phase-by-Phase Overtime-Statistiken und Trends', priority: 'medium', status: 'done', phase: 'phase-5', effort: 'medium' },
|
||||
{ id: 'f42', title: 'Post-Lesson Reflection', description: 'Reflexions-Notizen nach Stundenende (CRUD API, DB-Model)', priority: 'medium', status: 'done', phase: 'phase-5', effort: 'medium' },
|
||||
{ id: 'f43', title: 'Phase Duration Trends', description: 'Visualisierung der Phasendauer-Entwicklung', priority: 'medium', status: 'done', phase: 'phase-5', effort: 'small' },
|
||||
{ id: 'f44', title: 'Analytics Dashboard UI', description: 'Lehrer-Frontend fuer Analytics-Anzeige (Phasen-Bars, Overtime, Reflection)', priority: 'high', status: 'done', phase: 'phase-5', effort: 'medium' },
|
||||
|
||||
// Phase 6 - Real-time (WebSocket)
|
||||
{ id: 'f45', title: 'WebSocket API Endpoint', description: 'Real-time Verbindung unter /api/classroom/ws/{session_id}', priority: 'critical', status: 'done', phase: 'phase-6', effort: 'large' },
|
||||
{ id: 'f46', title: 'Connection Manager', description: 'Multi-Device Support mit Session-basierter Verbindungsverwaltung', priority: 'critical', status: 'done', phase: 'phase-6', effort: 'medium' },
|
||||
{ id: 'f47', title: 'Timer Broadcast Loop', description: 'Hintergrund-Task sendet Timer-Updates jede Sekunde an alle Clients', priority: 'critical', status: 'done', phase: 'phase-6', effort: 'medium' },
|
||||
{ id: 'f48', title: 'Client WebSocket Handler', description: 'Frontend-Integration mit automatischem Reconnect und Fallback zu Polling', priority: 'high', status: 'done', phase: 'phase-6', effort: 'large' },
|
||||
{ id: 'f49', title: 'Phase Change Notifications', description: 'Echtzeit-Benachrichtigung bei Phasenwechsel an alle Devices', priority: 'high', status: 'done', phase: 'phase-6', effort: 'small' },
|
||||
{ id: 'f50', title: 'Session End Notifications', description: 'Automatische Benachrichtigung bei Stundenende', priority: 'high', status: 'done', phase: 'phase-6', effort: 'small' },
|
||||
{ id: 'f51', title: 'Connection Status Indicator', description: 'UI-Element zeigt Live/Polling/Offline Status', priority: 'medium', status: 'done', phase: 'phase-6', effort: 'small' },
|
||||
{ id: 'f52', title: 'WebSocket Status API', description: 'GET /ws/status zeigt aktive Sessions und Verbindungszahlen', priority: 'low', status: 'done', phase: 'phase-6', effort: 'small' },
|
||||
|
||||
// Phase 7 - Erweiterungen (Auth & Feedback)
|
||||
{ id: 'f53', title: 'Teacher Feedback API', description: 'POST/GET /feedback Endpoints fuer Bug-Reports und Feature-Requests', priority: 'high', status: 'done', phase: 'phase-7', effort: 'large' },
|
||||
{ id: 'f54', title: 'Feedback Modal UI', description: 'Floating Action Button und Modal im Lehrer-Frontend', priority: 'high', status: 'done', phase: 'phase-7', effort: 'medium' },
|
||||
{ id: 'f55', title: 'Feedback DB Model', description: 'TeacherFeedbackDB SQLAlchemy Model mit Alembic Migration', priority: 'high', status: 'done', phase: 'phase-7', effort: 'medium' },
|
||||
{ id: 'f56', title: 'Feedback Repository', description: 'CRUD-Operationen fuer Feedback mit Status-Management', priority: 'high', status: 'done', phase: 'phase-7', effort: 'medium' },
|
||||
{ id: 'f57', title: 'Keycloak Auth Integration', description: 'Optional Auth Dependency mit Demo-User Fallback', priority: 'critical', status: 'done', phase: 'phase-7', effort: 'medium' },
|
||||
{ id: 'f58', title: 'Feedback Stats API', description: 'GET /feedback/stats fuer Dashboard-Statistiken', priority: 'medium', status: 'done', phase: 'phase-7', effort: 'small' },
|
||||
|
||||
// Phase 8 - Schuljahres-Begleiter (2-Schichten-Modell)
|
||||
{ id: 'f59', title: 'TeacherContextDB Model', description: 'Makro-Kontext pro Lehrer (Bundesland, Schulart, Schuljahr, Phase)', priority: 'critical', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f60', title: 'SchoolyearEventDB Model', description: 'Events (Klausuren, Elternabende, Klassenfahrten, etc.)', priority: 'critical', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f61', title: 'RecurringRoutineDB Model', description: 'Wiederkehrende Routinen (Konferenzen, Sprechstunden, etc.)', priority: 'critical', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f62', title: 'Alembic Migration 007', description: 'DB-Migration fuer teacher_contexts, schoolyear_events, recurring_routines', priority: 'critical', status: 'done', phase: 'phase-8', effort: 'small' },
|
||||
{ id: 'f63', title: 'GET /v1/context Endpoint', description: 'Makro-Kontext abrufen (Schuljahr, Woche, Phase, Flags)', priority: 'critical', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f64', title: 'PUT /v1/context Endpoint', description: 'Kontext aktualisieren (Bundesland, Schulart, Schuljahr)', priority: 'high', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f65', title: 'Events CRUD-API', description: 'GET/POST/DELETE /v1/events mit Status und Vorbereitung', priority: 'high', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f66', title: 'Routines CRUD-API', description: 'GET/POST/DELETE /v1/routines mit Wiederholungsmustern', priority: 'high', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f67', title: 'Stammdaten-APIs', description: '/v1/federal-states, /v1/school-types, /v1/macro-phases, /v1/event-types', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'small' },
|
||||
{ id: 'f68', title: 'Context Repositories', description: 'TeacherContextRepository, SchoolyearEventRepository, RecurringRoutineRepository', priority: 'high', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f69', title: 'Antizipations-Engine', description: 'Signal-Collector + Regel-Engine (12 Regeln) fuer proaktive Vorschlaege', priority: 'high', status: 'done', phase: 'phase-8', effort: 'epic' },
|
||||
{ id: 'f70', title: 'GET /v1/suggestions Endpoint', description: 'Kontextbasierte Vorschlaege mit active_contexts[]', priority: 'high', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f71', title: 'GET /v1/sidebar Endpoint', description: 'Dynamisches Sidebar-Model (Companion vs Classic Mode)', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f72', title: 'GET /v1/path Endpoint', description: 'Schuljahres-Meilensteine mit Status (DONE, CURRENT, UPCOMING)', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f73', title: 'ContextBar Component', description: 'Schuljahr, Woche, Bundesland Anzeige im Header', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f74', title: 'Begleiter-Sidebar', description: 'Top 5 relevante Module + Alle Module + Suche', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f75', title: 'PathPanel Component', description: 'Vertikaler Schuljahres-Pfad mit "Du bist hier" Markierung', priority: 'medium', status: 'done', phase: 'phase-8', effort: 'medium' },
|
||||
{ id: 'f76', title: 'Onboarding-Flow', description: 'Bundesland, Schulart, Schuljahres-Start, erste Klassen', priority: 'high', status: 'backlog', phase: 'phase-8', effort: 'large' },
|
||||
{ id: 'f77', title: 'Complete Onboarding API', description: 'POST /v1/context/complete-onboarding zum Abschliessen', priority: 'high', status: 'done', phase: 'phase-8', effort: 'small' },
|
||||
]
|
||||
|
||||
// ==================== FEEDBACK DATA ====================
|
||||
|
||||
export const initialFeedback: TeacherFeedback[] = [
|
||||
{
|
||||
id: 'fb1',
|
||||
teacher: 'Frau Mueller',
|
||||
date: '2026-01-14',
|
||||
type: 'feature_request',
|
||||
priority: 'high',
|
||||
status: 'implemented',
|
||||
title: 'Individuelle Phasen-Dauern',
|
||||
description: 'Ich moechte die Dauern der einzelnen Phasen selbst festlegen koennen, je nach Unterrichtseinheit.',
|
||||
relatedFeature: 'f16',
|
||||
response: 'In Phase 7 implementiert: Einstellungen-Button oeffnet Modal zur Konfiguration der Phasendauern.',
|
||||
},
|
||||
{
|
||||
id: 'fb2',
|
||||
teacher: 'Herr Schmidt',
|
||||
date: '2026-01-14',
|
||||
type: 'improvement',
|
||||
priority: 'medium',
|
||||
status: 'implemented',
|
||||
title: 'Akustisches Signal bei Phasen-Ende',
|
||||
description: 'Ein kurzer Ton wuerde helfen, das Ende einer Phase nicht zu verpassen.',
|
||||
relatedFeature: 'f33',
|
||||
response: 'Audio Cues wurden in Phase 2b implementiert - sanfte Toene statt harter Alarme. Taste A zum Toggle.',
|
||||
},
|
||||
{
|
||||
id: 'fb3',
|
||||
teacher: 'Frau Wagner',
|
||||
date: '2026-01-15',
|
||||
type: 'praise',
|
||||
priority: 'low',
|
||||
status: 'acknowledged',
|
||||
title: 'Super einfache Bedienung!',
|
||||
description: 'Die Stunden-Steuerung ist sehr intuitiv. Meine erste Stunde damit hat super geklappt.',
|
||||
},
|
||||
{
|
||||
id: 'fb4',
|
||||
teacher: 'Herr Becker',
|
||||
date: '2026-01-15',
|
||||
type: 'bug',
|
||||
priority: 'high',
|
||||
status: 'implemented',
|
||||
title: 'Timer stoppt bei Browser-Tab-Wechsel',
|
||||
description: 'Wenn ich den Browser-Tab wechsle und zurueckkomme, zeigt der Timer manchmal falsche Werte.',
|
||||
relatedFeature: 'f35',
|
||||
response: 'Offline Timer Fallback in Phase 2b implementiert + WebSocket Real-time in Phase 6.',
|
||||
},
|
||||
{
|
||||
id: 'fb5',
|
||||
teacher: 'Frau Klein',
|
||||
date: '2026-01-15',
|
||||
type: 'feature_request',
|
||||
priority: 'critical',
|
||||
status: 'implemented',
|
||||
title: 'Pause-Funktion',
|
||||
description: 'Manchmal muss ich die Stunde kurz unterbrechen (Stoerung, Durchsage). Eine Pause-Funktion waere super.',
|
||||
relatedFeature: 'f27',
|
||||
response: 'Pause Timer API und Quick Actions Bar wurden in Phase 2b implementiert. Tastenkuerzel: Leertaste.',
|
||||
},
|
||||
{
|
||||
id: 'fb6',
|
||||
teacher: 'Herr Hoffmann',
|
||||
date: '2026-01-15',
|
||||
type: 'feature_request',
|
||||
priority: 'high',
|
||||
status: 'implemented',
|
||||
title: 'Visueller Timer statt Zahlen',
|
||||
description: 'Der numerische Countdown ist manchmal stressig. Ein visueller Kreis-Timer waere entspannter.',
|
||||
relatedFeature: 'f21',
|
||||
response: 'Visual Pie Timer mit Farbverlauf (Gruen→Gelb→Rot) wurde in Phase 2b implementiert.',
|
||||
},
|
||||
{
|
||||
id: 'fb7',
|
||||
teacher: 'Frau Richter',
|
||||
date: '2026-01-15',
|
||||
type: 'feature_request',
|
||||
priority: 'high',
|
||||
status: 'implemented',
|
||||
title: 'Tablet-Nutzung im Klassenraum',
|
||||
description: 'Ich laufe waehrend des Unterrichts herum. Die Anzeige muesste auch auf meinem iPad gut funktionieren.',
|
||||
relatedFeature: 'f24',
|
||||
response: 'Tablet-First Responsive Design wurde in Phase 2b implementiert. Touch-Targets min 48x48px.',
|
||||
},
|
||||
{
|
||||
id: 'fb8',
|
||||
teacher: 'Herr Weber',
|
||||
date: '2026-01-15',
|
||||
type: 'improvement',
|
||||
priority: 'medium',
|
||||
status: 'implemented',
|
||||
title: '+5 Minuten Button',
|
||||
description: 'Manchmal brauche ich einfach nur 5 Minuten mehr fuer eine Phase. Ein Schnell-Button waere praktisch.',
|
||||
relatedFeature: 'f28',
|
||||
response: 'In Quick Actions Bar integriert. Tastenkuerzel: E.',
|
||||
},
|
||||
{
|
||||
id: 'fb9',
|
||||
teacher: 'Frau Schneider',
|
||||
date: '2026-01-15',
|
||||
type: 'praise',
|
||||
priority: 'low',
|
||||
status: 'acknowledged',
|
||||
title: 'Phasen-Vorschlaege sind hilfreich',
|
||||
description: 'Die Aktivitaets-Vorschlaege pro Phase geben mir gute Ideen. Weiter so!',
|
||||
},
|
||||
{
|
||||
id: 'fb10',
|
||||
teacher: 'Herr Meier',
|
||||
date: '2026-01-15',
|
||||
type: 'feature_request',
|
||||
priority: 'medium',
|
||||
status: 'implemented',
|
||||
title: 'Stundenvorlage speichern',
|
||||
description: 'Fuer Mathe-Stunden nutze ich immer die gleiche Phasen-Aufteilung. Waere cool, das als Template zu speichern.',
|
||||
relatedFeature: 'f37',
|
||||
response: 'Lesson Templates wurden in Phase 4 implementiert. 5 System-Templates + eigene Vorlagen moeglich.',
|
||||
},
|
||||
]
|
||||
119
website/app/admin/companion/_components/system-info.ts
Normal file
119
website/app/admin/companion/_components/system-info.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// ==================== SYSTEM INFO CONFIG ====================
|
||||
|
||||
export const companionSystemInfo = {
|
||||
title: 'Companion Module System Info',
|
||||
description: 'Technische Details zur Classroom State Machine',
|
||||
version: '1.1.0',
|
||||
architecture: {
|
||||
layers: [
|
||||
{
|
||||
title: 'Frontend Layer',
|
||||
components: [
|
||||
'companion.py (Lesson-Modus UI)',
|
||||
'Mode Toggle (Begleiter/Stunde/Klassisch)',
|
||||
'Timer Display Component',
|
||||
'Phase Timeline Component',
|
||||
'Suggestions Cards',
|
||||
'Material Design Icons (CDN)',
|
||||
],
|
||||
color: 'bg-blue-50',
|
||||
},
|
||||
{
|
||||
title: 'API Layer',
|
||||
components: [
|
||||
'classroom_api.py (FastAPI Router)',
|
||||
'POST /sessions - Session erstellen',
|
||||
'POST /sessions/{id}/start - Stunde starten',
|
||||
'POST /sessions/{id}/next-phase - Naechste Phase',
|
||||
'POST /sessions/{id}/pause - Timer pausieren',
|
||||
'POST /sessions/{id}/extend - Phase verlaengern',
|
||||
'GET /sessions/{id}/timer - Timer Status',
|
||||
'GET /sessions/{id}/suggestions - Vorschlaege',
|
||||
'GET /history/{teacher_id} - Session History',
|
||||
'GET /health - Health Check mit DB-Status',
|
||||
'GET/PUT /v1/context - Schuljahres-Kontext',
|
||||
'GET/POST/DELETE /v1/events - Events CRUD',
|
||||
'GET/POST/DELETE /v1/routines - Routinen CRUD',
|
||||
'GET /v1/federal-states, /v1/school-types, etc.',
|
||||
],
|
||||
color: 'bg-green-50',
|
||||
},
|
||||
{
|
||||
title: 'Engine Layer',
|
||||
components: [
|
||||
'classroom_engine/ Package',
|
||||
'models.py - LessonPhase, LessonSession',
|
||||
'fsm.py - LessonStateMachine',
|
||||
'timer.py - PhaseTimer',
|
||||
'suggestions.py - SuggestionEngine',
|
||||
'context_models.py - TeacherContextDB, SchoolyearEventDB, RecurringRoutineDB',
|
||||
'antizipation.py - AntizipationsEngine (geplant)',
|
||||
],
|
||||
color: 'bg-amber-50',
|
||||
},
|
||||
{
|
||||
title: 'Storage Layer',
|
||||
components: [
|
||||
'Hybrid Storage (Memory + PostgreSQL)',
|
||||
'SessionRepository (CRUD)',
|
||||
'TeacherSettingsRepository',
|
||||
'TeacherContextRepository (Phase 8)',
|
||||
'SchoolyearEventRepository (Phase 8)',
|
||||
'RecurringRoutineRepository (Phase 8)',
|
||||
'Alembic Migrations (007: Phase 8 Tables)',
|
||||
'Session History API',
|
||||
],
|
||||
color: 'bg-purple-50',
|
||||
},
|
||||
],
|
||||
},
|
||||
features: [
|
||||
{ name: '5-Phasen-Modell', status: 'active' as const, description: 'Einstieg, Erarbeitung, Sicherung, Transfer, Reflexion' },
|
||||
{ name: 'Timer mit Warning', status: 'active' as const, description: '2 Minuten Warnung vor Phasen-Ende' },
|
||||
{ name: 'Overtime Detection', status: 'active' as const, description: 'Anzeige wenn Phase ueberzogen wird' },
|
||||
{ name: 'Phasen-Suggestions', status: 'active' as const, description: '3-6 Aktivitaets-Vorschlaege pro Phase' },
|
||||
{ name: 'Visual Pie Timer', status: 'active' as const, description: 'Kreisfoermiger Countdown mit Farbverlauf' },
|
||||
{ name: 'Quick Actions Bar', status: 'active' as const, description: '+5min, Pause, Skip Buttons' },
|
||||
{ name: 'Tablet-First Design', status: 'active' as const, description: 'Touch-optimiert fuer Tablets' },
|
||||
{ name: 'Phasen-Farbschema', status: 'active' as const, description: 'Blau→Orange→Gruen→Lila→Grau' },
|
||||
{ name: 'Keyboard Shortcuts', status: 'active' as const, description: 'Space=Pause, N=Next, E=Extend, H=Contrast' },
|
||||
{ name: 'Audio Cues', status: 'active' as const, description: 'Sanfte Toene bei Phasenwechsel' },
|
||||
{ name: 'Offline Timer', status: 'active' as const, description: 'Client-seitige Fallback bei Verbindungsverlust' },
|
||||
{ name: 'DB Persistenz', status: 'active' as const, description: 'PostgreSQL Hybrid Storage' },
|
||||
{ name: 'Session History', status: 'active' as const, description: 'GET /history/{teacher_id} API' },
|
||||
{ name: 'Alembic Migrations', status: 'active' as const, description: 'Versionierte DB-Schema-Aenderungen' },
|
||||
{ name: 'Teacher Auth', status: 'active' as const, description: 'Keycloak Integration mit Optional Fallback (Phase 7)' },
|
||||
{ name: 'WebSocket Real-time', status: 'active' as const, description: 'Echtzeit-Updates mit 1-Sekunden-Genauigkeit (Phase 6)' },
|
||||
{ name: 'Schuljahres-Kontext', status: 'active' as const, description: 'Makro-Phasen, Bundesland, Schulart (Phase 8)' },
|
||||
{ name: 'Events & Routinen', status: 'active' as const, description: 'Klausuren, Konferenzen, Elternabende (Phase 8)' },
|
||||
{ name: 'Antizipations-Engine', status: 'active' as const, description: '12 Regeln fuer proaktive Vorschlaege (Phase 8)' },
|
||||
{ name: 'Dynamische Sidebar', status: 'active' as const, description: 'Top 5 relevante Module + Alle Module (Phase 8)' },
|
||||
{ name: 'Schuljahres-Pfad', status: 'active' as const, description: '7 Meilensteine mit Fortschrittsanzeige (Phase 8)' },
|
||||
],
|
||||
roadmap: [
|
||||
{ phase: 'Phase 1: Core Engine', priority: 'high' as const, items: ['FSM', 'Timer', 'Suggestions', 'API'] },
|
||||
{ phase: 'Phase 2: Frontend', priority: 'high' as const, items: ['Lesson-Modus UI', 'Timer Display', 'Timeline'] },
|
||||
{ phase: 'Phase 2b: UX Optimierung', priority: 'high' as const, items: ['Visual Timer', 'Farbschema', 'Tablet-First', 'Quick Actions'] },
|
||||
{ phase: 'Phase 3: Persistenz', priority: 'high' as const, items: ['PostgreSQL', 'Keycloak Auth', 'Session History'] },
|
||||
{ phase: 'Phase 4: Content', priority: 'medium' as const, items: ['Unit-Vorschlaege', 'Templates', 'Hausaufgaben'] },
|
||||
{ phase: 'Phase 5: Analytics', priority: 'medium' as const, items: ['Statistiken (ohne Bewertung)', 'PDF Export'] },
|
||||
{ phase: 'Phase 6: Real-time', priority: 'low' as const, items: ['WebSocket', 'Offline Fallback', 'Multi-Device'] },
|
||||
],
|
||||
technicalDetails: [
|
||||
{ component: 'Backend', technology: 'Python FastAPI', version: '0.123+', description: 'Async REST API' },
|
||||
{ component: 'State Machine', technology: 'Python Enum + Dataclass', description: 'Finite State Machine Pattern' },
|
||||
{ component: 'Timer', technology: 'datetime.utcnow()', description: 'Server-side Time Calculation' },
|
||||
{ component: 'Frontend', technology: 'Vanilla JavaScript ES6+', description: 'In companion.py eingebettet' },
|
||||
{ component: 'Icons', technology: 'Material Design Icons', description: 'Via Google Fonts CDN (Apache-2.0)' },
|
||||
{ component: 'WebSocket', technology: 'FastAPI WebSocket', description: 'Echtzeit-Updates mit 1-Sekunden-Genauigkeit + Polling Fallback' },
|
||||
{ component: 'Database', technology: 'PostgreSQL + SQLAlchemy 2.0', description: 'Hybrid Storage (Memory + DB)' },
|
||||
{ component: 'Migrations', technology: 'Alembic 1.14', description: 'Versionierte Schema-Migrationen' },
|
||||
],
|
||||
privacyNotes: [
|
||||
'Keine Schueler-Daten werden gespeichert',
|
||||
'Session-Daten sind nur waehrend der Stunde verfuegbar',
|
||||
'Lehrer-ID wird fuer Session-Zuordnung verwendet',
|
||||
'Keine Tracking-Cookies oder externe Services',
|
||||
'Analytics ohne bewertende Metriken (keine "70% Redezeit"-Anzeigen)',
|
||||
],
|
||||
}
|
||||
62
website/app/admin/companion/_components/types.ts
Normal file
62
website/app/admin/companion/_components/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// ==================== TYPES ====================
|
||||
|
||||
export interface RoadmapPhase {
|
||||
id: string
|
||||
name: string
|
||||
status: 'completed' | 'in_progress' | 'planned' | 'future'
|
||||
progress: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
description: string
|
||||
features: string[]
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
priority: 'critical' | 'high' | 'medium' | 'low'
|
||||
status: 'done' | 'in_progress' | 'todo' | 'backlog'
|
||||
phase: string
|
||||
effort: 'small' | 'medium' | 'large' | 'epic'
|
||||
assignee?: string
|
||||
dueDate?: string
|
||||
feedback?: string[]
|
||||
}
|
||||
|
||||
export interface TeacherFeedback {
|
||||
id: string
|
||||
teacher: string
|
||||
date: string
|
||||
type: 'bug' | 'feature_request' | 'improvement' | 'praise' | 'question'
|
||||
priority: 'critical' | 'high' | 'medium' | 'low'
|
||||
status: 'new' | 'acknowledged' | 'planned' | 'implemented' | 'declined'
|
||||
title: string
|
||||
description: string
|
||||
relatedFeature?: string
|
||||
response?: string
|
||||
}
|
||||
|
||||
// ==================== STYLE MAPS ====================
|
||||
|
||||
export const statusColors: Record<Feature['status'], string> = {
|
||||
done: 'bg-green-100 text-green-800',
|
||||
in_progress: 'bg-blue-100 text-blue-800',
|
||||
todo: 'bg-amber-100 text-amber-800',
|
||||
backlog: 'bg-slate-100 text-slate-600',
|
||||
}
|
||||
|
||||
export const priorityColors: Record<Feature['priority'], string> = {
|
||||
critical: 'bg-red-500 text-white',
|
||||
high: 'bg-orange-500 text-white',
|
||||
medium: 'bg-yellow-500 text-white',
|
||||
low: 'bg-slate-400 text-white',
|
||||
}
|
||||
|
||||
export const feedbackTypeIcons: Record<TeacherFeedback['type'], string> = {
|
||||
bug: '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',
|
||||
feature_request: 'M12 6v6m0 0v6m0-6h6m-6 0H6',
|
||||
improvement: 'M13 10V3L4 14h7v7l9-11h-7z',
|
||||
praise: 'M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z',
|
||||
question: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
}
|
||||
97
website/app/admin/companion/_components/useCompanionDev.ts
Normal file
97
website/app/admin/companion/_components/useCompanionDev.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { Feature, TeacherFeedback } from './types'
|
||||
import { initialFeatures, initialFeedback, roadmapPhases } from './data'
|
||||
|
||||
// Data version - increment when adding new features/feedback to force refresh
|
||||
const DATA_VERSION = '8.2.0' // Phase 8e: Frontend UI-Komponenten (ContextBar, Sidebar, PathPanel)
|
||||
|
||||
export function useCompanionDev() {
|
||||
const [features, setFeatures] = useState<Feature[]>(initialFeatures)
|
||||
const [feedback, setFeedback] = useState<TeacherFeedback[]>(initialFeedback)
|
||||
const [activeTab, setActiveTab] = useState<'roadmap' | 'features' | 'feedback' | 'backlog'>('roadmap')
|
||||
const [selectedPhase, setSelectedPhase] = useState<string | null>(null)
|
||||
const [feedbackFilter, setFeedbackFilter] = useState<string>('all')
|
||||
|
||||
// Load from localStorage with version check
|
||||
useEffect(() => {
|
||||
const savedVersion = localStorage.getItem('companion-dev-version')
|
||||
const savedFeatures = localStorage.getItem('companion-dev-features')
|
||||
const savedFeedback = localStorage.getItem('companion-dev-feedback')
|
||||
|
||||
// If version mismatch or no version, use initial data and save new version
|
||||
if (savedVersion !== DATA_VERSION) {
|
||||
console.log(`Companion Dev: Data version updated from ${savedVersion} to ${DATA_VERSION}`)
|
||||
localStorage.setItem('companion-dev-version', DATA_VERSION)
|
||||
localStorage.setItem('companion-dev-features', JSON.stringify(initialFeatures))
|
||||
localStorage.setItem('companion-dev-feedback', JSON.stringify(initialFeedback))
|
||||
// State already initialized with initialFeatures/initialFeedback, no need to setFeatures
|
||||
return
|
||||
}
|
||||
|
||||
// Load saved data if version matches
|
||||
if (savedFeatures) setFeatures(JSON.parse(savedFeatures))
|
||||
if (savedFeedback) setFeedback(JSON.parse(savedFeedback))
|
||||
}, [])
|
||||
|
||||
// Save to localStorage
|
||||
useEffect(() => {
|
||||
localStorage.setItem('companion-dev-features', JSON.stringify(features))
|
||||
}, [features])
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('companion-dev-feedback', JSON.stringify(feedback))
|
||||
}, [feedback])
|
||||
|
||||
const getPhaseStats = () => {
|
||||
const total = roadmapPhases.length
|
||||
const completed = roadmapPhases.filter(p => p.status === 'completed').length
|
||||
const inProgress = roadmapPhases.filter(p => p.status === 'in_progress').length
|
||||
return { total, completed, inProgress }
|
||||
}
|
||||
|
||||
const getFeatureStats = () => {
|
||||
const total = features.length
|
||||
const done = features.filter(f => f.status === 'done').length
|
||||
const inProgress = features.filter(f => f.status === 'in_progress').length
|
||||
return { total, done, inProgress, percentage: Math.round((done / total) * 100) }
|
||||
}
|
||||
|
||||
const getFeedbackStats = () => {
|
||||
const total = feedback.length
|
||||
const newCount = feedback.filter(f => f.status === 'new').length
|
||||
const bugs = feedback.filter(f => f.type === 'bug').length
|
||||
const requests = feedback.filter(f => f.type === 'feature_request').length
|
||||
return { total, newCount, bugs, requests }
|
||||
}
|
||||
|
||||
const updateFeatureStatus = (id: string, status: Feature['status']) => {
|
||||
setFeatures(features.map(f => f.id === id ? { ...f, status } : f))
|
||||
}
|
||||
|
||||
const updateFeedbackStatus = (id: string, status: TeacherFeedback['status']) => {
|
||||
setFeedback(feedback.map(f => f.id === id ? { ...f, status } : f))
|
||||
}
|
||||
|
||||
const filteredFeedback = feedbackFilter === 'all'
|
||||
? feedback
|
||||
: feedback.filter(f => f.type === feedbackFilter || f.status === feedbackFilter)
|
||||
|
||||
return {
|
||||
features,
|
||||
feedback,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
selectedPhase,
|
||||
setSelectedPhase,
|
||||
feedbackFilter,
|
||||
setFeedbackFilter,
|
||||
phaseStats: getPhaseStats(),
|
||||
featureStats: getFeatureStats(),
|
||||
feedbackStats: getFeedbackStats(),
|
||||
updateFeatureStatus,
|
||||
updateFeedbackStatus,
|
||||
filteredFeedback,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
143
website/app/admin/magic-help/_components/ArchitectureTab.tsx
Normal file
143
website/app/admin/magic-help/_components/ArchitectureTab.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
const ARCHITECTURE_DIAGRAM = `┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ MAGIC HELP ARCHITEKTUR │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌──────────────────┐ ┌───────────────┐ │
|
||||
│ │ FRONTEND │ │ BACKEND │ │ STORAGE │ │
|
||||
│ │ (Next.js) │ │ (FastAPI) │ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌─────────┐ │ REST │ ┌────────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │ Admin │──┼─────────┼──│ TrOCR │ │ │ │ Models │ │ │
|
||||
│ │ │ Panel │ │ │ │ Service │──┼─────────┼──│ (ONNX) │ │ │
|
||||
│ │ └─────────┘ │ │ └────────────┘ │ │ └─────────┘ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ │ ┌─────────┐ │ WebSocket│ ┌────────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │ Lehrer │──┼─────────┼──│ Klausur │ │ │ │ LoRA │ │ │
|
||||
│ │ │ Portal │ │ │ │ Processor │──┼─────────┼──│ Adapter │ │ │
|
||||
│ │ └─────────┘ │ │ └────────────┘ │ │ └─────────┘ │ │
|
||||
│ │ │ │ │ │ │ │ │
|
||||
│ └───────────────┘ │ ┌────────────┐ │ │ ┌─────────┐ │ │
|
||||
│ │ │ Pseudo- │ │ │ │Training │ │ │
|
||||
│ │ │ nymizer │──┼─────────┼──│ Data │ │ │
|
||||
│ │ └────────────┘ │ │ └─────────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ └──────────────────┘ └───────────────┘ │
|
||||
│ │ │
|
||||
│ │ (nur pseudonymisiert) │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ CLOUD LLM │ │
|
||||
│ │ (SysEleven) │ │
|
||||
│ │ Namespace- │ │
|
||||
│ │ Isolation │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘`
|
||||
|
||||
const COMPONENTS = [
|
||||
{
|
||||
icon: '🔍',
|
||||
title: 'TrOCR Service',
|
||||
details: [
|
||||
{ label: 'Modell', value: 'microsoft/trocr-base-handwritten' },
|
||||
{ label: 'Größe', value: '~350 MB' },
|
||||
{ label: 'Lizenz', value: 'MIT' },
|
||||
{ label: 'Framework', value: 'PyTorch / Transformers' },
|
||||
],
|
||||
description: 'Das TrOCR-Modell von Microsoft ist speziell für Handschrifterkennung trainiert. Es verwendet eine Vision-Transformer (ViT) Architektur für Bildverarbeitung und einen Text-Decoder für die Textgenerierung.',
|
||||
},
|
||||
{
|
||||
icon: '🎯',
|
||||
title: 'LoRA Fine-Tuning',
|
||||
details: [
|
||||
{ label: 'Methode', value: 'Low-Rank Adaptation' },
|
||||
{ label: 'Adapter-Größe', value: '~10 MB' },
|
||||
{ label: 'Trainingszeit', value: '5-15 Min (CPU)' },
|
||||
{ label: 'Min. Beispiele', value: '10' },
|
||||
],
|
||||
description: 'LoRA fügt kleine, trainierbare Matrizen zu bestimmten Schichten hinzu, ohne das Basismodell zu verändern. Dies ermöglicht effizientes Fine-Tuning mit minimaler Speichernutzung.',
|
||||
},
|
||||
{
|
||||
icon: '🔒',
|
||||
title: 'Pseudonymisierung',
|
||||
details: [
|
||||
{ label: 'Methode', value: 'QR-Code Tokens' },
|
||||
{ label: 'Token-Format', value: 'UUID v4' },
|
||||
{ label: 'Mapping', value: 'Lokal beim Lehrer' },
|
||||
{ label: 'Cloud-Daten', value: 'Nur Tokens + Text' },
|
||||
],
|
||||
description: 'Schülernamen werden durch anonyme Tokens ersetzt, bevor Daten die lokale Umgebung verlassen. Das Mapping wird ausschließlich lokal gespeichert.',
|
||||
},
|
||||
{
|
||||
icon: '☁️',
|
||||
title: 'Cloud LLM',
|
||||
details: [
|
||||
{ label: 'Provider', value: 'SysEleven (DE)' },
|
||||
{ label: 'Standort', value: 'Deutschland' },
|
||||
{ label: 'Isolation', value: 'Namespace pro Schule' },
|
||||
{ label: 'Datenverarbeitung', value: 'Nur pseudonymisiert' },
|
||||
],
|
||||
description: 'Die KI-Korrektur erfolgt auf deutschen Servern mit strikter Mandantentrennung. Es werden keine Klarnamen oder identifizierenden Informationen übertragen.',
|
||||
},
|
||||
]
|
||||
|
||||
const DATA_FLOW_STEPS = [
|
||||
{ color: 'blue', num: 1, title: 'Lokale Header-Extraktion', desc: 'TrOCR erkennt Schülernamen, Klasse und Fach direkt im Browser/PWA (offline-fähig)' },
|
||||
{ color: 'purple', num: 2, title: 'Pseudonymisierung', desc: 'Namen werden durch QR-Code Tokens ersetzt, Mapping bleibt lokal' },
|
||||
{ color: 'green', num: 3, title: 'Cloud-Korrektur', desc: 'Nur pseudonymisierte Dokument-Tokens werden an die KI gesendet' },
|
||||
{ color: 'yellow', num: 4, title: 'Re-Identifikation', desc: 'Ergebnisse werden lokal mit dem Mapping wieder den echten Namen zugeordnet' },
|
||||
]
|
||||
|
||||
export default function ArchitectureTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Architecture Diagram */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-6">Systemarchitektur</h2>
|
||||
<div className="bg-gray-900 rounded-lg p-6 font-mono text-xs overflow-x-auto">
|
||||
<pre className="text-gray-300">{ARCHITECTURE_DIAGRAM}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Components */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{COMPONENTS.map(comp => (
|
||||
<div key={comp.title} className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<span>{comp.icon}</span> {comp.title}
|
||||
</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
{comp.details.map(d => (
|
||||
<div key={d.label} className="flex justify-between">
|
||||
<span className="text-gray-400">{d.label}</span>
|
||||
<span className="text-white">{d.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mt-4">{comp.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Data Flow */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Datenfluss</h2>
|
||||
<div className="space-y-4">
|
||||
{DATA_FLOW_STEPS.map(step => (
|
||||
<div key={step.num} className="flex items-start gap-4 bg-gray-900/50 rounded-lg p-4">
|
||||
<div className={`w-8 h-8 rounded-full bg-${step.color}-500/20 flex items-center justify-center text-${step.color}-400 font-bold`}>
|
||||
{step.num}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-white">{step.title}</div>
|
||||
<div className="text-sm text-gray-400">{step.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
104
website/app/admin/magic-help/_components/OcrTestTab.tsx
Normal file
104
website/app/admin/magic-help/_components/OcrTestTab.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client'
|
||||
|
||||
import type { OCRResult } from './types'
|
||||
|
||||
interface OcrTestTabProps {
|
||||
ocrResult: OCRResult | null
|
||||
ocrLoading: boolean
|
||||
handleFileUpload: (file: File) => void
|
||||
}
|
||||
|
||||
export default function OcrTestTab({ ocrResult, ocrLoading, handleFileUpload }: OcrTestTabProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* OCR Test */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">OCR Test</h2>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Teste die Handschrifterkennung mit einem eigenen Bild. Das Ergebnis zeigt
|
||||
den erkannten Text, Konfidenz und Verarbeitungszeit.
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-600 rounded-lg p-8 text-center cursor-pointer hover:border-blue-500 transition-colors"
|
||||
onClick={() => document.getElementById('ocr-file-input')?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('border-blue-500') }}
|
||||
onDragLeave={(e) => { e.currentTarget.classList.remove('border-blue-500') }}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
e.currentTarget.classList.remove('border-blue-500')
|
||||
const file = e.dataTransfer.files[0]
|
||||
if (file?.type.startsWith('image/')) handleFileUpload(file)
|
||||
}}
|
||||
>
|
||||
<div className="text-4xl mb-2">📄</div>
|
||||
<div className="text-gray-300">Bild hierher ziehen oder klicken zum Hochladen</div>
|
||||
<div className="text-xs text-gray-500 mt-1">PNG, JPG - Handgeschriebener Text</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
id="ocr-file-input"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) handleFileUpload(file)
|
||||
}}
|
||||
/>
|
||||
|
||||
{ocrLoading && (
|
||||
<div className="mt-4 flex items-center gap-2 text-gray-400">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Analysiere Bild...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ocrResult && (
|
||||
<div className="mt-4 bg-gray-900/50 rounded-lg p-4">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-2">Erkannter Text:</h3>
|
||||
<pre className="bg-gray-950 p-3 rounded text-sm text-white whitespace-pre-wrap max-h-48 overflow-y-auto">
|
||||
{ocrResult.text || '(Kein Text erkannt)'}
|
||||
</pre>
|
||||
<div className="mt-3 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div className="bg-gray-800 rounded p-2">
|
||||
<div className="text-gray-400 text-xs">Konfidenz</div>
|
||||
<div className="text-white font-medium">{(ocrResult.confidence * 100).toFixed(1)}%</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded p-2">
|
||||
<div className="text-gray-400 text-xs">Verarbeitungszeit</div>
|
||||
<div className="text-white font-medium">{ocrResult.processing_time_ms}ms</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded p-2">
|
||||
<div className="text-gray-400 text-xs">Modell</div>
|
||||
<div className="text-white font-medium">{ocrResult.model || 'TrOCR'}</div>
|
||||
</div>
|
||||
<div className="bg-gray-800 rounded p-2">
|
||||
<div className="text-gray-400 text-xs">LoRA Adapter</div>
|
||||
<div className="text-white font-medium">{ocrResult.has_lora_adapter ? 'Ja' : 'Nein'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confidence Interpretation */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Konfidenz-Interpretation</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-green-900/20 border border-green-800 rounded-lg p-4">
|
||||
<div className="text-green-400 font-medium">90-100%</div>
|
||||
<div className="text-sm text-gray-300 mt-1">Sehr hohe Sicherheit - Text kann direkt übernommen werden</div>
|
||||
</div>
|
||||
<div className="bg-yellow-900/20 border border-yellow-800 rounded-lg p-4">
|
||||
<div className="text-yellow-400 font-medium">70-90%</div>
|
||||
<div className="text-sm text-gray-300 mt-1">Gute Sicherheit - manuelle Überprüfung empfohlen</div>
|
||||
</div>
|
||||
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4">
|
||||
<div className="text-red-400 font-medium">< 70%</div>
|
||||
<div className="text-sm text-gray-300 mt-1">Niedrige Sicherheit - manuelle Eingabe erforderlich</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
website/app/admin/magic-help/_components/OverviewTab.tsx
Normal file
111
website/app/admin/magic-help/_components/OverviewTab.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
|
||||
import type { TrOCRStatus } from './types'
|
||||
|
||||
interface OverviewTabProps {
|
||||
status: TrOCRStatus | null
|
||||
loading: boolean
|
||||
fetchStatus: () => void
|
||||
}
|
||||
|
||||
export default function OverviewTab({ status, loading, fetchStatus }: OverviewTabProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Status Card */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-white">Systemstatus</h2>
|
||||
<button
|
||||
onClick={fetchStatus}
|
||||
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm transition-colors"
|
||||
>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-gray-400">Lade Status...</div>
|
||||
) : status?.status === 'available' ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-gray-900/50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-white">{status.model_name || 'trocr-base'}</div>
|
||||
<div className="text-xs text-gray-400">Modell</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-white">{status.device || 'CPU'}</div>
|
||||
<div className="text-xs text-gray-400">Gerät</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-white">{status.training_examples_count || 0}</div>
|
||||
<div className="text-xs text-gray-400">Trainingsbeispiele</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-white">{status.has_lora_adapter ? 'Aktiv' : 'Keiner'}</div>
|
||||
<div className="text-xs text-gray-400">LoRA Adapter</div>
|
||||
</div>
|
||||
</div>
|
||||
) : status?.status === 'not_installed' ? (
|
||||
<div className="text-gray-400">
|
||||
<p className="mb-2">TrOCR ist nicht installiert. Führe aus:</p>
|
||||
<code className="bg-gray-900 px-3 py-2 rounded text-sm block">{status.install_command}</code>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-red-400">{status?.error || 'Unbekannter Fehler'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-gradient-to-br from-purple-900/30 to-purple-800/20 border border-purple-700/50 rounded-xl p-6">
|
||||
<div className="text-3xl mb-2">🎯</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Handschrifterkennung</h3>
|
||||
<p className="text-sm text-gray-300">
|
||||
TrOCR erkennt automatisch handgeschriebenen Text in Klausuren.
|
||||
Das Modell wurde speziell für deutsche Handschriften optimiert.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-green-900/30 to-green-800/20 border border-green-700/50 rounded-xl p-6">
|
||||
<div className="text-3xl mb-2">🔒</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Privacy by Design</h3>
|
||||
<p className="text-sm text-gray-300">
|
||||
Alle Daten werden lokal verarbeitet. Schülernamen werden durch
|
||||
QR-Codes pseudonymisiert - DSGVO-konform.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-br from-blue-900/30 to-blue-800/20 border border-blue-700/50 rounded-xl p-6">
|
||||
<div className="text-3xl mb-2">📈</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Kontinuierliches Lernen</h3>
|
||||
<p className="text-sm text-gray-300">
|
||||
Mit LoRA Fine-Tuning passt sich das Modell an individuelle
|
||||
Handschriften an - ohne das Basismodell zu verändern.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Overview */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Magic Onboarding Workflow</h2>
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||
{[
|
||||
{ icon: '📄', title: '1. Upload', desc: '25 Klausuren hochladen' },
|
||||
{ icon: '🔍', title: '2. Analyse', desc: 'Lokale OCR in 5-10 Sek' },
|
||||
{ icon: '✅', title: '3. Bestätigung', desc: 'Klasse, Schüler, Fach' },
|
||||
{ icon: '🤖', title: '4. KI-Korrektur', desc: 'Cloud mit Pseudonymisierung' },
|
||||
{ icon: '📊', title: '5. Integration', desc: 'Notenbuch, Zeugnisse' },
|
||||
].map((step, i, arr) => (
|
||||
<div key={step.title} className="contents">
|
||||
<div className="flex items-center gap-2 bg-gray-900/50 rounded-lg px-4 py-3">
|
||||
<span className="text-2xl">{step.icon}</span>
|
||||
<div>
|
||||
<div className="font-medium text-white">{step.title}</div>
|
||||
<div className="text-gray-400">{step.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{i < arr.length - 1 && <div className="text-gray-600">→</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
193
website/app/admin/magic-help/_components/SettingsTab.tsx
Normal file
193
website/app/admin/magic-help/_components/SettingsTab.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client'
|
||||
|
||||
import type { MagicSettings } from './types'
|
||||
import { DEFAULT_SETTINGS } from './types'
|
||||
|
||||
interface SettingsTabProps {
|
||||
settings: MagicSettings
|
||||
setSettings: (settings: MagicSettings) => void
|
||||
settingsSaved: boolean
|
||||
saveSettings: () => void
|
||||
}
|
||||
|
||||
export default function SettingsTab({ settings, setSettings, settingsSaved, saveSettings }: SettingsTabProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* OCR Settings */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">OCR Einstellungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.autoDetectLines}
|
||||
onChange={(e) => setSettings({ ...settings, autoDetectLines: e.target.checked })}
|
||||
className="w-5 h-5 rounded bg-gray-900 border-gray-700"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-white font-medium">Automatische Zeilenerkennung</div>
|
||||
<div className="text-sm text-gray-400">Erkennt und verarbeitet einzelne Zeilen separat</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">Konfidenz-Schwellwert</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={settings.confidenceThreshold}
|
||||
onChange={(e) => setSettings({ ...settings, confidenceThreshold: parseFloat(e.target.value) })}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||
<span>0%</span>
|
||||
<span className="text-white">{(settings.confidenceThreshold * 100).toFixed(0)}%</span>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">Max. Bildgröße (px)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maxImageSize}
|
||||
onChange={(e) => setSettings({ ...settings, maxImageSize: parseInt(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-1">Größere Bilder werden skaliert</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.enableCache}
|
||||
onChange={(e) => setSettings({ ...settings, enableCache: e.target.checked })}
|
||||
className="w-5 h-5 rounded bg-gray-900 border-gray-700"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-white font-medium">Ergebnis-Cache aktivieren</div>
|
||||
<div className="text-sm text-gray-400">Speichert OCR-Ergebnisse für identische Bilder</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Training Settings */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Training Einstellungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">LoRA Rank</label>
|
||||
<select
|
||||
value={settings.loraRank}
|
||||
onChange={(e) => setSettings({ ...settings, loraRank: parseInt(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
>
|
||||
<option value="4">4 (Schnell, weniger Kapazität)</option>
|
||||
<option value="8">8 (Ausgewogen)</option>
|
||||
<option value="16">16 (Mehr Kapazität)</option>
|
||||
<option value="32">32 (Maximum)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">LoRA Alpha</label>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.loraAlpha}
|
||||
onChange={(e) => setSettings({ ...settings, loraAlpha: parseInt(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
/>
|
||||
<div className="text-xs text-gray-500 mt-1">Empfohlen: 4 × LoRA Rank</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">Epochen</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="10"
|
||||
value={settings.epochs}
|
||||
onChange={(e) => setSettings({ ...settings, epochs: parseInt(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">Batch Size</label>
|
||||
<select
|
||||
value={settings.batchSize}
|
||||
onChange={(e) => setSettings({ ...settings, batchSize: parseInt(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
>
|
||||
<option value="1">1 (Wenig RAM)</option>
|
||||
<option value="2">2</option>
|
||||
<option value="4">4 (Standard)</option>
|
||||
<option value="8">8 (Viel RAM)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">Learning Rate</label>
|
||||
<select
|
||||
value={settings.learningRate}
|
||||
onChange={(e) => setSettings({ ...settings, learningRate: parseFloat(e.target.value) })}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-white"
|
||||
>
|
||||
<option value="0.0001">0.0001 (Schnell)</option>
|
||||
<option value="0.00005">0.00005 (Standard)</option>
|
||||
<option value="0.00001">0.00001 (Konservativ)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
onClick={() => setSettings(DEFAULT_SETTINGS)}
|
||||
className="px-6 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Zurücksetzen
|
||||
</button>
|
||||
<button
|
||||
onClick={saveSettings}
|
||||
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{settingsSaved ? '✓ Gespeichert!' : 'Einstellungen speichern'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Technical Info */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Technische Informationen</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">API Endpoint:</span>
|
||||
<code className="text-white ml-2 bg-gray-900 px-2 py-1 rounded text-xs">/api/klausur/trocr</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Model Path:</span>
|
||||
<code className="text-white ml-2 bg-gray-900 px-2 py-1 rounded text-xs">~/.cache/huggingface</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">LoRA Path:</span>
|
||||
<code className="text-white ml-2 bg-gray-900 px-2 py-1 rounded text-xs">./models/lora</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Training Data:</span>
|
||||
<code className="text-white ml-2 bg-gray-900 px-2 py-1 rounded text-xs">./data/training</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
182
website/app/admin/magic-help/_components/TrainingTab.tsx
Normal file
182
website/app/admin/magic-help/_components/TrainingTab.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
'use client'
|
||||
|
||||
import type { TrOCRStatus, TrainingExample, MagicSettings } from './types'
|
||||
|
||||
interface TrainingTabProps {
|
||||
status: TrOCRStatus | null
|
||||
examples: TrainingExample[]
|
||||
trainingImage: File | null
|
||||
setTrainingImage: (file: File | null) => void
|
||||
trainingText: string
|
||||
setTrainingText: (text: string) => void
|
||||
fineTuning: boolean
|
||||
settings: MagicSettings
|
||||
handleAddTrainingExample: () => void
|
||||
handleFineTune: () => void
|
||||
}
|
||||
|
||||
export default function TrainingTab({
|
||||
status,
|
||||
examples,
|
||||
trainingImage,
|
||||
setTrainingImage,
|
||||
trainingText,
|
||||
setTrainingText,
|
||||
fineTuning,
|
||||
settings,
|
||||
handleAddTrainingExample,
|
||||
handleFineTune,
|
||||
}: TrainingTabProps) {
|
||||
const examplesCount = status?.training_examples_count || 0
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Training Overview */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Training mit LoRA</h2>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
LoRA (Low-Rank Adaptation) ermöglicht effizientes Fine-Tuning ohne das Basismodell zu verändern.
|
||||
Das Training erfolgt lokal auf Ihrem System.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 text-center">
|
||||
<div className="text-3xl font-bold text-white">{examplesCount}</div>
|
||||
<div className="text-xs text-gray-400">Trainingsbeispiele</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 text-center">
|
||||
<div className="text-3xl font-bold text-white">10</div>
|
||||
<div className="text-xs text-gray-400">Minimum benötigt</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 text-center">
|
||||
<div className="text-3xl font-bold text-white">{settings.loraRank}</div>
|
||||
<div className="text-xs text-gray-400">LoRA Rank</div>
|
||||
</div>
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 text-center">
|
||||
<div className="text-3xl font-bold text-white">{status?.has_lora_adapter ? '✓' : '✗'}</div>
|
||||
<div className="text-xs text-gray-400">Adapter aktiv</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-gray-400">Fortschritt zum Fine-Tuning</span>
|
||||
<span className="text-gray-400">{Math.min(100, (examplesCount / 10) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500"
|
||||
style={{ width: `${Math.min(100, (examplesCount / 10) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Add Training Example */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Trainingsbeispiel hinzufügen</h2>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Lade ein Bild mit handgeschriebenem Text hoch und gib die korrekte Transkription ein.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">Bild</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm"
|
||||
onChange={(e) => setTrainingImage(e.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">Korrekter Text (Ground Truth)</label>
|
||||
<textarea
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white resize-none"
|
||||
rows={3}
|
||||
placeholder="Gib hier den korrekten Text ein..."
|
||||
value={trainingText}
|
||||
onChange={(e) => setTrainingText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddTrainingExample}
|
||||
className="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
+ Trainingsbeispiel hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fine-Tuning */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Fine-Tuning starten</h2>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Trainiere das Modell mit den gesammelten Beispielen. Der Prozess dauert
|
||||
je nach Anzahl der Beispiele einige Minuten.
|
||||
</p>
|
||||
|
||||
<div className="bg-gray-900/50 rounded-lg p-4 mb-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Epochen:</span>
|
||||
<span className="text-white ml-2">{settings.epochs}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Learning Rate:</span>
|
||||
<span className="text-white ml-2">{settings.learningRate}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">LoRA Rank:</span>
|
||||
<span className="text-white ml-2">{settings.loraRank}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Batch Size:</span>
|
||||
<span className="text-white ml-2">{settings.batchSize}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleFineTune}
|
||||
disabled={fineTuning || examplesCount < 10}
|
||||
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{fineTuning ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Fine-Tuning läuft...
|
||||
</span>
|
||||
) : (
|
||||
'Fine-Tuning starten'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{examplesCount < 10 && (
|
||||
<p className="text-xs text-yellow-400 mt-2 text-center">
|
||||
Noch {10 - examplesCount} Beispiele benötigt
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Training Examples List */}
|
||||
{examples.length > 0 && (
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold text-white mb-4">Trainingsbeispiele ({examples.length})</h2>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{examples.map((ex, i) => (
|
||||
<div key={i} className="flex items-center gap-4 bg-gray-900/50 rounded-lg p-3">
|
||||
<span className="text-gray-500 font-mono text-sm w-8">{i + 1}.</span>
|
||||
<span className="text-white text-sm flex-1 truncate">{ex.ground_truth}</span>
|
||||
<span className="text-gray-500 text-xs">{new Date(ex.created_at).toLocaleDateString('de-DE')}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
website/app/admin/magic-help/_components/types.ts
Normal file
62
website/app/admin/magic-help/_components/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export type TabId = 'overview' | 'test' | 'training' | 'architecture' | 'settings'
|
||||
|
||||
export interface TrOCRStatus {
|
||||
status: 'available' | 'not_installed' | 'error'
|
||||
model_name?: string
|
||||
model_id?: string
|
||||
device?: string
|
||||
is_loaded?: boolean
|
||||
has_lora_adapter?: boolean
|
||||
training_examples_count?: number
|
||||
error?: string
|
||||
install_command?: string
|
||||
}
|
||||
|
||||
export interface OCRResult {
|
||||
text: string
|
||||
confidence: number
|
||||
processing_time_ms: number
|
||||
model: string
|
||||
has_lora_adapter: boolean
|
||||
}
|
||||
|
||||
export interface TrainingExample {
|
||||
image_path: string
|
||||
ground_truth: string
|
||||
teacher_id: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface MagicSettings {
|
||||
autoDetectLines: boolean
|
||||
confidenceThreshold: number
|
||||
maxImageSize: number
|
||||
loraRank: number
|
||||
loraAlpha: number
|
||||
learningRate: number
|
||||
epochs: number
|
||||
batchSize: number
|
||||
enableCache: boolean
|
||||
cacheMaxAge: number
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MagicSettings = {
|
||||
autoDetectLines: true,
|
||||
confidenceThreshold: 0.7,
|
||||
maxImageSize: 4096,
|
||||
loraRank: 8,
|
||||
loraAlpha: 32,
|
||||
learningRate: 0.00005,
|
||||
epochs: 3,
|
||||
batchSize: 4,
|
||||
enableCache: true,
|
||||
cacheMaxAge: 3600,
|
||||
}
|
||||
|
||||
export const TABS = [
|
||||
{ id: 'overview' as TabId, label: 'Übersicht', icon: '📊' },
|
||||
{ id: 'test' as TabId, label: 'OCR Test', icon: '🔍' },
|
||||
{ id: 'training' as TabId, label: 'Training', icon: '🎯' },
|
||||
{ id: 'architecture' as TabId, label: 'Architektur', icon: '🏗️' },
|
||||
{ id: 'settings' as TabId, label: 'Einstellungen', icon: '⚙️' },
|
||||
]
|
||||
180
website/app/admin/magic-help/_components/useMagicHelp.tsx
Normal file
180
website/app/admin/magic-help/_components/useMagicHelp.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { TabId, TrOCRStatus, OCRResult, TrainingExample, MagicSettings } from './types'
|
||||
import { DEFAULT_SETTINGS } from './types'
|
||||
|
||||
export function useMagicHelp() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
||||
const [status, setStatus] = useState<TrOCRStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [ocrResult, setOcrResult] = useState<OCRResult | null>(null)
|
||||
const [ocrLoading, setOcrLoading] = useState(false)
|
||||
const [examples, setExamples] = useState<TrainingExample[]>([])
|
||||
const [trainingImage, setTrainingImage] = useState<File | null>(null)
|
||||
const [trainingText, setTrainingText] = useState('')
|
||||
const [fineTuning, setFineTuning] = useState(false)
|
||||
const [settings, setSettings] = useState<MagicSettings>(DEFAULT_SETTINGS)
|
||||
const [settingsSaved, setSettingsSaved] = useState(false)
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/klausur/trocr/status')
|
||||
const data = await res.json()
|
||||
setStatus(data)
|
||||
} catch {
|
||||
setStatus({ status: 'error', error: 'Failed to fetch status' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchExamples = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/klausur/trocr/training/examples')
|
||||
const data = await res.json()
|
||||
setExamples(data.examples || [])
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch examples:', error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus()
|
||||
fetchExamples()
|
||||
// Load settings from localStorage
|
||||
const saved = localStorage.getItem('magic-help-settings')
|
||||
if (saved) {
|
||||
try {
|
||||
setSettings(JSON.parse(saved))
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
}
|
||||
}, [fetchStatus, fetchExamples])
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
setOcrLoading(true)
|
||||
setOcrResult(null)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/klausur/trocr/extract?detect_lines=${settings.autoDetectLines}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.text !== undefined) {
|
||||
setOcrResult(data)
|
||||
} else {
|
||||
setOcrResult({ text: `Error: ${data.detail || 'Unknown error'}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false })
|
||||
}
|
||||
} catch (error) {
|
||||
setOcrResult({ text: `Error: ${error}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false })
|
||||
} finally {
|
||||
setOcrLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTrainingExample = async () => {
|
||||
if (!trainingImage || !trainingText.trim()) {
|
||||
alert('Please provide both an image and the correct text')
|
||||
return
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', trainingImage)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/klausur/trocr/training/add?ground_truth=${encodeURIComponent(trainingText)}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.example_id) {
|
||||
alert(`Training example added! Total: ${data.total_examples}`)
|
||||
setTrainingImage(null)
|
||||
setTrainingText('')
|
||||
fetchStatus()
|
||||
fetchExamples()
|
||||
} else {
|
||||
alert(`Error: ${data.detail || 'Unknown error'}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFineTune = async () => {
|
||||
if (!confirm('Start fine-tuning? This may take several minutes.')) return
|
||||
|
||||
setFineTuning(true)
|
||||
try {
|
||||
const res = await fetch('/api/klausur/trocr/training/fine-tune', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
epochs: settings.epochs,
|
||||
learning_rate: settings.learningRate,
|
||||
lora_rank: settings.loraRank,
|
||||
lora_alpha: settings.loraAlpha,
|
||||
}),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.status === 'success') {
|
||||
alert(`Fine-tuning successful!\nExamples used: ${data.examples_used}\nEpochs: ${data.epochs}`)
|
||||
fetchStatus()
|
||||
} else {
|
||||
alert(`Fine-tuning failed: ${data.message}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`Error: ${error}`)
|
||||
} finally {
|
||||
setFineTuning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = () => {
|
||||
localStorage.setItem('magic-help-settings', JSON.stringify(settings))
|
||||
setSettingsSaved(true)
|
||||
setTimeout(() => setSettingsSaved(false), 2000)
|
||||
}
|
||||
|
||||
const getStatusBadge = () => {
|
||||
if (!status) return null
|
||||
switch (status.status) {
|
||||
case 'available':
|
||||
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-green-500/20 text-green-400">Available</span>
|
||||
case 'not_installed':
|
||||
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-red-500/20 text-red-400">Not Installed</span>
|
||||
case 'error':
|
||||
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-500/20 text-yellow-400">Error</span>
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
status,
|
||||
loading,
|
||||
ocrResult,
|
||||
ocrLoading,
|
||||
examples,
|
||||
trainingImage,
|
||||
setTrainingImage,
|
||||
trainingText,
|
||||
setTrainingText,
|
||||
fineTuning,
|
||||
settings,
|
||||
setSettings,
|
||||
settingsSaved,
|
||||
fetchStatus,
|
||||
handleFileUpload,
|
||||
handleAddTrainingExample,
|
||||
handleFineTune,
|
||||
saveSettings,
|
||||
getStatusBadge,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user