diff --git a/admin-lehrer/.gitignore b/admin-lehrer/.gitignore deleted file mode 100644 index 9867a52..0000000 --- a/admin-lehrer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Symlink to shared/ (Docker COPY handles this in container) -shared diff --git a/admin-lehrer/Dockerfile b/admin-lehrer/Dockerfile index 6bce771..7fda5b0 100644 --- a/admin-lehrer/Dockerfile +++ b/admin-lehrer/Dockerfile @@ -4,14 +4,13 @@ FROM node:20-alpine AS builder WORKDIR /app # Copy package files -COPY admin-lehrer/package.json admin-lehrer/package-lock.json* ./ +COPY package.json package-lock.json* ./ # Install dependencies RUN npm install -# Copy source code + shared types (inside project for Turbopack) -COPY admin-lehrer/ . -COPY shared/ ./shared/ +# Copy source code +COPY . . # Build arguments for environment variables ARG NEXT_PUBLIC_API_URL diff --git a/admin-lehrer/app/(admin)/ai/ocr-labeling/types.ts b/admin-lehrer/app/(admin)/ai/ocr-labeling/types.ts index 845e7ce..3061055 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-labeling/types.ts +++ b/admin-lehrer/app/(admin)/ai/ocr-labeling/types.ts @@ -1 +1,127 @@ -export * from '@shared/types/ocr-labeling' +/** + * Shared TypeScript types for OCR Labeling UI. + * + * Single source of truth used by: + * - admin-lehrer (ai/ocr-labeling) + * - website (admin/ocr-labeling) + */ + +/** + * Available OCR Models + * + * - llama3.2-vision:11b: Vision LLM, beste Qualitaet bei Handschrift (Standard) + * - trocr: Microsoft TrOCR, schnell bei gedrucktem Text + * - paddleocr: PaddleOCR + LLM, 4x schneller durch Hybrid-Ansatz + * - donut: Document Understanding Transformer, strukturierte Dokumente + */ +export type OCRModel = 'llama3.2-vision:11b' | 'trocr' | 'paddleocr' | 'donut' + +export const OCR_MODEL_INFO: Record = { + 'llama3.2-vision:11b': { + label: 'Vision LLM', + description: 'Beste Qualitaet bei Handschrift', + speed: 'langsam', + }, + trocr: { + label: 'Microsoft TrOCR', + description: 'Schnell bei gedrucktem Text', + speed: 'schnell', + }, + paddleocr: { + label: 'PaddleOCR + LLM', + description: 'Hybrid-Ansatz: OCR + Strukturierung', + speed: 'sehr schnell', + }, + donut: { + label: 'Donut', + description: 'Document Understanding fuer Tabellen/Formulare', + speed: 'mittel', + }, +} + +export interface OCRSession { + id: string + name: string + source_type: 'klausur' | 'handwriting_sample' | 'scan' + description?: string + ocr_model?: OCRModel + total_items: number + labeled_items: number + confirmed_items: number + corrected_items: number + skipped_items: number + created_at: string +} + +export interface OCRItem { + id: string + session_id: string + session_name: string + image_path: string + image_url?: string + ocr_text?: string + ocr_confidence?: number + ground_truth?: string + status: 'pending' | 'confirmed' | 'corrected' | 'skipped' + metadata?: Record + created_at: string +} + +export interface OCRStats { + total_sessions?: number + session_id?: string + name?: string + total_items: number + labeled_items: number + confirmed_items: number + corrected_items: number + skipped_items?: number + pending_items: number + exportable_items?: number + accuracy_rate: number + avg_label_time_seconds?: number + progress_percent?: number +} + +export interface TrainingSample { + id: string + image_path: string + ground_truth: string + export_format: 'generic' | 'trocr' | 'llama_vision' + training_batch: string + exported_at?: string +} + +export interface CreateSessionRequest { + name: string + source_type: 'klausur' | 'handwriting_sample' | 'scan' + description?: string + ocr_model?: OCRModel +} + +export interface ConfirmRequest { + item_id: string + label_time_seconds?: number +} + +export interface CorrectRequest { + item_id: string + ground_truth: string + label_time_seconds?: number +} + +export interface ExportRequest { + export_format: 'generic' | 'trocr' | 'llama_vision' + session_id?: string + batch_id?: string +} + +export interface UploadResult { + id: string + filename: string + image_path: string + image_hash: string + ocr_text?: string + ocr_confidence?: number + status: string +} diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/[klausurId]/[studentId]/_components/workspace-types.ts b/admin-lehrer/app/(admin)/education/klausur-korrektur/[klausurId]/[studentId]/_components/workspace-types.ts index dab4788..fede189 100644 --- a/admin-lehrer/app/(admin)/education/klausur-korrektur/[klausurId]/[studentId]/_components/workspace-types.ts +++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/[klausurId]/[studentId]/_components/workspace-types.ts @@ -12,13 +12,13 @@ export type { ActiveTab, GradeTotals, CriteriaScores, -} from '@shared/types/klausur' +} from '../../../../types' export { WORKFLOW_STATUS_LABELS, ROLE_LABELS, GRADE_LABELS, -} from '@shared/types/klausur' +} from '../../../../types' /** Same-origin proxy to avoid CORS issues */ export const API_BASE = '/klausur-api' diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts index 258a794..56b2dde 100644 --- a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts +++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts @@ -8,4 +8,4 @@ export type { VorabiturEHForm, EHTemplate, DirektuploadForm, -} from '@shared/types/klausur' +} from '../../types' diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/types.ts b/admin-lehrer/app/(admin)/education/klausur-korrektur/types.ts index 4def074..e867255 100644 --- a/admin-lehrer/app/(admin)/education/klausur-korrektur/types.ts +++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/types.ts @@ -1 +1,432 @@ -export * from '@shared/types/klausur' +/** + * Shared Klausur-Korrektur types and constants. + * + * This is the single source of truth used by: + * - admin-lehrer (education/klausur-korrektur) + * - studio-v2 (korrektur) + * - website/admin (klausur-korrektur) + * - website/lehrer (klausur-korrektur) + */ + +// --------------------------------------------------------------------------- +// Core domain interfaces +// --------------------------------------------------------------------------- + +export interface Klausur { + id: string + title: string + subject: string + year: number + semester: string + modus: KlausurModus + eh_id?: string + created_at: string + student_count?: number + completed_count?: number + status?: 'draft' | 'in_progress' | 'completed' +} + +/** Union of all modus values used across services */ +export type KlausurModus = 'abitur' | 'vorabitur' | 'landes_abitur' + +export interface StudentWork { + id: string + klausur_id: string + anonym_id: string + file_path: string + file_type: 'pdf' | 'image' + ocr_text: string + criteria_scores: CriteriaScores + gutachten: string + status: StudentStatus + raw_points: number + grade_points: number + grade_label?: string + created_at: string + examiner_id?: string + second_examiner_id?: string + second_examiner_grade?: number +} + +export type StudentStatus = + | 'UPLOADED' + | 'OCR_PROCESSING' + | 'OCR_COMPLETE' + | 'ANALYZING' + | 'FIRST_EXAMINER' + | 'SECOND_EXAMINER' + | 'COMPLETED' + | 'ERROR' + +export interface CriteriaScores { + rechtschreibung?: number + grammatik?: number + inhalt?: number + struktur?: number + stil?: number + [key: string]: number | undefined +} + +export interface Criterion { + id: string + name: string + weight: number + description?: string +} + +export interface GradeInfo { + thresholds: Record + labels: Record + criteria: Record +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + +export interface Annotation { + id: string + student_work_id: string + page: number + position: AnnotationPosition + type: AnnotationType + text: string + severity: 'minor' | 'major' | 'critical' + suggestion?: string + created_by: string + created_at: string + role: 'first_examiner' | 'second_examiner' + linked_criterion?: string +} + +export interface AnnotationPosition { + x: number // Prozent (0-100) + y: number // Prozent (0-100) + width: number // Prozent (0-100) + height: number // Prozent (0-100) +} + +export type AnnotationType = + | 'rechtschreibung' + | 'grammatik' + | 'inhalt' + | 'struktur' + | 'stil' + | 'comment' + | 'highlight' + +// --------------------------------------------------------------------------- +// Fairness analysis +// --------------------------------------------------------------------------- + +export interface FairnessAnalysis { + klausur_id: string + student_count: number + average_grade: number + std_deviation: number + spread: number + outliers: OutlierInfo[] + criteria_analysis: Record + fairness_score: number + warnings: string[] +} + +export interface OutlierInfo { + student_id: string + anonym_id: string + grade_points: number + deviation: number + reason: string +} + +export interface CriteriaStats { + min: number + max: number + average: number + std_deviation: number +} + +// --------------------------------------------------------------------------- +// EH suggestions +// --------------------------------------------------------------------------- + +export interface EHSuggestion { + criterion: string + excerpt: string + relevance_score: number + source_chunk_id: string + // Attribution fields (CTRL-SRC-002) + source_document?: string + source_url?: string + license?: string + license_url?: string + publisher?: string +} + +/** Default Attribution for NiBiS documents (CTRL-SRC-002) */ +export const NIBIS_ATTRIBUTION = { + publisher: 'Niedersaechsischer Bildungsserver (NiBiS)', + license: 'DL-DE-BY-2.0', + license_url: 'https://www.govdata.de/dl-de/by-2-0', + source_url: 'https://nibis.de', +} as const + +// --------------------------------------------------------------------------- +// Gutachten +// --------------------------------------------------------------------------- + +export interface GutachtenSection { + title: string + content: string + evidence_links?: string[] +} + +export interface Gutachten { + einleitung: string + hauptteil: string + fazit: string + staerken: string[] + schwaechen: string[] + generated_at?: string +} + +// --------------------------------------------------------------------------- +// API response types +// --------------------------------------------------------------------------- + +export interface KlausurenResponse { + klausuren: Klausur[] + total: number +} + +export interface StudentsResponse { + students: StudentWork[] + total: number +} + +export interface AnnotationsResponse { + annotations: Annotation[] +} + +// --------------------------------------------------------------------------- +// Create / update types +// --------------------------------------------------------------------------- + +export interface CreateKlausurData { + title: string + subject?: string + year?: number + semester?: string + modus?: KlausurModus +} + +// --------------------------------------------------------------------------- +// Constants — annotation colors +// --------------------------------------------------------------------------- + +export const ANNOTATION_COLORS: Record = { + rechtschreibung: '#dc2626', // Red + grammatik: '#2563eb', // Blue + inhalt: '#16a34a', // Green + struktur: '#9333ea', // Purple + stil: '#ea580c', // Orange + comment: '#6b7280', // Gray + highlight: '#eab308', // Yellow +} + +// --------------------------------------------------------------------------- +// Constants — status colors & labels +// --------------------------------------------------------------------------- + +export const STATUS_COLORS: Record = { + UPLOADED: '#6b7280', + OCR_PROCESSING: '#eab308', + OCR_COMPLETE: '#3b82f6', + ANALYZING: '#8b5cf6', + FIRST_EXAMINER: '#f97316', + SECOND_EXAMINER: '#06b6d4', + COMPLETED: '#22c55e', + ERROR: '#ef4444', +} + +export const STATUS_LABELS: Record = { + UPLOADED: 'Hochgeladen', + OCR_PROCESSING: 'OCR laeuft', + OCR_COMPLETE: 'OCR fertig', + ANALYZING: 'Analyse laeuft', + FIRST_EXAMINER: 'Erstkorrektur', + SECOND_EXAMINER: 'Zweitkorrektur', + COMPLETED: 'Abgeschlossen', + ERROR: 'Fehler', +} + +// --------------------------------------------------------------------------- +// Constants — criteria & grades +// --------------------------------------------------------------------------- + +/** Default criteria with weights (Niedersachsen standard) */ +export const DEFAULT_CRITERIA: Record = { + rechtschreibung: { name: 'Rechtschreibung', weight: 15 }, + grammatik: { name: 'Grammatik', weight: 15 }, + inhalt: { name: 'Inhalt', weight: 40 }, + struktur: { name: 'Struktur', weight: 15 }, + stil: { name: 'Stil', weight: 15 }, +} + +/** Grade thresholds (15-point system) */ +export const GRADE_THRESHOLDS: Record = { + 15: 95, 14: 90, 13: 85, 12: 80, 11: 75, + 10: 70, 9: 65, 8: 60, 7: 55, 6: 50, + 5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0, +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/** Calculate grade points from a percentage (0-100). */ +export function calculateGrade(percentage: number): number { + for (const [grade, threshold] of Object.entries(GRADE_THRESHOLDS).sort( + (a, b) => Number(b[0]) - Number(a[0]), + )) { + if (percentage >= threshold) { + return Number(grade) + } + } + return 0 +} + +/** Human-readable label for a 15-point grade value. */ +export function getGradeLabel(points: number): string { + const labels: Record = { + 15: '1+', 14: '1', 13: '1-', + 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', + 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', + 0: '6', + } + return labels[points] || String(points) +} + +// --------------------------------------------------------------------------- +// Examiner workflow types (workspace) +// --------------------------------------------------------------------------- + +export interface ExaminerInfo { + id: string + assigned_at: string + notes?: string +} + +export interface ExaminerResult { + grade_points: number + criteria_scores?: CriteriaScores + notes?: string + submitted_at: string +} + +export interface ExaminerWorkflow { + student_id: string + workflow_status: string + visibility_mode: string + user_role: 'ek' | 'zk' | 'dk' | 'viewer' + first_examiner?: ExaminerInfo + second_examiner?: ExaminerInfo + third_examiner?: ExaminerInfo + first_result?: ExaminerResult + first_result_visible?: boolean + second_result?: ExaminerResult + third_result?: ExaminerResult + grade_difference?: number + final_grade?: number + consensus_reached?: boolean + consensus_type?: string + einigung?: { + final_grade: number + notes: string + type: string + submitted_by: string + submitted_at: string + ek_grade: number + zk_grade: number + } + drittkorrektur_reason?: string +} + +export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege' + +export interface GradeTotals { + raw: number + weighted: number + gradePoints: number +} + +// --------------------------------------------------------------------------- +// Constants — workflow status & roles +// --------------------------------------------------------------------------- + +export const GRADE_LABELS: Record = { + 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', 0: '6', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + not_started: { label: 'Nicht gestartet', color: 'bg-slate-100 text-slate-700' }, + ek_in_progress: { label: 'EK in Arbeit', color: 'bg-blue-100 text-blue-700' }, + ek_completed: { label: 'EK abgeschlossen', color: 'bg-blue-200 text-blue-800' }, + zk_assigned: { label: 'ZK zugewiesen', color: 'bg-amber-100 text-amber-700' }, + zk_in_progress: { label: 'ZK in Arbeit', color: 'bg-amber-200 text-amber-800' }, + zk_completed: { label: 'ZK abgeschlossen', color: 'bg-amber-300 text-amber-900' }, + einigung_required: { label: 'Einigung erforderlich', color: 'bg-orange-100 text-orange-700' }, + einigung_completed: { label: 'Einigung abgeschlossen', color: 'bg-green-100 text-green-700' }, + drittkorrektur_required: { label: 'DK erforderlich', color: 'bg-red-100 text-red-700' }, + drittkorrektur_assigned: { label: 'DK zugewiesen', color: 'bg-red-200 text-red-800' }, + drittkorrektur_in_progress: { label: 'DK in Arbeit', color: 'bg-red-300 text-red-900' }, + completed: { label: 'Abgeschlossen', color: 'bg-green-200 text-green-800' }, +} + +export const ROLE_LABELS: Record = { + ek: { label: 'Erstkorrektor', color: 'bg-blue-500' }, + zk: { label: 'Zweitkorrektor', color: 'bg-amber-500' }, + dk: { label: 'Drittkorrektor', color: 'bg-purple-500' }, + viewer: { label: 'Betrachter', color: 'bg-slate-500' }, +} + +// --------------------------------------------------------------------------- +// Form types (create / upload) +// --------------------------------------------------------------------------- + +export interface CreateKlausurForm { + title: string + subject: string + year: number + semester: string + modus: 'abitur' | 'vorabitur' +} + +export interface VorabiturEHForm { + aufgabentyp: string + titel: string + text_titel: string + text_autor: string + aufgabenstellung: string +} + +export interface EHTemplate { + aufgabentyp: string + name: string + description: string + category: string +} + +export interface DirektuploadForm { + files: File[] + ehFile: File | null + ehText: string + aufgabentyp: string + klausurTitle: string +} + +export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken' diff --git a/admin-lehrer/lib/companion/types.ts b/admin-lehrer/lib/companion/types.ts index 90ba262..5a4405b 100644 --- a/admin-lehrer/lib/companion/types.ts +++ b/admin-lehrer/lib/companion/types.ts @@ -1 +1,329 @@ -export * from '@shared/types/companion' +/** + * TypeScript Types for Companion Module + * Migration from Flask companion.py/companion_js.py + */ + +// ============================================================================ +// Phase System +// ============================================================================ + +export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion' + +export interface Phase { + id: PhaseId + shortName: string // E, A, S, T, R + displayName: string + duration: number // minutes + status: 'planned' | 'active' | 'completed' + actualTime?: number // seconds (actual time spent) + color: string // hex color +} + +export interface PhaseContext { + currentPhase: PhaseId + phaseDisplayName: string +} + +// ============================================================================ +// Dashboard / Companion Mode +// ============================================================================ + +export interface CompanionStats { + classesCount: number + studentsCount: number + learningUnitsCreated: number + gradesEntered: number +} + +export interface Progress { + percentage: number + completed: number + total: number +} + +export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low' + +export interface Suggestion { + id: string + title: string + description: string + priority: SuggestionPriority + icon: string // lucide icon name + actionTarget: string // navigation path + estimatedTime: number // minutes +} + +export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other' + +export interface UpcomingEvent { + id: string + title: string + date: string // ISO date string + type: EventType + inDays: number +} + +export interface CompanionData { + context: PhaseContext + stats: CompanionStats + phases: Phase[] + progress: Progress + suggestions: Suggestion[] + upcomingEvents: UpcomingEvent[] +} + +// ============================================================================ +// Lesson Mode +// ============================================================================ + +export type LessonStatus = + | 'not_started' + | 'in_progress' + | 'paused' + | 'completed' + | 'overtime' + +export interface LessonPhase { + phase: PhaseId + duration: number // planned duration in minutes + status: 'planned' | 'active' | 'completed' | 'skipped' + actualTime: number // actual time spent in seconds + startedAt?: string // ISO timestamp + completedAt?: string // ISO timestamp +} + +export interface Homework { + id: string + title: string + description?: string + dueDate: string // ISO date + attachments?: string[] + completed?: boolean +} + +export interface Material { + id: string + title: string + type: 'document' | 'video' | 'presentation' | 'link' | 'other' + url?: string + fileName?: string +} + +export interface LessonReflection { + rating: number // 1-5 stars + notes: string + nextSteps: string + savedAt?: string +} + +export interface LessonSession { + sessionId: string + classId: string + className: string + subject: string + topic?: string + startTime: string // ISO timestamp + endTime?: string // ISO timestamp + phases: LessonPhase[] + totalPlannedDuration: number // minutes + currentPhaseIndex: number + elapsedTime: number // seconds + isPaused: boolean + pausedAt?: string + pauseDuration: number // total pause time in seconds + overtimeMinutes: number + status: LessonStatus + homeworkList: Homework[] + materials: Material[] + reflection?: LessonReflection +} + +// ============================================================================ +// Lesson Templates +// ============================================================================ + +export interface PhaseDurations { + einstieg: number + erarbeitung: number + sicherung: number + transfer: number + reflexion: number +} + +export interface LessonTemplate { + templateId: string + name: string + description?: string + subject?: string + durations: PhaseDurations + isSystemTemplate: boolean + createdBy?: string + createdAt?: string +} + +// ============================================================================ +// Settings +// ============================================================================ + +export interface TeacherSettings { + defaultPhaseDurations: PhaseDurations + preferredLessonLength: number // minutes (default 45) + autoAdvancePhases: boolean + soundNotifications: boolean + showKeyboardShortcuts: boolean + highContrastMode: boolean + onboardingCompleted: boolean + selectedTemplateId?: string +} + +// ============================================================================ +// Timer State +// ============================================================================ + +export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime' + +export interface TimerState { + isRunning: boolean + isPaused: boolean + elapsedSeconds: number + remainingSeconds: number + totalSeconds: number + progress: number // 0-1 + colorStatus: TimerColorStatus + currentPhase: LessonPhase | null +} + +// ============================================================================ +// Forms +// ============================================================================ + +export interface LessonStartFormData { + classId: string + subject: string + topic?: string + templateId?: string + customDurations?: PhaseDurations +} + +export interface Class { + id: string + name: string + grade: string + studentCount: number +} + +// ============================================================================ +// Feedback +// ============================================================================ + +export type FeedbackType = 'bug' | 'feature' | 'feedback' + +export interface FeedbackSubmission { + type: FeedbackType + title: string + description: string + screenshot?: string // base64 + sessionId?: string + metadata?: Record +} + +// ============================================================================ +// Onboarding +// ============================================================================ + +export interface OnboardingStep { + step: number + title: string + description: string + completed: boolean +} + +export interface OnboardingState { + currentStep: number + totalSteps: number + steps: OnboardingStep[] + selectedState?: string // Bundesland + selectedSchoolType?: string + completed: boolean +} + +// ============================================================================ +// WebSocket Messages +// ============================================================================ + +export type WSMessageType = + | 'phase_update' + | 'timer_tick' + | 'overtime_warning' + | 'pause_toggle' + | 'session_end' + | 'sync_request' + +export interface WSMessage { + type: WSMessageType + payload: { + sessionId: string + phase?: number + elapsed?: number + isPaused?: boolean + overtimeMinutes?: number + [key: string]: unknown + } + timestamp: string +} + +// ============================================================================ +// API Responses +// ============================================================================ + +export interface APIResponse { + success: boolean + data?: T + error?: string + message?: string +} + +export interface DashboardResponse extends APIResponse {} + +export interface LessonResponse extends APIResponse {} + +export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {} + +export interface SettingsResponse extends APIResponse {} + +// ============================================================================ +// Component Props +// ============================================================================ + +export type CompanionMode = 'companion' | 'lesson' | 'classic' + +export interface ModeToggleProps { + currentMode: CompanionMode + onModeChange: (mode: CompanionMode) => void +} + +export interface PhaseTimelineProps { + phases: Phase[] + currentPhaseIndex: number + onPhaseClick?: (index: number) => void +} + +export interface VisualPieTimerProps { + progress: number // 0-1 + remainingSeconds: number + totalSeconds: number + colorStatus: TimerColorStatus + isPaused: boolean + currentPhaseName: string + phaseColor: string +} + +export interface QuickActionsBarProps { + onExtend: (minutes: number) => void + onPause: () => void + onResume: () => void + onSkip: () => void + isPaused: boolean + isLastPhase: boolean + disabled?: boolean +} diff --git a/admin-lehrer/tsconfig.json b/admin-lehrer/tsconfig.json index 2b1ea82..b66a30f 100644 --- a/admin-lehrer/tsconfig.json +++ b/admin-lehrer/tsconfig.json @@ -24,9 +24,6 @@ "paths": { "@/*": [ "./*" - ], - "@shared/*": [ - "../shared/*" ] }, "target": "ES2017" diff --git a/docker-compose.yml b/docker-compose.yml index 25ce8c3..6dac132 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -60,8 +60,8 @@ services: # ========================================================= admin-lehrer: build: - context: . - dockerfile: admin-lehrer/Dockerfile + context: ./admin-lehrer + dockerfile: Dockerfile args: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://macmini:8001} NEXT_PUBLIC_OLD_ADMIN_URL: ${NEXT_PUBLIC_OLD_ADMIN_URL:-http://macmini:3000/admin} @@ -95,8 +95,8 @@ services: studio-v2: build: - context: . - dockerfile: studio-v2/Dockerfile + context: ./studio-v2 + dockerfile: Dockerfile args: NEXT_PUBLIC_VOICE_SERVICE_URL: ${NEXT_PUBLIC_VOICE_SERVICE_URL:-wss://macmini:8091} NEXT_PUBLIC_KLAUSUR_SERVICE_URL: ${NEXT_PUBLIC_KLAUSUR_SERVICE_URL:-https://macmini:8086} @@ -116,8 +116,8 @@ services: website: build: - context: . - dockerfile: website/Dockerfile + context: ./website + dockerfile: Dockerfile args: NEXT_PUBLIC_BILLING_API_URL: ${NEXT_PUBLIC_BILLING_API_URL:-https://macmini:8083} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-https://macmini} diff --git a/scripts/sync-shared-types.sh b/scripts/sync-shared-types.sh new file mode 100755 index 0000000..a96bb85 --- /dev/null +++ b/scripts/sync-shared-types.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Sync shared types from shared/types/ into each frontend service. +# +# The canonical source of truth is shared/types/*.ts. +# This script copies them into each service's local directory +# because Turbopack doesn't support tsconfig path aliases +# pointing outside the project root. +# +# Run this after modifying any file in shared/types/. + +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +SHARED="$ROOT/shared/types" + +echo "Syncing shared types from $SHARED..." + +# Companion types +cp "$SHARED/companion.ts" "$ROOT/admin-lehrer/lib/companion/types.ts" +cp "$SHARED/companion.ts" "$ROOT/studio-v2/lib/companion/types.ts" + +# Klausur-Korrektur types +cp "$SHARED/klausur.ts" "$ROOT/admin-lehrer/app/(admin)/education/klausur-korrektur/types.ts" +cp "$SHARED/klausur.ts" "$ROOT/studio-v2/app/korrektur/types.ts" +cp "$SHARED/klausur.ts" "$ROOT/website/app/admin/klausur-korrektur/types.ts" +cp "$SHARED/klausur.ts" "$ROOT/website/app/lehrer/klausur-korrektur/types.ts" +cp "$SHARED/klausur.ts" "$ROOT/website/components/klausur-korrektur/klausur-types.ts" + +# OCR Labeling types +cp "$SHARED/ocr-labeling.ts" "$ROOT/admin-lehrer/app/(admin)/ai/ocr-labeling/types.ts" +cp "$SHARED/ocr-labeling.ts" "$ROOT/website/app/admin/ocr-labeling/types.ts" + +echo "Done. Synced to 9 locations." diff --git a/studio-v2/.gitignore b/studio-v2/.gitignore deleted file mode 100644 index 9867a52..0000000 --- a/studio-v2/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Symlink to shared/ (Docker COPY handles this in container) -shared diff --git a/studio-v2/Dockerfile b/studio-v2/Dockerfile index 483948b..5937223 100644 --- a/studio-v2/Dockerfile +++ b/studio-v2/Dockerfile @@ -4,14 +4,13 @@ FROM node:20-alpine AS builder WORKDIR /app # Copy package files -COPY studio-v2/package.json studio-v2/package-lock.json* ./ +COPY package.json package-lock.json* ./ # Install dependencies RUN npm install -# Copy source files + shared types (inside project for Turbopack) -COPY studio-v2/ . -COPY shared/ ./shared/ +# Copy source files +COPY . . # Build arguments for environment variables (needed at build time for Next.js) ARG NEXT_PUBLIC_VOICE_SERVICE_URL diff --git a/studio-v2/app/korrektur/types.ts b/studio-v2/app/korrektur/types.ts index 4def074..e867255 100644 --- a/studio-v2/app/korrektur/types.ts +++ b/studio-v2/app/korrektur/types.ts @@ -1 +1,432 @@ -export * from '@shared/types/klausur' +/** + * Shared Klausur-Korrektur types and constants. + * + * This is the single source of truth used by: + * - admin-lehrer (education/klausur-korrektur) + * - studio-v2 (korrektur) + * - website/admin (klausur-korrektur) + * - website/lehrer (klausur-korrektur) + */ + +// --------------------------------------------------------------------------- +// Core domain interfaces +// --------------------------------------------------------------------------- + +export interface Klausur { + id: string + title: string + subject: string + year: number + semester: string + modus: KlausurModus + eh_id?: string + created_at: string + student_count?: number + completed_count?: number + status?: 'draft' | 'in_progress' | 'completed' +} + +/** Union of all modus values used across services */ +export type KlausurModus = 'abitur' | 'vorabitur' | 'landes_abitur' + +export interface StudentWork { + id: string + klausur_id: string + anonym_id: string + file_path: string + file_type: 'pdf' | 'image' + ocr_text: string + criteria_scores: CriteriaScores + gutachten: string + status: StudentStatus + raw_points: number + grade_points: number + grade_label?: string + created_at: string + examiner_id?: string + second_examiner_id?: string + second_examiner_grade?: number +} + +export type StudentStatus = + | 'UPLOADED' + | 'OCR_PROCESSING' + | 'OCR_COMPLETE' + | 'ANALYZING' + | 'FIRST_EXAMINER' + | 'SECOND_EXAMINER' + | 'COMPLETED' + | 'ERROR' + +export interface CriteriaScores { + rechtschreibung?: number + grammatik?: number + inhalt?: number + struktur?: number + stil?: number + [key: string]: number | undefined +} + +export interface Criterion { + id: string + name: string + weight: number + description?: string +} + +export interface GradeInfo { + thresholds: Record + labels: Record + criteria: Record +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + +export interface Annotation { + id: string + student_work_id: string + page: number + position: AnnotationPosition + type: AnnotationType + text: string + severity: 'minor' | 'major' | 'critical' + suggestion?: string + created_by: string + created_at: string + role: 'first_examiner' | 'second_examiner' + linked_criterion?: string +} + +export interface AnnotationPosition { + x: number // Prozent (0-100) + y: number // Prozent (0-100) + width: number // Prozent (0-100) + height: number // Prozent (0-100) +} + +export type AnnotationType = + | 'rechtschreibung' + | 'grammatik' + | 'inhalt' + | 'struktur' + | 'stil' + | 'comment' + | 'highlight' + +// --------------------------------------------------------------------------- +// Fairness analysis +// --------------------------------------------------------------------------- + +export interface FairnessAnalysis { + klausur_id: string + student_count: number + average_grade: number + std_deviation: number + spread: number + outliers: OutlierInfo[] + criteria_analysis: Record + fairness_score: number + warnings: string[] +} + +export interface OutlierInfo { + student_id: string + anonym_id: string + grade_points: number + deviation: number + reason: string +} + +export interface CriteriaStats { + min: number + max: number + average: number + std_deviation: number +} + +// --------------------------------------------------------------------------- +// EH suggestions +// --------------------------------------------------------------------------- + +export interface EHSuggestion { + criterion: string + excerpt: string + relevance_score: number + source_chunk_id: string + // Attribution fields (CTRL-SRC-002) + source_document?: string + source_url?: string + license?: string + license_url?: string + publisher?: string +} + +/** Default Attribution for NiBiS documents (CTRL-SRC-002) */ +export const NIBIS_ATTRIBUTION = { + publisher: 'Niedersaechsischer Bildungsserver (NiBiS)', + license: 'DL-DE-BY-2.0', + license_url: 'https://www.govdata.de/dl-de/by-2-0', + source_url: 'https://nibis.de', +} as const + +// --------------------------------------------------------------------------- +// Gutachten +// --------------------------------------------------------------------------- + +export interface GutachtenSection { + title: string + content: string + evidence_links?: string[] +} + +export interface Gutachten { + einleitung: string + hauptteil: string + fazit: string + staerken: string[] + schwaechen: string[] + generated_at?: string +} + +// --------------------------------------------------------------------------- +// API response types +// --------------------------------------------------------------------------- + +export interface KlausurenResponse { + klausuren: Klausur[] + total: number +} + +export interface StudentsResponse { + students: StudentWork[] + total: number +} + +export interface AnnotationsResponse { + annotations: Annotation[] +} + +// --------------------------------------------------------------------------- +// Create / update types +// --------------------------------------------------------------------------- + +export interface CreateKlausurData { + title: string + subject?: string + year?: number + semester?: string + modus?: KlausurModus +} + +// --------------------------------------------------------------------------- +// Constants — annotation colors +// --------------------------------------------------------------------------- + +export const ANNOTATION_COLORS: Record = { + rechtschreibung: '#dc2626', // Red + grammatik: '#2563eb', // Blue + inhalt: '#16a34a', // Green + struktur: '#9333ea', // Purple + stil: '#ea580c', // Orange + comment: '#6b7280', // Gray + highlight: '#eab308', // Yellow +} + +// --------------------------------------------------------------------------- +// Constants — status colors & labels +// --------------------------------------------------------------------------- + +export const STATUS_COLORS: Record = { + UPLOADED: '#6b7280', + OCR_PROCESSING: '#eab308', + OCR_COMPLETE: '#3b82f6', + ANALYZING: '#8b5cf6', + FIRST_EXAMINER: '#f97316', + SECOND_EXAMINER: '#06b6d4', + COMPLETED: '#22c55e', + ERROR: '#ef4444', +} + +export const STATUS_LABELS: Record = { + UPLOADED: 'Hochgeladen', + OCR_PROCESSING: 'OCR laeuft', + OCR_COMPLETE: 'OCR fertig', + ANALYZING: 'Analyse laeuft', + FIRST_EXAMINER: 'Erstkorrektur', + SECOND_EXAMINER: 'Zweitkorrektur', + COMPLETED: 'Abgeschlossen', + ERROR: 'Fehler', +} + +// --------------------------------------------------------------------------- +// Constants — criteria & grades +// --------------------------------------------------------------------------- + +/** Default criteria with weights (Niedersachsen standard) */ +export const DEFAULT_CRITERIA: Record = { + rechtschreibung: { name: 'Rechtschreibung', weight: 15 }, + grammatik: { name: 'Grammatik', weight: 15 }, + inhalt: { name: 'Inhalt', weight: 40 }, + struktur: { name: 'Struktur', weight: 15 }, + stil: { name: 'Stil', weight: 15 }, +} + +/** Grade thresholds (15-point system) */ +export const GRADE_THRESHOLDS: Record = { + 15: 95, 14: 90, 13: 85, 12: 80, 11: 75, + 10: 70, 9: 65, 8: 60, 7: 55, 6: 50, + 5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0, +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/** Calculate grade points from a percentage (0-100). */ +export function calculateGrade(percentage: number): number { + for (const [grade, threshold] of Object.entries(GRADE_THRESHOLDS).sort( + (a, b) => Number(b[0]) - Number(a[0]), + )) { + if (percentage >= threshold) { + return Number(grade) + } + } + return 0 +} + +/** Human-readable label for a 15-point grade value. */ +export function getGradeLabel(points: number): string { + const labels: Record = { + 15: '1+', 14: '1', 13: '1-', + 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', + 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', + 0: '6', + } + return labels[points] || String(points) +} + +// --------------------------------------------------------------------------- +// Examiner workflow types (workspace) +// --------------------------------------------------------------------------- + +export interface ExaminerInfo { + id: string + assigned_at: string + notes?: string +} + +export interface ExaminerResult { + grade_points: number + criteria_scores?: CriteriaScores + notes?: string + submitted_at: string +} + +export interface ExaminerWorkflow { + student_id: string + workflow_status: string + visibility_mode: string + user_role: 'ek' | 'zk' | 'dk' | 'viewer' + first_examiner?: ExaminerInfo + second_examiner?: ExaminerInfo + third_examiner?: ExaminerInfo + first_result?: ExaminerResult + first_result_visible?: boolean + second_result?: ExaminerResult + third_result?: ExaminerResult + grade_difference?: number + final_grade?: number + consensus_reached?: boolean + consensus_type?: string + einigung?: { + final_grade: number + notes: string + type: string + submitted_by: string + submitted_at: string + ek_grade: number + zk_grade: number + } + drittkorrektur_reason?: string +} + +export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege' + +export interface GradeTotals { + raw: number + weighted: number + gradePoints: number +} + +// --------------------------------------------------------------------------- +// Constants — workflow status & roles +// --------------------------------------------------------------------------- + +export const GRADE_LABELS: Record = { + 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', 0: '6', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + not_started: { label: 'Nicht gestartet', color: 'bg-slate-100 text-slate-700' }, + ek_in_progress: { label: 'EK in Arbeit', color: 'bg-blue-100 text-blue-700' }, + ek_completed: { label: 'EK abgeschlossen', color: 'bg-blue-200 text-blue-800' }, + zk_assigned: { label: 'ZK zugewiesen', color: 'bg-amber-100 text-amber-700' }, + zk_in_progress: { label: 'ZK in Arbeit', color: 'bg-amber-200 text-amber-800' }, + zk_completed: { label: 'ZK abgeschlossen', color: 'bg-amber-300 text-amber-900' }, + einigung_required: { label: 'Einigung erforderlich', color: 'bg-orange-100 text-orange-700' }, + einigung_completed: { label: 'Einigung abgeschlossen', color: 'bg-green-100 text-green-700' }, + drittkorrektur_required: { label: 'DK erforderlich', color: 'bg-red-100 text-red-700' }, + drittkorrektur_assigned: { label: 'DK zugewiesen', color: 'bg-red-200 text-red-800' }, + drittkorrektur_in_progress: { label: 'DK in Arbeit', color: 'bg-red-300 text-red-900' }, + completed: { label: 'Abgeschlossen', color: 'bg-green-200 text-green-800' }, +} + +export const ROLE_LABELS: Record = { + ek: { label: 'Erstkorrektor', color: 'bg-blue-500' }, + zk: { label: 'Zweitkorrektor', color: 'bg-amber-500' }, + dk: { label: 'Drittkorrektor', color: 'bg-purple-500' }, + viewer: { label: 'Betrachter', color: 'bg-slate-500' }, +} + +// --------------------------------------------------------------------------- +// Form types (create / upload) +// --------------------------------------------------------------------------- + +export interface CreateKlausurForm { + title: string + subject: string + year: number + semester: string + modus: 'abitur' | 'vorabitur' +} + +export interface VorabiturEHForm { + aufgabentyp: string + titel: string + text_titel: string + text_autor: string + aufgabenstellung: string +} + +export interface EHTemplate { + aufgabentyp: string + name: string + description: string + category: string +} + +export interface DirektuploadForm { + files: File[] + ehFile: File | null + ehText: string + aufgabentyp: string + klausurTitle: string +} + +export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken' diff --git a/studio-v2/lib/companion/types.ts b/studio-v2/lib/companion/types.ts index 90ba262..5a4405b 100644 --- a/studio-v2/lib/companion/types.ts +++ b/studio-v2/lib/companion/types.ts @@ -1 +1,329 @@ -export * from '@shared/types/companion' +/** + * TypeScript Types for Companion Module + * Migration from Flask companion.py/companion_js.py + */ + +// ============================================================================ +// Phase System +// ============================================================================ + +export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion' + +export interface Phase { + id: PhaseId + shortName: string // E, A, S, T, R + displayName: string + duration: number // minutes + status: 'planned' | 'active' | 'completed' + actualTime?: number // seconds (actual time spent) + color: string // hex color +} + +export interface PhaseContext { + currentPhase: PhaseId + phaseDisplayName: string +} + +// ============================================================================ +// Dashboard / Companion Mode +// ============================================================================ + +export interface CompanionStats { + classesCount: number + studentsCount: number + learningUnitsCreated: number + gradesEntered: number +} + +export interface Progress { + percentage: number + completed: number + total: number +} + +export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low' + +export interface Suggestion { + id: string + title: string + description: string + priority: SuggestionPriority + icon: string // lucide icon name + actionTarget: string // navigation path + estimatedTime: number // minutes +} + +export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other' + +export interface UpcomingEvent { + id: string + title: string + date: string // ISO date string + type: EventType + inDays: number +} + +export interface CompanionData { + context: PhaseContext + stats: CompanionStats + phases: Phase[] + progress: Progress + suggestions: Suggestion[] + upcomingEvents: UpcomingEvent[] +} + +// ============================================================================ +// Lesson Mode +// ============================================================================ + +export type LessonStatus = + | 'not_started' + | 'in_progress' + | 'paused' + | 'completed' + | 'overtime' + +export interface LessonPhase { + phase: PhaseId + duration: number // planned duration in minutes + status: 'planned' | 'active' | 'completed' | 'skipped' + actualTime: number // actual time spent in seconds + startedAt?: string // ISO timestamp + completedAt?: string // ISO timestamp +} + +export interface Homework { + id: string + title: string + description?: string + dueDate: string // ISO date + attachments?: string[] + completed?: boolean +} + +export interface Material { + id: string + title: string + type: 'document' | 'video' | 'presentation' | 'link' | 'other' + url?: string + fileName?: string +} + +export interface LessonReflection { + rating: number // 1-5 stars + notes: string + nextSteps: string + savedAt?: string +} + +export interface LessonSession { + sessionId: string + classId: string + className: string + subject: string + topic?: string + startTime: string // ISO timestamp + endTime?: string // ISO timestamp + phases: LessonPhase[] + totalPlannedDuration: number // minutes + currentPhaseIndex: number + elapsedTime: number // seconds + isPaused: boolean + pausedAt?: string + pauseDuration: number // total pause time in seconds + overtimeMinutes: number + status: LessonStatus + homeworkList: Homework[] + materials: Material[] + reflection?: LessonReflection +} + +// ============================================================================ +// Lesson Templates +// ============================================================================ + +export interface PhaseDurations { + einstieg: number + erarbeitung: number + sicherung: number + transfer: number + reflexion: number +} + +export interface LessonTemplate { + templateId: string + name: string + description?: string + subject?: string + durations: PhaseDurations + isSystemTemplate: boolean + createdBy?: string + createdAt?: string +} + +// ============================================================================ +// Settings +// ============================================================================ + +export interface TeacherSettings { + defaultPhaseDurations: PhaseDurations + preferredLessonLength: number // minutes (default 45) + autoAdvancePhases: boolean + soundNotifications: boolean + showKeyboardShortcuts: boolean + highContrastMode: boolean + onboardingCompleted: boolean + selectedTemplateId?: string +} + +// ============================================================================ +// Timer State +// ============================================================================ + +export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime' + +export interface TimerState { + isRunning: boolean + isPaused: boolean + elapsedSeconds: number + remainingSeconds: number + totalSeconds: number + progress: number // 0-1 + colorStatus: TimerColorStatus + currentPhase: LessonPhase | null +} + +// ============================================================================ +// Forms +// ============================================================================ + +export interface LessonStartFormData { + classId: string + subject: string + topic?: string + templateId?: string + customDurations?: PhaseDurations +} + +export interface Class { + id: string + name: string + grade: string + studentCount: number +} + +// ============================================================================ +// Feedback +// ============================================================================ + +export type FeedbackType = 'bug' | 'feature' | 'feedback' + +export interface FeedbackSubmission { + type: FeedbackType + title: string + description: string + screenshot?: string // base64 + sessionId?: string + metadata?: Record +} + +// ============================================================================ +// Onboarding +// ============================================================================ + +export interface OnboardingStep { + step: number + title: string + description: string + completed: boolean +} + +export interface OnboardingState { + currentStep: number + totalSteps: number + steps: OnboardingStep[] + selectedState?: string // Bundesland + selectedSchoolType?: string + completed: boolean +} + +// ============================================================================ +// WebSocket Messages +// ============================================================================ + +export type WSMessageType = + | 'phase_update' + | 'timer_tick' + | 'overtime_warning' + | 'pause_toggle' + | 'session_end' + | 'sync_request' + +export interface WSMessage { + type: WSMessageType + payload: { + sessionId: string + phase?: number + elapsed?: number + isPaused?: boolean + overtimeMinutes?: number + [key: string]: unknown + } + timestamp: string +} + +// ============================================================================ +// API Responses +// ============================================================================ + +export interface APIResponse { + success: boolean + data?: T + error?: string + message?: string +} + +export interface DashboardResponse extends APIResponse {} + +export interface LessonResponse extends APIResponse {} + +export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {} + +export interface SettingsResponse extends APIResponse {} + +// ============================================================================ +// Component Props +// ============================================================================ + +export type CompanionMode = 'companion' | 'lesson' | 'classic' + +export interface ModeToggleProps { + currentMode: CompanionMode + onModeChange: (mode: CompanionMode) => void +} + +export interface PhaseTimelineProps { + phases: Phase[] + currentPhaseIndex: number + onPhaseClick?: (index: number) => void +} + +export interface VisualPieTimerProps { + progress: number // 0-1 + remainingSeconds: number + totalSeconds: number + colorStatus: TimerColorStatus + isPaused: boolean + currentPhaseName: string + phaseColor: string +} + +export interface QuickActionsBarProps { + onExtend: (minutes: number) => void + onPause: () => void + onResume: () => void + onSkip: () => void + isPaused: boolean + isLastPhase: boolean + disabled?: boolean +} diff --git a/studio-v2/tsconfig.json b/studio-v2/tsconfig.json index 3d016ab..9e9bbf7 100644 --- a/studio-v2/tsconfig.json +++ b/studio-v2/tsconfig.json @@ -24,9 +24,6 @@ "paths": { "@/*": [ "./*" - ], - "@shared/*": [ - "../shared/*" ] }, "target": "ES2017" diff --git a/website/.gitignore b/website/.gitignore deleted file mode 100644 index 9867a52..0000000 --- a/website/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Symlink to shared/ (Docker COPY handles this in container) -shared diff --git a/website/Dockerfile b/website/Dockerfile index 3c93ba2..a4a1735 100644 --- a/website/Dockerfile +++ b/website/Dockerfile @@ -4,14 +4,13 @@ FROM node:20-alpine AS builder WORKDIR /app # Copy package files -COPY website/package.json website/package-lock.json* ./ +COPY package.json package-lock.json* ./ # Install dependencies RUN npm install -# Copy source code + shared types (inside project for Turbopack) -COPY website/ . -COPY shared/ ./shared/ +# Copy source code +COPY . . # Build arguments for environment variables ARG NEXT_PUBLIC_BILLING_API_URL diff --git a/website/app/admin/klausur-korrektur/types.ts b/website/app/admin/klausur-korrektur/types.ts index 4def074..e867255 100644 --- a/website/app/admin/klausur-korrektur/types.ts +++ b/website/app/admin/klausur-korrektur/types.ts @@ -1 +1,432 @@ -export * from '@shared/types/klausur' +/** + * Shared Klausur-Korrektur types and constants. + * + * This is the single source of truth used by: + * - admin-lehrer (education/klausur-korrektur) + * - studio-v2 (korrektur) + * - website/admin (klausur-korrektur) + * - website/lehrer (klausur-korrektur) + */ + +// --------------------------------------------------------------------------- +// Core domain interfaces +// --------------------------------------------------------------------------- + +export interface Klausur { + id: string + title: string + subject: string + year: number + semester: string + modus: KlausurModus + eh_id?: string + created_at: string + student_count?: number + completed_count?: number + status?: 'draft' | 'in_progress' | 'completed' +} + +/** Union of all modus values used across services */ +export type KlausurModus = 'abitur' | 'vorabitur' | 'landes_abitur' + +export interface StudentWork { + id: string + klausur_id: string + anonym_id: string + file_path: string + file_type: 'pdf' | 'image' + ocr_text: string + criteria_scores: CriteriaScores + gutachten: string + status: StudentStatus + raw_points: number + grade_points: number + grade_label?: string + created_at: string + examiner_id?: string + second_examiner_id?: string + second_examiner_grade?: number +} + +export type StudentStatus = + | 'UPLOADED' + | 'OCR_PROCESSING' + | 'OCR_COMPLETE' + | 'ANALYZING' + | 'FIRST_EXAMINER' + | 'SECOND_EXAMINER' + | 'COMPLETED' + | 'ERROR' + +export interface CriteriaScores { + rechtschreibung?: number + grammatik?: number + inhalt?: number + struktur?: number + stil?: number + [key: string]: number | undefined +} + +export interface Criterion { + id: string + name: string + weight: number + description?: string +} + +export interface GradeInfo { + thresholds: Record + labels: Record + criteria: Record +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + +export interface Annotation { + id: string + student_work_id: string + page: number + position: AnnotationPosition + type: AnnotationType + text: string + severity: 'minor' | 'major' | 'critical' + suggestion?: string + created_by: string + created_at: string + role: 'first_examiner' | 'second_examiner' + linked_criterion?: string +} + +export interface AnnotationPosition { + x: number // Prozent (0-100) + y: number // Prozent (0-100) + width: number // Prozent (0-100) + height: number // Prozent (0-100) +} + +export type AnnotationType = + | 'rechtschreibung' + | 'grammatik' + | 'inhalt' + | 'struktur' + | 'stil' + | 'comment' + | 'highlight' + +// --------------------------------------------------------------------------- +// Fairness analysis +// --------------------------------------------------------------------------- + +export interface FairnessAnalysis { + klausur_id: string + student_count: number + average_grade: number + std_deviation: number + spread: number + outliers: OutlierInfo[] + criteria_analysis: Record + fairness_score: number + warnings: string[] +} + +export interface OutlierInfo { + student_id: string + anonym_id: string + grade_points: number + deviation: number + reason: string +} + +export interface CriteriaStats { + min: number + max: number + average: number + std_deviation: number +} + +// --------------------------------------------------------------------------- +// EH suggestions +// --------------------------------------------------------------------------- + +export interface EHSuggestion { + criterion: string + excerpt: string + relevance_score: number + source_chunk_id: string + // Attribution fields (CTRL-SRC-002) + source_document?: string + source_url?: string + license?: string + license_url?: string + publisher?: string +} + +/** Default Attribution for NiBiS documents (CTRL-SRC-002) */ +export const NIBIS_ATTRIBUTION = { + publisher: 'Niedersaechsischer Bildungsserver (NiBiS)', + license: 'DL-DE-BY-2.0', + license_url: 'https://www.govdata.de/dl-de/by-2-0', + source_url: 'https://nibis.de', +} as const + +// --------------------------------------------------------------------------- +// Gutachten +// --------------------------------------------------------------------------- + +export interface GutachtenSection { + title: string + content: string + evidence_links?: string[] +} + +export interface Gutachten { + einleitung: string + hauptteil: string + fazit: string + staerken: string[] + schwaechen: string[] + generated_at?: string +} + +// --------------------------------------------------------------------------- +// API response types +// --------------------------------------------------------------------------- + +export interface KlausurenResponse { + klausuren: Klausur[] + total: number +} + +export interface StudentsResponse { + students: StudentWork[] + total: number +} + +export interface AnnotationsResponse { + annotations: Annotation[] +} + +// --------------------------------------------------------------------------- +// Create / update types +// --------------------------------------------------------------------------- + +export interface CreateKlausurData { + title: string + subject?: string + year?: number + semester?: string + modus?: KlausurModus +} + +// --------------------------------------------------------------------------- +// Constants — annotation colors +// --------------------------------------------------------------------------- + +export const ANNOTATION_COLORS: Record = { + rechtschreibung: '#dc2626', // Red + grammatik: '#2563eb', // Blue + inhalt: '#16a34a', // Green + struktur: '#9333ea', // Purple + stil: '#ea580c', // Orange + comment: '#6b7280', // Gray + highlight: '#eab308', // Yellow +} + +// --------------------------------------------------------------------------- +// Constants — status colors & labels +// --------------------------------------------------------------------------- + +export const STATUS_COLORS: Record = { + UPLOADED: '#6b7280', + OCR_PROCESSING: '#eab308', + OCR_COMPLETE: '#3b82f6', + ANALYZING: '#8b5cf6', + FIRST_EXAMINER: '#f97316', + SECOND_EXAMINER: '#06b6d4', + COMPLETED: '#22c55e', + ERROR: '#ef4444', +} + +export const STATUS_LABELS: Record = { + UPLOADED: 'Hochgeladen', + OCR_PROCESSING: 'OCR laeuft', + OCR_COMPLETE: 'OCR fertig', + ANALYZING: 'Analyse laeuft', + FIRST_EXAMINER: 'Erstkorrektur', + SECOND_EXAMINER: 'Zweitkorrektur', + COMPLETED: 'Abgeschlossen', + ERROR: 'Fehler', +} + +// --------------------------------------------------------------------------- +// Constants — criteria & grades +// --------------------------------------------------------------------------- + +/** Default criteria with weights (Niedersachsen standard) */ +export const DEFAULT_CRITERIA: Record = { + rechtschreibung: { name: 'Rechtschreibung', weight: 15 }, + grammatik: { name: 'Grammatik', weight: 15 }, + inhalt: { name: 'Inhalt', weight: 40 }, + struktur: { name: 'Struktur', weight: 15 }, + stil: { name: 'Stil', weight: 15 }, +} + +/** Grade thresholds (15-point system) */ +export const GRADE_THRESHOLDS: Record = { + 15: 95, 14: 90, 13: 85, 12: 80, 11: 75, + 10: 70, 9: 65, 8: 60, 7: 55, 6: 50, + 5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0, +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/** Calculate grade points from a percentage (0-100). */ +export function calculateGrade(percentage: number): number { + for (const [grade, threshold] of Object.entries(GRADE_THRESHOLDS).sort( + (a, b) => Number(b[0]) - Number(a[0]), + )) { + if (percentage >= threshold) { + return Number(grade) + } + } + return 0 +} + +/** Human-readable label for a 15-point grade value. */ +export function getGradeLabel(points: number): string { + const labels: Record = { + 15: '1+', 14: '1', 13: '1-', + 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', + 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', + 0: '6', + } + return labels[points] || String(points) +} + +// --------------------------------------------------------------------------- +// Examiner workflow types (workspace) +// --------------------------------------------------------------------------- + +export interface ExaminerInfo { + id: string + assigned_at: string + notes?: string +} + +export interface ExaminerResult { + grade_points: number + criteria_scores?: CriteriaScores + notes?: string + submitted_at: string +} + +export interface ExaminerWorkflow { + student_id: string + workflow_status: string + visibility_mode: string + user_role: 'ek' | 'zk' | 'dk' | 'viewer' + first_examiner?: ExaminerInfo + second_examiner?: ExaminerInfo + third_examiner?: ExaminerInfo + first_result?: ExaminerResult + first_result_visible?: boolean + second_result?: ExaminerResult + third_result?: ExaminerResult + grade_difference?: number + final_grade?: number + consensus_reached?: boolean + consensus_type?: string + einigung?: { + final_grade: number + notes: string + type: string + submitted_by: string + submitted_at: string + ek_grade: number + zk_grade: number + } + drittkorrektur_reason?: string +} + +export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege' + +export interface GradeTotals { + raw: number + weighted: number + gradePoints: number +} + +// --------------------------------------------------------------------------- +// Constants — workflow status & roles +// --------------------------------------------------------------------------- + +export const GRADE_LABELS: Record = { + 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', 0: '6', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + not_started: { label: 'Nicht gestartet', color: 'bg-slate-100 text-slate-700' }, + ek_in_progress: { label: 'EK in Arbeit', color: 'bg-blue-100 text-blue-700' }, + ek_completed: { label: 'EK abgeschlossen', color: 'bg-blue-200 text-blue-800' }, + zk_assigned: { label: 'ZK zugewiesen', color: 'bg-amber-100 text-amber-700' }, + zk_in_progress: { label: 'ZK in Arbeit', color: 'bg-amber-200 text-amber-800' }, + zk_completed: { label: 'ZK abgeschlossen', color: 'bg-amber-300 text-amber-900' }, + einigung_required: { label: 'Einigung erforderlich', color: 'bg-orange-100 text-orange-700' }, + einigung_completed: { label: 'Einigung abgeschlossen', color: 'bg-green-100 text-green-700' }, + drittkorrektur_required: { label: 'DK erforderlich', color: 'bg-red-100 text-red-700' }, + drittkorrektur_assigned: { label: 'DK zugewiesen', color: 'bg-red-200 text-red-800' }, + drittkorrektur_in_progress: { label: 'DK in Arbeit', color: 'bg-red-300 text-red-900' }, + completed: { label: 'Abgeschlossen', color: 'bg-green-200 text-green-800' }, +} + +export const ROLE_LABELS: Record = { + ek: { label: 'Erstkorrektor', color: 'bg-blue-500' }, + zk: { label: 'Zweitkorrektor', color: 'bg-amber-500' }, + dk: { label: 'Drittkorrektor', color: 'bg-purple-500' }, + viewer: { label: 'Betrachter', color: 'bg-slate-500' }, +} + +// --------------------------------------------------------------------------- +// Form types (create / upload) +// --------------------------------------------------------------------------- + +export interface CreateKlausurForm { + title: string + subject: string + year: number + semester: string + modus: 'abitur' | 'vorabitur' +} + +export interface VorabiturEHForm { + aufgabentyp: string + titel: string + text_titel: string + text_autor: string + aufgabenstellung: string +} + +export interface EHTemplate { + aufgabentyp: string + name: string + description: string + category: string +} + +export interface DirektuploadForm { + files: File[] + ehFile: File | null + ehText: string + aufgabentyp: string + klausurTitle: string +} + +export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken' diff --git a/website/app/admin/ocr-labeling/types.ts b/website/app/admin/ocr-labeling/types.ts index 845e7ce..3061055 100644 --- a/website/app/admin/ocr-labeling/types.ts +++ b/website/app/admin/ocr-labeling/types.ts @@ -1 +1,127 @@ -export * from '@shared/types/ocr-labeling' +/** + * Shared TypeScript types for OCR Labeling UI. + * + * Single source of truth used by: + * - admin-lehrer (ai/ocr-labeling) + * - website (admin/ocr-labeling) + */ + +/** + * Available OCR Models + * + * - llama3.2-vision:11b: Vision LLM, beste Qualitaet bei Handschrift (Standard) + * - trocr: Microsoft TrOCR, schnell bei gedrucktem Text + * - paddleocr: PaddleOCR + LLM, 4x schneller durch Hybrid-Ansatz + * - donut: Document Understanding Transformer, strukturierte Dokumente + */ +export type OCRModel = 'llama3.2-vision:11b' | 'trocr' | 'paddleocr' | 'donut' + +export const OCR_MODEL_INFO: Record = { + 'llama3.2-vision:11b': { + label: 'Vision LLM', + description: 'Beste Qualitaet bei Handschrift', + speed: 'langsam', + }, + trocr: { + label: 'Microsoft TrOCR', + description: 'Schnell bei gedrucktem Text', + speed: 'schnell', + }, + paddleocr: { + label: 'PaddleOCR + LLM', + description: 'Hybrid-Ansatz: OCR + Strukturierung', + speed: 'sehr schnell', + }, + donut: { + label: 'Donut', + description: 'Document Understanding fuer Tabellen/Formulare', + speed: 'mittel', + }, +} + +export interface OCRSession { + id: string + name: string + source_type: 'klausur' | 'handwriting_sample' | 'scan' + description?: string + ocr_model?: OCRModel + total_items: number + labeled_items: number + confirmed_items: number + corrected_items: number + skipped_items: number + created_at: string +} + +export interface OCRItem { + id: string + session_id: string + session_name: string + image_path: string + image_url?: string + ocr_text?: string + ocr_confidence?: number + ground_truth?: string + status: 'pending' | 'confirmed' | 'corrected' | 'skipped' + metadata?: Record + created_at: string +} + +export interface OCRStats { + total_sessions?: number + session_id?: string + name?: string + total_items: number + labeled_items: number + confirmed_items: number + corrected_items: number + skipped_items?: number + pending_items: number + exportable_items?: number + accuracy_rate: number + avg_label_time_seconds?: number + progress_percent?: number +} + +export interface TrainingSample { + id: string + image_path: string + ground_truth: string + export_format: 'generic' | 'trocr' | 'llama_vision' + training_batch: string + exported_at?: string +} + +export interface CreateSessionRequest { + name: string + source_type: 'klausur' | 'handwriting_sample' | 'scan' + description?: string + ocr_model?: OCRModel +} + +export interface ConfirmRequest { + item_id: string + label_time_seconds?: number +} + +export interface CorrectRequest { + item_id: string + ground_truth: string + label_time_seconds?: number +} + +export interface ExportRequest { + export_format: 'generic' | 'trocr' | 'llama_vision' + session_id?: string + batch_id?: string +} + +export interface UploadResult { + id: string + filename: string + image_path: string + image_hash: string + ocr_text?: string + ocr_confidence?: number + status: string +} diff --git a/website/app/lehrer/klausur-korrektur/types.ts b/website/app/lehrer/klausur-korrektur/types.ts index 4def074..e867255 100644 --- a/website/app/lehrer/klausur-korrektur/types.ts +++ b/website/app/lehrer/klausur-korrektur/types.ts @@ -1 +1,432 @@ -export * from '@shared/types/klausur' +/** + * Shared Klausur-Korrektur types and constants. + * + * This is the single source of truth used by: + * - admin-lehrer (education/klausur-korrektur) + * - studio-v2 (korrektur) + * - website/admin (klausur-korrektur) + * - website/lehrer (klausur-korrektur) + */ + +// --------------------------------------------------------------------------- +// Core domain interfaces +// --------------------------------------------------------------------------- + +export interface Klausur { + id: string + title: string + subject: string + year: number + semester: string + modus: KlausurModus + eh_id?: string + created_at: string + student_count?: number + completed_count?: number + status?: 'draft' | 'in_progress' | 'completed' +} + +/** Union of all modus values used across services */ +export type KlausurModus = 'abitur' | 'vorabitur' | 'landes_abitur' + +export interface StudentWork { + id: string + klausur_id: string + anonym_id: string + file_path: string + file_type: 'pdf' | 'image' + ocr_text: string + criteria_scores: CriteriaScores + gutachten: string + status: StudentStatus + raw_points: number + grade_points: number + grade_label?: string + created_at: string + examiner_id?: string + second_examiner_id?: string + second_examiner_grade?: number +} + +export type StudentStatus = + | 'UPLOADED' + | 'OCR_PROCESSING' + | 'OCR_COMPLETE' + | 'ANALYZING' + | 'FIRST_EXAMINER' + | 'SECOND_EXAMINER' + | 'COMPLETED' + | 'ERROR' + +export interface CriteriaScores { + rechtschreibung?: number + grammatik?: number + inhalt?: number + struktur?: number + stil?: number + [key: string]: number | undefined +} + +export interface Criterion { + id: string + name: string + weight: number + description?: string +} + +export interface GradeInfo { + thresholds: Record + labels: Record + criteria: Record +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + +export interface Annotation { + id: string + student_work_id: string + page: number + position: AnnotationPosition + type: AnnotationType + text: string + severity: 'minor' | 'major' | 'critical' + suggestion?: string + created_by: string + created_at: string + role: 'first_examiner' | 'second_examiner' + linked_criterion?: string +} + +export interface AnnotationPosition { + x: number // Prozent (0-100) + y: number // Prozent (0-100) + width: number // Prozent (0-100) + height: number // Prozent (0-100) +} + +export type AnnotationType = + | 'rechtschreibung' + | 'grammatik' + | 'inhalt' + | 'struktur' + | 'stil' + | 'comment' + | 'highlight' + +// --------------------------------------------------------------------------- +// Fairness analysis +// --------------------------------------------------------------------------- + +export interface FairnessAnalysis { + klausur_id: string + student_count: number + average_grade: number + std_deviation: number + spread: number + outliers: OutlierInfo[] + criteria_analysis: Record + fairness_score: number + warnings: string[] +} + +export interface OutlierInfo { + student_id: string + anonym_id: string + grade_points: number + deviation: number + reason: string +} + +export interface CriteriaStats { + min: number + max: number + average: number + std_deviation: number +} + +// --------------------------------------------------------------------------- +// EH suggestions +// --------------------------------------------------------------------------- + +export interface EHSuggestion { + criterion: string + excerpt: string + relevance_score: number + source_chunk_id: string + // Attribution fields (CTRL-SRC-002) + source_document?: string + source_url?: string + license?: string + license_url?: string + publisher?: string +} + +/** Default Attribution for NiBiS documents (CTRL-SRC-002) */ +export const NIBIS_ATTRIBUTION = { + publisher: 'Niedersaechsischer Bildungsserver (NiBiS)', + license: 'DL-DE-BY-2.0', + license_url: 'https://www.govdata.de/dl-de/by-2-0', + source_url: 'https://nibis.de', +} as const + +// --------------------------------------------------------------------------- +// Gutachten +// --------------------------------------------------------------------------- + +export interface GutachtenSection { + title: string + content: string + evidence_links?: string[] +} + +export interface Gutachten { + einleitung: string + hauptteil: string + fazit: string + staerken: string[] + schwaechen: string[] + generated_at?: string +} + +// --------------------------------------------------------------------------- +// API response types +// --------------------------------------------------------------------------- + +export interface KlausurenResponse { + klausuren: Klausur[] + total: number +} + +export interface StudentsResponse { + students: StudentWork[] + total: number +} + +export interface AnnotationsResponse { + annotations: Annotation[] +} + +// --------------------------------------------------------------------------- +// Create / update types +// --------------------------------------------------------------------------- + +export interface CreateKlausurData { + title: string + subject?: string + year?: number + semester?: string + modus?: KlausurModus +} + +// --------------------------------------------------------------------------- +// Constants — annotation colors +// --------------------------------------------------------------------------- + +export const ANNOTATION_COLORS: Record = { + rechtschreibung: '#dc2626', // Red + grammatik: '#2563eb', // Blue + inhalt: '#16a34a', // Green + struktur: '#9333ea', // Purple + stil: '#ea580c', // Orange + comment: '#6b7280', // Gray + highlight: '#eab308', // Yellow +} + +// --------------------------------------------------------------------------- +// Constants — status colors & labels +// --------------------------------------------------------------------------- + +export const STATUS_COLORS: Record = { + UPLOADED: '#6b7280', + OCR_PROCESSING: '#eab308', + OCR_COMPLETE: '#3b82f6', + ANALYZING: '#8b5cf6', + FIRST_EXAMINER: '#f97316', + SECOND_EXAMINER: '#06b6d4', + COMPLETED: '#22c55e', + ERROR: '#ef4444', +} + +export const STATUS_LABELS: Record = { + UPLOADED: 'Hochgeladen', + OCR_PROCESSING: 'OCR laeuft', + OCR_COMPLETE: 'OCR fertig', + ANALYZING: 'Analyse laeuft', + FIRST_EXAMINER: 'Erstkorrektur', + SECOND_EXAMINER: 'Zweitkorrektur', + COMPLETED: 'Abgeschlossen', + ERROR: 'Fehler', +} + +// --------------------------------------------------------------------------- +// Constants — criteria & grades +// --------------------------------------------------------------------------- + +/** Default criteria with weights (Niedersachsen standard) */ +export const DEFAULT_CRITERIA: Record = { + rechtschreibung: { name: 'Rechtschreibung', weight: 15 }, + grammatik: { name: 'Grammatik', weight: 15 }, + inhalt: { name: 'Inhalt', weight: 40 }, + struktur: { name: 'Struktur', weight: 15 }, + stil: { name: 'Stil', weight: 15 }, +} + +/** Grade thresholds (15-point system) */ +export const GRADE_THRESHOLDS: Record = { + 15: 95, 14: 90, 13: 85, 12: 80, 11: 75, + 10: 70, 9: 65, 8: 60, 7: 55, 6: 50, + 5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0, +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/** Calculate grade points from a percentage (0-100). */ +export function calculateGrade(percentage: number): number { + for (const [grade, threshold] of Object.entries(GRADE_THRESHOLDS).sort( + (a, b) => Number(b[0]) - Number(a[0]), + )) { + if (percentage >= threshold) { + return Number(grade) + } + } + return 0 +} + +/** Human-readable label for a 15-point grade value. */ +export function getGradeLabel(points: number): string { + const labels: Record = { + 15: '1+', 14: '1', 13: '1-', + 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', + 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', + 0: '6', + } + return labels[points] || String(points) +} + +// --------------------------------------------------------------------------- +// Examiner workflow types (workspace) +// --------------------------------------------------------------------------- + +export interface ExaminerInfo { + id: string + assigned_at: string + notes?: string +} + +export interface ExaminerResult { + grade_points: number + criteria_scores?: CriteriaScores + notes?: string + submitted_at: string +} + +export interface ExaminerWorkflow { + student_id: string + workflow_status: string + visibility_mode: string + user_role: 'ek' | 'zk' | 'dk' | 'viewer' + first_examiner?: ExaminerInfo + second_examiner?: ExaminerInfo + third_examiner?: ExaminerInfo + first_result?: ExaminerResult + first_result_visible?: boolean + second_result?: ExaminerResult + third_result?: ExaminerResult + grade_difference?: number + final_grade?: number + consensus_reached?: boolean + consensus_type?: string + einigung?: { + final_grade: number + notes: string + type: string + submitted_by: string + submitted_at: string + ek_grade: number + zk_grade: number + } + drittkorrektur_reason?: string +} + +export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege' + +export interface GradeTotals { + raw: number + weighted: number + gradePoints: number +} + +// --------------------------------------------------------------------------- +// Constants — workflow status & roles +// --------------------------------------------------------------------------- + +export const GRADE_LABELS: Record = { + 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', 0: '6', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + not_started: { label: 'Nicht gestartet', color: 'bg-slate-100 text-slate-700' }, + ek_in_progress: { label: 'EK in Arbeit', color: 'bg-blue-100 text-blue-700' }, + ek_completed: { label: 'EK abgeschlossen', color: 'bg-blue-200 text-blue-800' }, + zk_assigned: { label: 'ZK zugewiesen', color: 'bg-amber-100 text-amber-700' }, + zk_in_progress: { label: 'ZK in Arbeit', color: 'bg-amber-200 text-amber-800' }, + zk_completed: { label: 'ZK abgeschlossen', color: 'bg-amber-300 text-amber-900' }, + einigung_required: { label: 'Einigung erforderlich', color: 'bg-orange-100 text-orange-700' }, + einigung_completed: { label: 'Einigung abgeschlossen', color: 'bg-green-100 text-green-700' }, + drittkorrektur_required: { label: 'DK erforderlich', color: 'bg-red-100 text-red-700' }, + drittkorrektur_assigned: { label: 'DK zugewiesen', color: 'bg-red-200 text-red-800' }, + drittkorrektur_in_progress: { label: 'DK in Arbeit', color: 'bg-red-300 text-red-900' }, + completed: { label: 'Abgeschlossen', color: 'bg-green-200 text-green-800' }, +} + +export const ROLE_LABELS: Record = { + ek: { label: 'Erstkorrektor', color: 'bg-blue-500' }, + zk: { label: 'Zweitkorrektor', color: 'bg-amber-500' }, + dk: { label: 'Drittkorrektor', color: 'bg-purple-500' }, + viewer: { label: 'Betrachter', color: 'bg-slate-500' }, +} + +// --------------------------------------------------------------------------- +// Form types (create / upload) +// --------------------------------------------------------------------------- + +export interface CreateKlausurForm { + title: string + subject: string + year: number + semester: string + modus: 'abitur' | 'vorabitur' +} + +export interface VorabiturEHForm { + aufgabentyp: string + titel: string + text_titel: string + text_autor: string + aufgabenstellung: string +} + +export interface EHTemplate { + aufgabentyp: string + name: string + description: string + category: string +} + +export interface DirektuploadForm { + files: File[] + ehFile: File | null + ehText: string + aufgabentyp: string + klausurTitle: string +} + +export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken' diff --git a/website/components/klausur-korrektur/klausur-types.ts b/website/components/klausur-korrektur/klausur-types.ts new file mode 100644 index 0000000..e867255 --- /dev/null +++ b/website/components/klausur-korrektur/klausur-types.ts @@ -0,0 +1,432 @@ +/** + * Shared Klausur-Korrektur types and constants. + * + * This is the single source of truth used by: + * - admin-lehrer (education/klausur-korrektur) + * - studio-v2 (korrektur) + * - website/admin (klausur-korrektur) + * - website/lehrer (klausur-korrektur) + */ + +// --------------------------------------------------------------------------- +// Core domain interfaces +// --------------------------------------------------------------------------- + +export interface Klausur { + id: string + title: string + subject: string + year: number + semester: string + modus: KlausurModus + eh_id?: string + created_at: string + student_count?: number + completed_count?: number + status?: 'draft' | 'in_progress' | 'completed' +} + +/** Union of all modus values used across services */ +export type KlausurModus = 'abitur' | 'vorabitur' | 'landes_abitur' + +export interface StudentWork { + id: string + klausur_id: string + anonym_id: string + file_path: string + file_type: 'pdf' | 'image' + ocr_text: string + criteria_scores: CriteriaScores + gutachten: string + status: StudentStatus + raw_points: number + grade_points: number + grade_label?: string + created_at: string + examiner_id?: string + second_examiner_id?: string + second_examiner_grade?: number +} + +export type StudentStatus = + | 'UPLOADED' + | 'OCR_PROCESSING' + | 'OCR_COMPLETE' + | 'ANALYZING' + | 'FIRST_EXAMINER' + | 'SECOND_EXAMINER' + | 'COMPLETED' + | 'ERROR' + +export interface CriteriaScores { + rechtschreibung?: number + grammatik?: number + inhalt?: number + struktur?: number + stil?: number + [key: string]: number | undefined +} + +export interface Criterion { + id: string + name: string + weight: number + description?: string +} + +export interface GradeInfo { + thresholds: Record + labels: Record + criteria: Record +} + +// --------------------------------------------------------------------------- +// Annotations +// --------------------------------------------------------------------------- + +export interface Annotation { + id: string + student_work_id: string + page: number + position: AnnotationPosition + type: AnnotationType + text: string + severity: 'minor' | 'major' | 'critical' + suggestion?: string + created_by: string + created_at: string + role: 'first_examiner' | 'second_examiner' + linked_criterion?: string +} + +export interface AnnotationPosition { + x: number // Prozent (0-100) + y: number // Prozent (0-100) + width: number // Prozent (0-100) + height: number // Prozent (0-100) +} + +export type AnnotationType = + | 'rechtschreibung' + | 'grammatik' + | 'inhalt' + | 'struktur' + | 'stil' + | 'comment' + | 'highlight' + +// --------------------------------------------------------------------------- +// Fairness analysis +// --------------------------------------------------------------------------- + +export interface FairnessAnalysis { + klausur_id: string + student_count: number + average_grade: number + std_deviation: number + spread: number + outliers: OutlierInfo[] + criteria_analysis: Record + fairness_score: number + warnings: string[] +} + +export interface OutlierInfo { + student_id: string + anonym_id: string + grade_points: number + deviation: number + reason: string +} + +export interface CriteriaStats { + min: number + max: number + average: number + std_deviation: number +} + +// --------------------------------------------------------------------------- +// EH suggestions +// --------------------------------------------------------------------------- + +export interface EHSuggestion { + criterion: string + excerpt: string + relevance_score: number + source_chunk_id: string + // Attribution fields (CTRL-SRC-002) + source_document?: string + source_url?: string + license?: string + license_url?: string + publisher?: string +} + +/** Default Attribution for NiBiS documents (CTRL-SRC-002) */ +export const NIBIS_ATTRIBUTION = { + publisher: 'Niedersaechsischer Bildungsserver (NiBiS)', + license: 'DL-DE-BY-2.0', + license_url: 'https://www.govdata.de/dl-de/by-2-0', + source_url: 'https://nibis.de', +} as const + +// --------------------------------------------------------------------------- +// Gutachten +// --------------------------------------------------------------------------- + +export interface GutachtenSection { + title: string + content: string + evidence_links?: string[] +} + +export interface Gutachten { + einleitung: string + hauptteil: string + fazit: string + staerken: string[] + schwaechen: string[] + generated_at?: string +} + +// --------------------------------------------------------------------------- +// API response types +// --------------------------------------------------------------------------- + +export interface KlausurenResponse { + klausuren: Klausur[] + total: number +} + +export interface StudentsResponse { + students: StudentWork[] + total: number +} + +export interface AnnotationsResponse { + annotations: Annotation[] +} + +// --------------------------------------------------------------------------- +// Create / update types +// --------------------------------------------------------------------------- + +export interface CreateKlausurData { + title: string + subject?: string + year?: number + semester?: string + modus?: KlausurModus +} + +// --------------------------------------------------------------------------- +// Constants — annotation colors +// --------------------------------------------------------------------------- + +export const ANNOTATION_COLORS: Record = { + rechtschreibung: '#dc2626', // Red + grammatik: '#2563eb', // Blue + inhalt: '#16a34a', // Green + struktur: '#9333ea', // Purple + stil: '#ea580c', // Orange + comment: '#6b7280', // Gray + highlight: '#eab308', // Yellow +} + +// --------------------------------------------------------------------------- +// Constants — status colors & labels +// --------------------------------------------------------------------------- + +export const STATUS_COLORS: Record = { + UPLOADED: '#6b7280', + OCR_PROCESSING: '#eab308', + OCR_COMPLETE: '#3b82f6', + ANALYZING: '#8b5cf6', + FIRST_EXAMINER: '#f97316', + SECOND_EXAMINER: '#06b6d4', + COMPLETED: '#22c55e', + ERROR: '#ef4444', +} + +export const STATUS_LABELS: Record = { + UPLOADED: 'Hochgeladen', + OCR_PROCESSING: 'OCR laeuft', + OCR_COMPLETE: 'OCR fertig', + ANALYZING: 'Analyse laeuft', + FIRST_EXAMINER: 'Erstkorrektur', + SECOND_EXAMINER: 'Zweitkorrektur', + COMPLETED: 'Abgeschlossen', + ERROR: 'Fehler', +} + +// --------------------------------------------------------------------------- +// Constants — criteria & grades +// --------------------------------------------------------------------------- + +/** Default criteria with weights (Niedersachsen standard) */ +export const DEFAULT_CRITERIA: Record = { + rechtschreibung: { name: 'Rechtschreibung', weight: 15 }, + grammatik: { name: 'Grammatik', weight: 15 }, + inhalt: { name: 'Inhalt', weight: 40 }, + struktur: { name: 'Struktur', weight: 15 }, + stil: { name: 'Stil', weight: 15 }, +} + +/** Grade thresholds (15-point system) */ +export const GRADE_THRESHOLDS: Record = { + 15: 95, 14: 90, 13: 85, 12: 80, 11: 75, + 10: 70, 9: 65, 8: 60, 7: 55, 6: 50, + 5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0, +} + +// --------------------------------------------------------------------------- +// Helper functions +// --------------------------------------------------------------------------- + +/** Calculate grade points from a percentage (0-100). */ +export function calculateGrade(percentage: number): number { + for (const [grade, threshold] of Object.entries(GRADE_THRESHOLDS).sort( + (a, b) => Number(b[0]) - Number(a[0]), + )) { + if (percentage >= threshold) { + return Number(grade) + } + } + return 0 +} + +/** Human-readable label for a 15-point grade value. */ +export function getGradeLabel(points: number): string { + const labels: Record = { + 15: '1+', 14: '1', 13: '1-', + 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', + 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', + 0: '6', + } + return labels[points] || String(points) +} + +// --------------------------------------------------------------------------- +// Examiner workflow types (workspace) +// --------------------------------------------------------------------------- + +export interface ExaminerInfo { + id: string + assigned_at: string + notes?: string +} + +export interface ExaminerResult { + grade_points: number + criteria_scores?: CriteriaScores + notes?: string + submitted_at: string +} + +export interface ExaminerWorkflow { + student_id: string + workflow_status: string + visibility_mode: string + user_role: 'ek' | 'zk' | 'dk' | 'viewer' + first_examiner?: ExaminerInfo + second_examiner?: ExaminerInfo + third_examiner?: ExaminerInfo + first_result?: ExaminerResult + first_result_visible?: boolean + second_result?: ExaminerResult + third_result?: ExaminerResult + grade_difference?: number + final_grade?: number + consensus_reached?: boolean + consensus_type?: string + einigung?: { + final_grade: number + notes: string + type: string + submitted_by: string + submitted_at: string + ek_grade: number + zk_grade: number + } + drittkorrektur_reason?: string +} + +export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege' + +export interface GradeTotals { + raw: number + weighted: number + gradePoints: number +} + +// --------------------------------------------------------------------------- +// Constants — workflow status & roles +// --------------------------------------------------------------------------- + +export const GRADE_LABELS: Record = { + 15: '1+', 14: '1', 13: '1-', 12: '2+', 11: '2', 10: '2-', + 9: '3+', 8: '3', 7: '3-', 6: '4+', 5: '4', 4: '4-', + 3: '5+', 2: '5', 1: '5-', 0: '6', +} + +export const WORKFLOW_STATUS_LABELS: Record = { + not_started: { label: 'Nicht gestartet', color: 'bg-slate-100 text-slate-700' }, + ek_in_progress: { label: 'EK in Arbeit', color: 'bg-blue-100 text-blue-700' }, + ek_completed: { label: 'EK abgeschlossen', color: 'bg-blue-200 text-blue-800' }, + zk_assigned: { label: 'ZK zugewiesen', color: 'bg-amber-100 text-amber-700' }, + zk_in_progress: { label: 'ZK in Arbeit', color: 'bg-amber-200 text-amber-800' }, + zk_completed: { label: 'ZK abgeschlossen', color: 'bg-amber-300 text-amber-900' }, + einigung_required: { label: 'Einigung erforderlich', color: 'bg-orange-100 text-orange-700' }, + einigung_completed: { label: 'Einigung abgeschlossen', color: 'bg-green-100 text-green-700' }, + drittkorrektur_required: { label: 'DK erforderlich', color: 'bg-red-100 text-red-700' }, + drittkorrektur_assigned: { label: 'DK zugewiesen', color: 'bg-red-200 text-red-800' }, + drittkorrektur_in_progress: { label: 'DK in Arbeit', color: 'bg-red-300 text-red-900' }, + completed: { label: 'Abgeschlossen', color: 'bg-green-200 text-green-800' }, +} + +export const ROLE_LABELS: Record = { + ek: { label: 'Erstkorrektor', color: 'bg-blue-500' }, + zk: { label: 'Zweitkorrektor', color: 'bg-amber-500' }, + dk: { label: 'Drittkorrektor', color: 'bg-purple-500' }, + viewer: { label: 'Betrachter', color: 'bg-slate-500' }, +} + +// --------------------------------------------------------------------------- +// Form types (create / upload) +// --------------------------------------------------------------------------- + +export interface CreateKlausurForm { + title: string + subject: string + year: number + semester: string + modus: 'abitur' | 'vorabitur' +} + +export interface VorabiturEHForm { + aufgabentyp: string + titel: string + text_titel: string + text_autor: string + aufgabenstellung: string +} + +export interface EHTemplate { + aufgabentyp: string + name: string + description: string + category: string +} + +export interface DirektuploadForm { + files: File[] + ehFile: File | null + ehText: string + aufgabentyp: string + klausurTitle: string +} + +export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken' diff --git a/website/components/klausur-korrektur/list-types.ts b/website/components/klausur-korrektur/list-types.ts index 3e2dbeb..4490884 100644 --- a/website/components/klausur-korrektur/list-types.ts +++ b/website/components/klausur-korrektur/list-types.ts @@ -12,6 +12,6 @@ export type { VorabiturEHForm, EHTemplate, DirektuploadForm, -} from '@shared/types/klausur' +} from './klausur-types' export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' diff --git a/website/components/klausur-korrektur/workspace-types.ts b/website/components/klausur-korrektur/workspace-types.ts index 089bb37..4376b82 100644 --- a/website/components/klausur-korrektur/workspace-types.ts +++ b/website/components/klausur-korrektur/workspace-types.ts @@ -12,12 +12,12 @@ export type { ExaminerWorkflow, ActiveTab, CriteriaScores, -} from '@shared/types/klausur' +} from './klausur-types' export { WORKFLOW_STATUS_LABELS, ROLE_LABELS, GRADE_LABELS, -} from '@shared/types/klausur' +} from './klausur-types' export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' diff --git a/website/tsconfig.json b/website/tsconfig.json index fcdc330..d81d4ee 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -24,9 +24,6 @@ "paths": { "@/*": [ "./*" - ], - "@shared/*": [ - "../shared/*" ] }, "target": "ES2017"