/** * Korrekturplattform API Service Layer * * Connects to klausur-service (Port 8086) for all correction-related operations. * Uses dynamic host detection for local network compatibility. */ import type { Klausur, StudentWork, CriteriaScores, Annotation, AnnotationPosition, AnnotationType, FairnessAnalysis, EHSuggestion, GradeInfo, CreateKlausurData, } from '@/app/korrektur/types' // Get API base URL dynamically // On localhost: direct connection to port 8086 // On macmini: use nginx proxy /klausur-api/ to avoid certificate issues const getApiBase = (): string => { if (typeof window === 'undefined') return 'http://localhost:8086' const { hostname } = window.location // Use nginx proxy on macmini to avoid cross-origin certificate issues return hostname === 'localhost' ? 'http://localhost:8086' : '/klausur-api' } // Generic fetch wrapper with error handling async function apiFetch( endpoint: string, options: RequestInit = {} ): Promise { const url = `${getApiBase()}${endpoint}` const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }) if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })) throw new Error(errorData.detail || `HTTP ${response.status}`) } return response.json() } // ============================================================================ // KLAUSUREN API // ============================================================================ export async function getKlausuren(): Promise { const data = await apiFetch<{ klausuren: Klausur[] }>('/api/v1/klausuren') return data.klausuren || [] } export async function getKlausur(id: string): Promise { return apiFetch(`/api/v1/klausuren/${id}`) } export async function createKlausur(data: CreateKlausurData): Promise { return apiFetch('/api/v1/klausuren', { method: 'POST', body: JSON.stringify(data), }) } export async function deleteKlausur(id: string): Promise { await apiFetch(`/api/v1/klausuren/${id}`, { method: 'DELETE' }) } // ============================================================================ // STUDENTS API // ============================================================================ export async function getStudents(klausurId: string): Promise { const data = await apiFetch<{ students: StudentWork[] }>( `/api/v1/klausuren/${klausurId}/students` ) return data.students || [] } export async function getStudent(studentId: string): Promise { return apiFetch(`/api/v1/students/${studentId}`) } export async function uploadStudentWork( klausurId: string, file: File, anonymId: string ): Promise { const formData = new FormData() formData.append('file', file) formData.append('anonym_id', anonymId) const response = await fetch( `${getApiBase()}/api/v1/klausuren/${klausurId}/students`, { method: 'POST', body: formData, } ) if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'Upload failed' })) throw new Error(errorData.detail || `HTTP ${response.status}`) } return response.json() } export async function deleteStudent(studentId: string): Promise { await apiFetch(`/api/v1/students/${studentId}`, { method: 'DELETE' }) } // ============================================================================ // CRITERIA & GUTACHTEN API // ============================================================================ export async function updateCriteria( studentId: string, criteria: CriteriaScores ): Promise { return apiFetch(`/api/v1/students/${studentId}/criteria`, { method: 'PUT', body: JSON.stringify({ criteria_scores: criteria }), }) } export async function updateGutachten( studentId: string, gutachten: string ): Promise { return apiFetch(`/api/v1/students/${studentId}/gutachten`, { method: 'PUT', body: JSON.stringify({ gutachten }), }) } export async function generateGutachten( studentId: string ): Promise<{ gutachten: string }> { return apiFetch<{ gutachten: string }>( `/api/v1/students/${studentId}/gutachten/generate`, { method: 'POST' } ) } // ============================================================================ // ANNOTATIONS API // ============================================================================ export async function getAnnotations(studentId: string): Promise { const data = await apiFetch<{ annotations: Annotation[] }>( `/api/v1/students/${studentId}/annotations` ) return data.annotations || [] } export async function createAnnotation( studentId: string, annotation: { page: number position: AnnotationPosition type: AnnotationType text: string severity?: 'minor' | 'major' | 'critical' suggestion?: string linked_criterion?: string } ): Promise { return apiFetch(`/api/v1/students/${studentId}/annotations`, { method: 'POST', body: JSON.stringify(annotation), }) } export async function updateAnnotation( annotationId: string, updates: Partial<{ text: string severity: 'minor' | 'major' | 'critical' suggestion: string }> ): Promise { return apiFetch(`/api/v1/annotations/${annotationId}`, { method: 'PUT', body: JSON.stringify(updates), }) } export async function deleteAnnotation(annotationId: string): Promise { await apiFetch(`/api/v1/annotations/${annotationId}`, { method: 'DELETE' }) } // ============================================================================ // EH/RAG API (500+ NiBiS Dokumente) // ============================================================================ export async function getEHSuggestions( studentId: string, criterion?: string ): Promise { const data = await apiFetch<{ suggestions: EHSuggestion[] }>( `/api/v1/students/${studentId}/eh-suggestions`, { method: 'POST', body: JSON.stringify({ criterion }), } ) return data.suggestions || [] } export async function queryRAG( query: string, topK: number = 5 ): Promise<{ results: Array<{ text: string; score: number; metadata: any }> }> { return apiFetch('/api/v1/eh/rag-query', { method: 'POST', body: JSON.stringify({ query, top_k: topK }), }) } export async function uploadEH(file: File): Promise<{ id: string; name: string }> { const formData = new FormData() formData.append('file', file) const response = await fetch( `${getApiBase()}/api/v1/eh/upload`, { method: 'POST', body: formData, } ) if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'EH Upload failed' })) throw new Error(errorData.detail || `HTTP ${response.status}`) } return response.json() } // ============================================================================ // FAIRNESS & EXPORT API // ============================================================================ export async function getFairnessAnalysis( klausurId: string ): Promise { return apiFetch(`/api/v1/klausuren/${klausurId}/fairness`) } export async function getGradeInfo(): Promise { return apiFetch('/api/v1/grade-info') } // Export endpoints return file downloads export function getGutachtenExportUrl(studentId: string): string { return `${getApiBase()}/api/v1/students/${studentId}/export/gutachten` } export function getAnnotationsExportUrl(studentId: string): string { return `${getApiBase()}/api/v1/students/${studentId}/export/annotations` } export function getOverviewExportUrl(klausurId: string): string { return `${getApiBase()}/api/v1/klausuren/${klausurId}/export/overview` } export function getAllGutachtenExportUrl(klausurId: string): string { return `${getApiBase()}/api/v1/klausuren/${klausurId}/export/all-gutachten` } // Get student file URL (PDF/Image) export function getStudentFileUrl(studentId: string): string { return `${getApiBase()}/api/v1/students/${studentId}/file` } // ============================================================================ // ARCHIV API (NiBiS Zentralabitur Documents) // ============================================================================ export interface ArchivDokument { id: string title: string subject: string niveau: string year: number task_number?: number doc_type: string variant?: string bundesland: string minio_path?: string preview_url?: string } export interface ArchivSearchResponse { total: number documents: ArchivDokument[] filters: { subjects: string[] years: number[] niveaus: string[] doc_types: string[] } } export interface ArchivFilters { subject?: string year?: number bundesland?: string niveau?: string doc_type?: string search?: string limit?: number offset?: number } export async function getArchivDocuments(filters: ArchivFilters = {}): Promise { const params = new URLSearchParams() if (filters.subject && filters.subject !== 'Alle') params.append('subject', filters.subject) if (filters.year) params.append('year', filters.year.toString()) if (filters.bundesland && filters.bundesland !== 'Alle') params.append('bundesland', filters.bundesland) if (filters.niveau && filters.niveau !== 'Alle') params.append('niveau', filters.niveau) if (filters.doc_type && filters.doc_type !== 'Alle') params.append('doc_type', filters.doc_type) if (filters.search) params.append('search', filters.search) if (filters.limit) params.append('limit', filters.limit.toString()) if (filters.offset) params.append('offset', filters.offset.toString()) const queryString = params.toString() const endpoint = `/api/v1/archiv${queryString ? `?${queryString}` : ''}` return apiFetch(endpoint) } export async function getArchivDocument(docId: string): Promise { return apiFetch(`/api/v1/archiv/${docId}`) } export async function getArchivDocumentUrl(docId: string, expires: number = 3600): Promise<{ url: string; expires_in: number; filename: string }> { return apiFetch<{ url: string; expires_in: number; filename: string }>(`/api/v1/archiv/${docId}/url?expires=${expires}`) } export async function searchArchivSemantic( query: string, options: { year?: number; subject?: string; niveau?: string; limit?: number } = {} ): Promise> { const params = new URLSearchParams({ query }) if (options.year) params.append('year', options.year.toString()) if (options.subject) params.append('subject', options.subject) if (options.niveau) params.append('niveau', options.niveau) if (options.limit) params.append('limit', options.limit.toString()) return apiFetch(`/api/v1/archiv/search/semantic?${params.toString()}`) } export async function getArchivSuggestions(query: string): Promise> { return apiFetch>(`/api/v1/archiv/suggest?query=${encodeURIComponent(query)}`) } export async function getArchivStats(): Promise<{ total_documents: number total_chunks: number by_year: Record by_subject: Record by_niveau: Record }> { return apiFetch('/api/v1/archiv/stats') } // ============================================================================ // STATS API (for Dashboard) // ============================================================================ export interface KorrekturStats { totalKlausuren: number totalStudents: number openCorrections: number completedThisWeek: number averageGrade: number timeSavedHours: number } export async function getKorrekturStats(): Promise { try { const klausuren = await getKlausuren() let totalStudents = 0 let openCorrections = 0 let completedCount = 0 let gradeSum = 0 let gradedCount = 0 for (const klausur of klausuren) { totalStudents += klausur.student_count || 0 completedCount += klausur.completed_count || 0 openCorrections += (klausur.student_count || 0) - (klausur.completed_count || 0) } // Get average from all klausuren for (const klausur of klausuren) { if (klausur.status === 'completed' || klausur.status === 'in_progress') { try { const fairness = await getFairnessAnalysis(klausur.id) if (fairness.average_grade > 0) { gradeSum += fairness.average_grade gradedCount++ } } catch { // Skip if fairness analysis not available } } } const averageGrade = gradedCount > 0 ? gradeSum / gradedCount : 0 // Estimate time saved: ~30 minutes per correction with AI assistance const timeSavedHours = Math.round(completedCount * 0.5) return { totalKlausuren: klausuren.length, totalStudents, openCorrections, completedThisWeek: completedCount, // Simplified, would need date filtering averageGrade: Math.round(averageGrade * 10) / 10, timeSavedHours, } } catch { return { totalKlausuren: 0, totalStudents: 0, openCorrections: 0, completedThisWeek: 0, averageGrade: 0, timeSavedHours: 0, } } } // Export all functions as a namespace export const korrekturApi = { // Klausuren getKlausuren, getKlausur, createKlausur, deleteKlausur, // Students getStudents, getStudent, uploadStudentWork, deleteStudent, // Criteria & Gutachten updateCriteria, updateGutachten, generateGutachten, // Annotations getAnnotations, createAnnotation, updateAnnotation, deleteAnnotation, // EH/RAG getEHSuggestions, queryRAG, uploadEH, // Fairness & Export getFairnessAnalysis, getGradeInfo, getGutachtenExportUrl, getAnnotationsExportUrl, getOverviewExportUrl, getAllGutachtenExportUrl, getStudentFileUrl, // Archiv (NiBiS) getArchivDocuments, getArchivDocument, getArchivDocumentUrl, searchArchivSemantic, getArchivSuggestions, getArchivStats, // Stats getKorrekturStats, }