From b49ee3467e43b4a283ba34fb8d8511a2acf37b56 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 25 Apr 2026 17:13:18 +0200 Subject: [PATCH] Fix: Revert to inline shared types (Turbopack can't resolve path aliases) Turbopack doesn't support tsconfig path aliases pointing outside the project root. Reverted to copying shared types directly into each service. The canonical source remains shared/types/*.ts, synced via scripts/sync-shared-types.sh. Changes: - Reverted docker-compose.yml contexts to ./service - Reverted Dockerfiles to simple COPY . . - Removed @shared/* from tsconfigs - Removed symlinks + .gitignore hacks - Added scripts/sync-shared-types.sh for keeping copies in sync Co-Authored-By: Claude Opus 4.6 (1M context) --- admin-lehrer/.gitignore | 2 - admin-lehrer/Dockerfile | 7 +- .../app/(admin)/ai/ocr-labeling/types.ts | 128 +++++- .../_components/workspace-types.ts | 4 +- .../klausur-korrektur/_components/types.ts | 2 +- .../education/klausur-korrektur/types.ts | 433 +++++++++++++++++- admin-lehrer/lib/companion/types.ts | 330 ++++++++++++- admin-lehrer/tsconfig.json | 3 - docker-compose.yml | 12 +- scripts/sync-shared-types.sh | 34 ++ studio-v2/.gitignore | 2 - studio-v2/Dockerfile | 7 +- studio-v2/app/korrektur/types.ts | 433 +++++++++++++++++- studio-v2/lib/companion/types.ts | 330 ++++++++++++- studio-v2/tsconfig.json | 3 - website/.gitignore | 2 - website/Dockerfile | 7 +- website/app/admin/klausur-korrektur/types.ts | 433 +++++++++++++++++- website/app/admin/ocr-labeling/types.ts | 128 +++++- website/app/lehrer/klausur-korrektur/types.ts | 433 +++++++++++++++++- .../klausur-korrektur/klausur-types.ts | 432 +++++++++++++++++ .../klausur-korrektur/list-types.ts | 2 +- .../klausur-korrektur/workspace-types.ts | 4 +- website/tsconfig.json | 3 - 24 files changed, 3127 insertions(+), 47 deletions(-) delete mode 100644 admin-lehrer/.gitignore create mode 100755 scripts/sync-shared-types.sh delete mode 100644 studio-v2/.gitignore delete mode 100644 website/.gitignore create mode 100644 website/components/klausur-korrektur/klausur-types.ts 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"