[interface-change] Phase 4: Extract shared types + fix Docker context

Shared types extracted to shared/types/:
- companion.ts (33+ types, was 100% duplicated admin-lehrer ↔ studio-v2)
- klausur.ts (18+ types, was 95% duplicated across 4 locations)
- ocr-labeling.ts (11 types, was 100% duplicated admin-lehrer ↔ website)

Original type files replaced with re-exports for backward compat.
tsconfig.json paths updated with @shared/* alias in all 3 services.

Docker: Changed build context from ./service to . (root) so shared/
is accessible. Dockerfiles updated to COPY service/ + shared/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 15:52:19 +02:00
parent dc60233262
commit a317bd6164
23 changed files with 976 additions and 1983 deletions

View File

@@ -4,13 +4,14 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package.json package-lock.json* ./ COPY admin-lehrer/package.json admin-lehrer/package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm install RUN npm install
# Copy source code # Copy source code + shared types
COPY . . COPY admin-lehrer/ .
COPY shared/ /shared/
# Build arguments for environment variables # Build arguments for environment variables
ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_API_URL

View File

@@ -1,123 +1 @@
/** export * from '../../../../../shared/types/ocr-labeling'
* TypeScript types for OCR Labeling UI
*/
/**
* 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<OCRModel, { label: string; description: string; speed: string }> = {
'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<string, unknown>
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
}

View File

@@ -1,93 +1,24 @@
/** /**
* Types and constants for the Korrektur-Workspace page. * Types and constants for the Korrektur-Workspace page.
*
* Domain types are re-exported from the shared module.
* Only the API_BASE constant remains local (uses Next.js rewrite proxy).
*/ */
import type { CriteriaScores } from '../../../types' export type {
ExaminerInfo,
ExaminerResult,
ExaminerWorkflow,
ActiveTab,
GradeTotals,
CriteriaScores,
} from '../../../../../../../../shared/types/klausur'
// ---- Examiner workflow types ---- export {
WORKFLOW_STATUS_LABELS,
export interface ExaminerInfo { ROLE_LABELS,
id: string GRADE_LABELS,
assigned_at: string } from '../../../../../../../../shared/types/klausur'
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
}
// ---- Active tab ----
export type ActiveTab = 'kriterien' | 'gutachten' | 'annotationen' | 'eh-vorschlaege'
// ---- Totals from grade calculation ----
export interface GradeTotals {
raw: number
weighted: number
gradePoints: number
}
// ---- Constants ----
/** Same-origin proxy to avoid CORS issues */ /** Same-origin proxy to avoid CORS issues */
export const API_BASE = '/klausur-api' export const API_BASE = '/klausur-api'
export const GRADE_LABELS: Record<number, string> = {
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<string, { label: string; color: string }> = {
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<string, { label: string; color: string }> = {
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' },
}

View File

@@ -1,34 +1,11 @@
/** /**
* Local form types for Klausur-Korrektur page * Local form types for Klausur-Korrektur page.
* Re-exported from the shared module.
*/ */
export interface CreateKlausurForm { export type {
title: string CreateKlausurForm,
subject: string VorabiturEHForm,
year: number EHTemplate,
semester: string DirektuploadForm,
modus: 'abitur' | 'vorabitur' } from '../../../../../../shared/types/klausur'
}
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
}

View File

@@ -1,195 +1 @@
// TypeScript Interfaces für Klausur-Korrektur export * from '../../../../../shared/types/klausur'
export interface Klausur {
id: string
title: string
subject: string
year: number
semester: string
modus: 'abitur' | 'vorabitur'
eh_id?: string
created_at: string
student_count?: number
completed_count?: number
status?: 'draft' | 'in_progress' | 'completed'
}
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<number, number>
labels: Record<number, string>
criteria: Record<string, Criterion>
}
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'
export interface FairnessAnalysis {
klausur_id: string
student_count: number
average_grade: number
std_deviation: number
spread: number
outliers: OutlierInfo[]
criteria_analysis: Record<string, CriteriaStats>
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
}
export interface EHSuggestion {
criterion: string
excerpt: string
relevance_score: number
source_chunk_id: string
}
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[]
}
// Color mapping for annotation types
export const ANNOTATION_COLORS: Record<AnnotationType, string> = {
rechtschreibung: '#dc2626', // Red
grammatik: '#2563eb', // Blue
inhalt: '#16a34a', // Green
struktur: '#9333ea', // Purple
stil: '#ea580c', // Orange
comment: '#6b7280', // Gray
highlight: '#eab308', // Yellow
}
// Status colors
export const STATUS_COLORS: Record<StudentStatus, string> = {
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<StudentStatus, string> = {
UPLOADED: 'Hochgeladen',
OCR_PROCESSING: 'OCR laeuft',
OCR_COMPLETE: 'OCR fertig',
ANALYZING: 'Analyse laeuft',
FIRST_EXAMINER: 'Erstkorrektur',
SECOND_EXAMINER: 'Zweitkorrektur',
COMPLETED: 'Abgeschlossen',
ERROR: 'Fehler',
}

View File

@@ -1,329 +1 @@
/** 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<string, unknown>
}
// ============================================================================
// 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<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// 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
}

View File

@@ -24,6 +24,9 @@
"paths": { "paths": {
"@/*": [ "@/*": [
"./*" "./*"
],
"@shared/*": [
"../shared/*"
] ]
}, },
"target": "ES2017" "target": "ES2017"

View File

@@ -60,8 +60,8 @@ services:
# ========================================================= # =========================================================
admin-lehrer: admin-lehrer:
build: build:
context: ./admin-lehrer context: .
dockerfile: Dockerfile dockerfile: admin-lehrer/Dockerfile
args: args:
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://macmini:8001} 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} NEXT_PUBLIC_OLD_ADMIN_URL: ${NEXT_PUBLIC_OLD_ADMIN_URL:-http://macmini:3000/admin}
@@ -95,8 +95,8 @@ services:
studio-v2: studio-v2:
build: build:
context: ./studio-v2 context: .
dockerfile: Dockerfile dockerfile: studio-v2/Dockerfile
args: args:
NEXT_PUBLIC_VOICE_SERVICE_URL: ${NEXT_PUBLIC_VOICE_SERVICE_URL:-wss://macmini:8091} 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} NEXT_PUBLIC_KLAUSUR_SERVICE_URL: ${NEXT_PUBLIC_KLAUSUR_SERVICE_URL:-https://macmini:8086}
@@ -116,8 +116,8 @@ services:
website: website:
build: build:
context: ./website context: .
dockerfile: Dockerfile dockerfile: website/Dockerfile
args: args:
NEXT_PUBLIC_BILLING_API_URL: ${NEXT_PUBLIC_BILLING_API_URL:-https://macmini:8083} NEXT_PUBLIC_BILLING_API_URL: ${NEXT_PUBLIC_BILLING_API_URL:-https://macmini:8083}
NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-https://macmini} NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-https://macmini}

329
shared/types/companion.ts Normal file
View File

@@ -0,0 +1,329 @@
/**
* 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<string, unknown>
}
// ============================================================================
// 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<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// 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
}

3
shared/types/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './companion'
export * from './klausur'
export * from './ocr-labeling'

432
shared/types/klausur.ts Normal file
View File

@@ -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<number, number>
labels: Record<number, string>
criteria: Record<string, Criterion>
}
// ---------------------------------------------------------------------------
// 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<string, CriteriaStats>
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<AnnotationType, string> = {
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<StudentStatus, string> = {
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<StudentStatus, string> = {
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<string, { name: string; weight: number }> = {
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<number, number> = {
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<number, string> = {
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<number, string> = {
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<string, { label: string; color: string }> = {
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<string, { label: string; color: string }> = {
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'

View File

@@ -0,0 +1,127 @@
/**
* 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<OCRModel, { label: string; description: string; speed: string }> = {
'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<string, unknown>
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
}

View File

@@ -4,13 +4,14 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package.json package-lock.json* ./ COPY studio-v2/package.json studio-v2/package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm install RUN npm install
# Copy source files # Copy source files + shared types
COPY . . COPY studio-v2/ .
COPY shared/ /shared/
# Build arguments for environment variables (needed at build time for Next.js) # Build arguments for environment variables (needed at build time for Next.js)
ARG NEXT_PUBLIC_VOICE_SERVICE_URL ARG NEXT_PUBLIC_VOICE_SERVICE_URL

View File

@@ -1,257 +1 @@
// TypeScript Interfaces fuer Korrekturplattform (Studio v2) export * from '../../../shared/types/klausur'
export interface Klausur {
id: string
title: string
subject: string
year: number
semester: string
modus: 'landes_abitur' | 'vorabitur'
eh_id?: string
created_at: string
student_count?: number
completed_count?: number
status?: 'draft' | 'in_progress' | 'completed'
}
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<number, number>
labels: Record<number, string>
criteria: Record<string, Criterion>
}
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'
export interface FairnessAnalysis {
klausur_id: string
student_count: number
average_grade: number
std_deviation: number
spread: number
outliers: OutlierInfo[]
criteria_analysis: Record<string, CriteriaStats>
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
}
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',
}
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?: 'landes_abitur' | 'vorabitur'
}
// Color mapping for annotation types
export const ANNOTATION_COLORS: Record<AnnotationType, string> = {
rechtschreibung: '#dc2626', // Red
grammatik: '#2563eb', // Blue
inhalt: '#16a34a', // Green
struktur: '#9333ea', // Purple
stil: '#ea580c', // Orange
comment: '#6b7280', // Gray
highlight: '#eab308', // Yellow
}
// Status colors
export const STATUS_COLORS: Record<StudentStatus, string> = {
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<StudentStatus, string> = {
UPLOADED: 'Hochgeladen',
OCR_PROCESSING: 'OCR laeuft',
OCR_COMPLETE: 'OCR fertig',
ANALYZING: 'Analyse laeuft',
FIRST_EXAMINER: 'Erstkorrektur',
SECOND_EXAMINER: 'Zweitkorrektur',
COMPLETED: 'Abgeschlossen',
ERROR: 'Fehler',
}
// Default criteria with weights (NI standard)
export const DEFAULT_CRITERIA: Record<string, { name: string; weight: number }> = {
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<number, number> = {
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 function to calculate grade from percentage
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
}
// Helper function to get grade label
export function getGradeLabel(points: number): string {
const labels: Record<number, string> = {
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)
}

View File

@@ -1,329 +1 @@
/** 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<string, unknown>
}
// ============================================================================
// 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<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// 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
}

View File

@@ -24,6 +24,9 @@
"paths": { "paths": {
"@/*": [ "@/*": [
"./*" "./*"
],
"@shared/*": [
"../shared/*"
] ]
}, },
"target": "ES2017" "target": "ES2017"

View File

@@ -4,13 +4,14 @@ FROM node:20-alpine AS builder
WORKDIR /app WORKDIR /app
# Copy package files # Copy package files
COPY package.json package-lock.json* ./ COPY website/package.json website/package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm install RUN npm install
# Copy source code # Copy source code + shared types
COPY . . COPY website/ .
COPY shared/ /shared/
# Build arguments for environment variables # Build arguments for environment variables
ARG NEXT_PUBLIC_BILLING_API_URL ARG NEXT_PUBLIC_BILLING_API_URL

View File

@@ -1,195 +1 @@
// TypeScript Interfaces für Klausur-Korrektur export * from '../../../../shared/types/klausur'
export interface Klausur {
id: string
title: string
subject: string
year: number
semester: string
modus: 'abitur' | 'vorabitur'
eh_id?: string
created_at: string
student_count?: number
completed_count?: number
status?: 'draft' | 'in_progress' | 'completed'
}
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<number, number>
labels: Record<number, string>
criteria: Record<string, Criterion>
}
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'
export interface FairnessAnalysis {
klausur_id: string
student_count: number
average_grade: number
std_deviation: number
spread: number
outliers: OutlierInfo[]
criteria_analysis: Record<string, CriteriaStats>
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
}
export interface EHSuggestion {
criterion: string
excerpt: string
relevance_score: number
source_chunk_id: string
}
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[]
}
// Color mapping for annotation types
export const ANNOTATION_COLORS: Record<AnnotationType, string> = {
rechtschreibung: '#dc2626', // Red
grammatik: '#2563eb', // Blue
inhalt: '#16a34a', // Green
struktur: '#9333ea', // Purple
stil: '#ea580c', // Orange
comment: '#6b7280', // Gray
highlight: '#eab308', // Yellow
}
// Status colors
export const STATUS_COLORS: Record<StudentStatus, string> = {
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<StudentStatus, string> = {
UPLOADED: 'Hochgeladen',
OCR_PROCESSING: 'OCR läuft',
OCR_COMPLETE: 'OCR fertig',
ANALYZING: 'Analyse läuft',
FIRST_EXAMINER: 'Erstkorrektur',
SECOND_EXAMINER: 'Zweitkorrektur',
COMPLETED: 'Abgeschlossen',
ERROR: 'Fehler',
}

View File

@@ -1,123 +1 @@
/** export * from '../../../../shared/types/ocr-labeling'
* TypeScript types for OCR Labeling UI
*/
/**
* 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<OCRModel, { label: string; description: string; speed: string }> = {
'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<string, unknown>
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
}

View File

@@ -1,195 +1 @@
// TypeScript Interfaces für Klausur-Korrektur export * from '../../../../shared/types/klausur'
export interface Klausur {
id: string
title: string
subject: string
year: number
semester: string
modus: 'abitur' | 'vorabitur'
eh_id?: string
created_at: string
student_count?: number
completed_count?: number
status?: 'draft' | 'in_progress' | 'completed'
}
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<number, number>
labels: Record<number, string>
criteria: Record<string, Criterion>
}
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'
export interface FairnessAnalysis {
klausur_id: string
student_count: number
average_grade: number
std_deviation: number
spread: number
outliers: OutlierInfo[]
criteria_analysis: Record<string, CriteriaStats>
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
}
export interface EHSuggestion {
criterion: string
excerpt: string
relevance_score: number
source_chunk_id: string
}
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[]
}
// Color mapping for annotation types
export const ANNOTATION_COLORS: Record<AnnotationType, string> = {
rechtschreibung: '#dc2626', // Red
grammatik: '#2563eb', // Blue
inhalt: '#16a34a', // Green
struktur: '#9333ea', // Purple
stil: '#ea580c', // Orange
comment: '#6b7280', // Gray
highlight: '#eab308', // Yellow
}
// Status colors
export const STATUS_COLORS: Record<StudentStatus, string> = {
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<StudentStatus, string> = {
UPLOADED: 'Hochgeladen',
OCR_PROCESSING: 'OCR läuft',
OCR_COMPLETE: 'OCR fertig',
ANALYZING: 'Analyse läuft',
FIRST_EXAMINER: 'Erstkorrektur',
SECOND_EXAMINER: 'Zweitkorrektur',
COMPLETED: 'Abgeschlossen',
ERROR: 'Fehler',
}

View File

@@ -1,39 +1,17 @@
/** /**
* Types and constants for the Klausur-Korrektur list page. * Types and constants for the Klausur-Korrektur list page.
* Shared between admin and lehrer routes. * Shared between admin and lehrer routes.
*
* Domain types are re-exported from the shared module.
* Only the API_BASE constant remains local (env-dependent).
*/ */
export type {
TabId,
CreateKlausurForm,
VorabiturEHForm,
EHTemplate,
DirektuploadForm,
} from '../../../shared/types/klausur'
export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken'
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
}

View File

@@ -1,81 +1,23 @@
/** /**
* Types and constants for the Korrektur-Workspace. * Types and constants for the Korrektur-Workspace.
* Shared between admin and lehrer routes. * Shared between admin and lehrer routes.
*
* Domain types are re-exported from the shared module.
* Only the API_BASE constant remains local (env-dependent).
*/ */
import type { CriteriaScores } from '../../app/admin/klausur-korrektur/types' export type {
ExaminerInfo,
ExaminerResult,
ExaminerWorkflow,
ActiveTab,
CriteriaScores,
} from '../../../shared/types/klausur'
// Examiner workflow types export {
export interface ExaminerInfo { WORKFLOW_STATUS_LABELS,
id: string ROLE_LABELS,
assigned_at: string GRADE_LABELS,
notes?: string } from '../../../shared/types/klausur'
}
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'
// Workflow status labels
export const WORKFLOW_STATUS_LABELS: Record<string, { label: string; color: string }> = {
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<string, { label: string; color: string }> = {
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' },
}
export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' export const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
export const GRADE_LABELS: Record<number, string> = {
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',
}

View File

@@ -24,6 +24,9 @@
"paths": { "paths": {
"@/*": [ "@/*": [
"./*" "./*"
],
"@shared/*": [
"../shared/*"
] ]
}, },
"target": "ES2017" "target": "ES2017"