[split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
137
studio-v2/lib/korrektur/api-archiv.ts
Normal file
137
studio-v2/lib/korrektur/api-archiv.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Korrektur Archiv API - NiBiS Zentralabitur Documents and Stats
|
||||
*
|
||||
* Split from api.ts to stay under 500 LOC.
|
||||
*/
|
||||
|
||||
import { getApiBase, apiFetch, getFairnessAnalysis, getKlausuren } from './api-core'
|
||||
|
||||
// ============================================================================
|
||||
// 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()
|
||||
return apiFetch<ArchivSearchResponse>(`/api/v1/archiv${queryString ? `?${queryString}` : ''}`)
|
||||
}
|
||||
|
||||
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, openCorrections = 0, completedCount = 0, gradeSum = 0, 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)
|
||||
}
|
||||
|
||||
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 */ }
|
||||
}
|
||||
}
|
||||
|
||||
const averageGrade = gradedCount > 0 ? gradeSum / gradedCount : 0
|
||||
return {
|
||||
totalKlausuren: klausuren.length, totalStudents, openCorrections,
|
||||
completedThisWeek: completedCount,
|
||||
averageGrade: Math.round(averageGrade * 10) / 10,
|
||||
timeSavedHours: Math.round(completedCount * 0.5),
|
||||
}
|
||||
} catch {
|
||||
return { totalKlausuren: 0, totalStudents: 0, openCorrections: 0, completedThisWeek: 0, averageGrade: 0, timeSavedHours: 0 }
|
||||
}
|
||||
}
|
||||
139
studio-v2/lib/korrektur/api-core.ts
Normal file
139
studio-v2/lib/korrektur/api-core.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* Korrektur Core API - Base functions and CRUD operations
|
||||
*
|
||||
* Split from api.ts. This module contains the base fetch wrapper
|
||||
* and all core Klausur/Student/Annotation/Fairness operations.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Klausur, StudentWork, CriteriaScores, Annotation,
|
||||
AnnotationPosition, AnnotationType, FairnessAnalysis,
|
||||
EHSuggestion, GradeInfo, CreateKlausurData,
|
||||
} from '@/app/korrektur/types'
|
||||
|
||||
export const getApiBase = (): string => {
|
||||
if (typeof window === 'undefined') return 'http://localhost:8086'
|
||||
const { hostname } = window.location
|
||||
return hostname === 'localhost' ? 'http://localhost:8086' : '/klausur-api'
|
||||
}
|
||||
|
||||
export 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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 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` }
|
||||
export function getStudentFileUrl(studentId: string): string { return `${getApiBase()}/api/v1/students/${studentId}/file` }
|
||||
@@ -1,506 +1,76 @@
|
||||
/**
|
||||
* Korrekturplattform API Service Layer
|
||||
* Korrekturplattform API Service Layer - Barrel re-export
|
||||
*
|
||||
* Connects to klausur-service (Port 8086) for all correction-related operations.
|
||||
* Uses dynamic host detection for local network compatibility.
|
||||
* Split into:
|
||||
* - api-core.ts: Base functions, CRUD, annotations, fairness, export URLs
|
||||
* - api-archiv.ts: NiBiS archiv and dashboard stats
|
||||
* - api.ts (this file): Barrel re-export + korrekturApi namespace
|
||||
*/
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Re-export everything from core
|
||||
export {
|
||||
getApiBase, apiFetch,
|
||||
getKlausuren, getKlausur, createKlausur, deleteKlausur,
|
||||
getStudents, getStudent, uploadStudentWork, deleteStudent,
|
||||
updateCriteria, updateGutachten, generateGutachten,
|
||||
getAnnotations, createAnnotation, updateAnnotation, deleteAnnotation,
|
||||
getEHSuggestions, queryRAG, uploadEH,
|
||||
getFairnessAnalysis, getGradeInfo,
|
||||
getGutachtenExportUrl, getAnnotationsExportUrl, getOverviewExportUrl, getAllGutachtenExportUrl, getStudentFileUrl,
|
||||
} from './api-core'
|
||||
|
||||
// Re-export everything from archiv
|
||||
export type { ArchivDokument, ArchivSearchResponse, ArchivFilters, KorrekturStats } from './api-archiv'
|
||||
export {
|
||||
getArchivDocuments, getArchivDocument, getArchivDocumentUrl,
|
||||
searchArchivSemantic, getArchivSuggestions, getArchivStats,
|
||||
getKorrekturStats,
|
||||
} from './api-archiv'
|
||||
|
||||
// Import for namespace
|
||||
import * as core from './api-core'
|
||||
import * as archiv from './api-archiv'
|
||||
|
||||
// Export all functions as a namespace
|
||||
export const korrekturApi = {
|
||||
// Klausuren
|
||||
getKlausuren,
|
||||
getKlausur,
|
||||
createKlausur,
|
||||
deleteKlausur,
|
||||
|
||||
getKlausuren: core.getKlausuren,
|
||||
getKlausur: core.getKlausur,
|
||||
createKlausur: core.createKlausur,
|
||||
deleteKlausur: core.deleteKlausur,
|
||||
// Students
|
||||
getStudents,
|
||||
getStudent,
|
||||
uploadStudentWork,
|
||||
deleteStudent,
|
||||
|
||||
getStudents: core.getStudents,
|
||||
getStudent: core.getStudent,
|
||||
uploadStudentWork: core.uploadStudentWork,
|
||||
deleteStudent: core.deleteStudent,
|
||||
// Criteria & Gutachten
|
||||
updateCriteria,
|
||||
updateGutachten,
|
||||
generateGutachten,
|
||||
|
||||
updateCriteria: core.updateCriteria,
|
||||
updateGutachten: core.updateGutachten,
|
||||
generateGutachten: core.generateGutachten,
|
||||
// Annotations
|
||||
getAnnotations,
|
||||
createAnnotation,
|
||||
updateAnnotation,
|
||||
deleteAnnotation,
|
||||
|
||||
getAnnotations: core.getAnnotations,
|
||||
createAnnotation: core.createAnnotation,
|
||||
updateAnnotation: core.updateAnnotation,
|
||||
deleteAnnotation: core.deleteAnnotation,
|
||||
// EH/RAG
|
||||
getEHSuggestions,
|
||||
queryRAG,
|
||||
uploadEH,
|
||||
|
||||
getEHSuggestions: core.getEHSuggestions,
|
||||
queryRAG: core.queryRAG,
|
||||
uploadEH: core.uploadEH,
|
||||
// Fairness & Export
|
||||
getFairnessAnalysis,
|
||||
getGradeInfo,
|
||||
getGutachtenExportUrl,
|
||||
getAnnotationsExportUrl,
|
||||
getOverviewExportUrl,
|
||||
getAllGutachtenExportUrl,
|
||||
getStudentFileUrl,
|
||||
|
||||
getFairnessAnalysis: core.getFairnessAnalysis,
|
||||
getGradeInfo: core.getGradeInfo,
|
||||
getGutachtenExportUrl: core.getGutachtenExportUrl,
|
||||
getAnnotationsExportUrl: core.getAnnotationsExportUrl,
|
||||
getOverviewExportUrl: core.getOverviewExportUrl,
|
||||
getAllGutachtenExportUrl: core.getAllGutachtenExportUrl,
|
||||
getStudentFileUrl: core.getStudentFileUrl,
|
||||
// Archiv (NiBiS)
|
||||
getArchivDocuments,
|
||||
getArchivDocument,
|
||||
getArchivDocumentUrl,
|
||||
searchArchivSemantic,
|
||||
getArchivSuggestions,
|
||||
getArchivStats,
|
||||
|
||||
getArchivDocuments: archiv.getArchivDocuments,
|
||||
getArchivDocument: archiv.getArchivDocument,
|
||||
getArchivDocumentUrl: archiv.getArchivDocumentUrl,
|
||||
searchArchivSemantic: archiv.searchArchivSemantic,
|
||||
getArchivSuggestions: archiv.getArchivSuggestions,
|
||||
getArchivStats: archiv.getArchivStats,
|
||||
// Stats
|
||||
getKorrekturStats,
|
||||
getKorrekturStats: archiv.getKorrekturStats,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user