Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

507 lines
14 KiB
TypeScript

/**
* 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<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
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<Klausur[]> {
const data = await apiFetch<{ klausuren: Klausur[] }>('/api/v1/klausuren')
return data.klausuren || []
}
export async function getKlausur(id: string): Promise<Klausur> {
return apiFetch<Klausur>(`/api/v1/klausuren/${id}`)
}
export async function createKlausur(data: CreateKlausurData): Promise<Klausur> {
return apiFetch<Klausur>('/api/v1/klausuren', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function deleteKlausur(id: string): Promise<void> {
await apiFetch(`/api/v1/klausuren/${id}`, { method: 'DELETE' })
}
// ============================================================================
// STUDENTS API
// ============================================================================
export async function getStudents(klausurId: string): Promise<StudentWork[]> {
const data = await apiFetch<{ students: StudentWork[] }>(
`/api/v1/klausuren/${klausurId}/students`
)
return data.students || []
}
export async function getStudent(studentId: string): Promise<StudentWork> {
return apiFetch<StudentWork>(`/api/v1/students/${studentId}`)
}
export async function uploadStudentWork(
klausurId: string,
file: File,
anonymId: string
): Promise<StudentWork> {
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<void> {
await apiFetch(`/api/v1/students/${studentId}`, { method: 'DELETE' })
}
// ============================================================================
// CRITERIA & GUTACHTEN API
// ============================================================================
export async function updateCriteria(
studentId: string,
criteria: CriteriaScores
): Promise<StudentWork> {
return apiFetch<StudentWork>(`/api/v1/students/${studentId}/criteria`, {
method: 'PUT',
body: JSON.stringify({ criteria_scores: criteria }),
})
}
export async function updateGutachten(
studentId: string,
gutachten: string
): Promise<StudentWork> {
return apiFetch<StudentWork>(`/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<Annotation[]> {
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<Annotation> {
return apiFetch<Annotation>(`/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<Annotation> {
return apiFetch<Annotation>(`/api/v1/annotations/${annotationId}`, {
method: 'PUT',
body: JSON.stringify(updates),
})
}
export async function deleteAnnotation(annotationId: string): Promise<void> {
await apiFetch(`/api/v1/annotations/${annotationId}`, { method: 'DELETE' })
}
// ============================================================================
// EH/RAG API (500+ NiBiS Dokumente)
// ============================================================================
export async function getEHSuggestions(
studentId: string,
criterion?: string
): Promise<EHSuggestion[]> {
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<FairnessAnalysis> {
return apiFetch<FairnessAnalysis>(`/api/v1/klausuren/${klausurId}/fairness`)
}
export async function getGradeInfo(): Promise<GradeInfo> {
return apiFetch<GradeInfo>('/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<ArchivSearchResponse> {
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<ArchivSearchResponse>(endpoint)
}
export async function getArchivDocument(docId: string): Promise<ArchivDokument & { text_preview?: string }> {
return apiFetch<ArchivDokument & { text_preview?: string }>(`/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<Array<{
id: string
score: number
text: string
year: number
subject: string
niveau: string
task_number?: number
doc_type: string
}>> {
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<Array<{ label: string; type: string }>> {
return apiFetch<Array<{ label: string; type: string }>>(`/api/v1/archiv/suggest?query=${encodeURIComponent(query)}`)
}
export async function getArchivStats(): Promise<{
total_documents: number
total_chunks: number
by_year: Record<string, number>
by_subject: Record<string, number>
by_niveau: Record<string, number>
}> {
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<KorrekturStats> {
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,
}