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>
This commit is contained in:
506
studio-v2/lib/korrektur/api.ts
Normal file
506
studio-v2/lib/korrektur/api.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* 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,
|
||||
}
|
||||
1
studio-v2/lib/korrektur/index.ts
Normal file
1
studio-v2/lib/korrektur/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './api'
|
||||
Reference in New Issue
Block a user