[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:
Benjamin Admin
2026-04-24 23:17:30 +02:00
parent b2a0126f14
commit 6811264756
67 changed files with 12270 additions and 13651 deletions

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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.',
},
]

View 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)',
],
}

View 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',
}

View 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

View 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>
)
}

View 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">&lt; 70%</div>
<div className="text-sm text-gray-300 mt-1">Niedrige Sicherheit - manuelle Eingabe erforderlich</div>
</div>
</div>
</div>
</div>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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: '⚙️' },
]

View 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