[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:
92
klausur-service/frontend/src/services/api-eh-types.ts
Normal file
92
klausur-service/frontend/src/services/api-eh-types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* BYOEH (Erwartungshorizont) Types
|
||||
*
|
||||
* Split from api.ts for file size compliance.
|
||||
*/
|
||||
|
||||
export interface Erwartungshorizont {
|
||||
id: string
|
||||
tenant_id: string
|
||||
teacher_id: string
|
||||
title: string
|
||||
subject: string
|
||||
niveau: 'eA' | 'gA'
|
||||
year: number
|
||||
aufgaben_nummer: string | null
|
||||
status: 'pending_rights' | 'processing' | 'indexed' | 'error'
|
||||
chunk_count: number
|
||||
rights_confirmed: boolean
|
||||
rights_confirmed_at: string | null
|
||||
indexed_at: string | null
|
||||
file_size_bytes: number
|
||||
original_filename: string
|
||||
training_allowed: boolean
|
||||
created_at: string
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
export interface EHRAGResult {
|
||||
context: string
|
||||
sources: Array<{
|
||||
text: string; eh_id: string; eh_title: string;
|
||||
chunk_index: number; score: number; reranked?: boolean
|
||||
}>
|
||||
query: string
|
||||
search_info?: {
|
||||
retrieval_time_ms?: number; rerank_time_ms?: number; total_time_ms?: number;
|
||||
reranked?: boolean; rerank_applied?: boolean; hybrid_search_applied?: boolean;
|
||||
embedding_model?: string; total_candidates?: number; original_count?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface EHAuditEntry {
|
||||
id: string; eh_id: string | null; tenant_id: string; user_id: string;
|
||||
action: string; details: Record<string, unknown> | null; created_at: string
|
||||
}
|
||||
|
||||
export interface EHKeyShare {
|
||||
id: string; eh_id: string; user_id: string; passphrase_hint: string;
|
||||
granted_by: string; granted_at: string;
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head';
|
||||
klausur_id: string | null; active: boolean
|
||||
}
|
||||
|
||||
export interface EHKlausurLink {
|
||||
id: string; eh_id: string; klausur_id: string;
|
||||
linked_by: string; linked_at: string
|
||||
}
|
||||
|
||||
export interface SharedEHInfo { eh: Erwartungshorizont; share: EHKeyShare }
|
||||
|
||||
export interface LinkedEHInfo {
|
||||
eh: Erwartungshorizont; link: EHKlausurLink;
|
||||
is_owner: boolean; share: EHKeyShare | null
|
||||
}
|
||||
|
||||
export interface EHShareInvitation {
|
||||
id: string; eh_id: string; inviter_id: string; invitee_id: string;
|
||||
invitee_email: string;
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head' | 'fachvorsitz';
|
||||
klausur_id: string | null; message: string | null;
|
||||
status: 'pending' | 'accepted' | 'declined' | 'expired' | 'revoked';
|
||||
expires_at: string; created_at: string;
|
||||
accepted_at: string | null; declined_at: string | null
|
||||
}
|
||||
|
||||
export interface PendingInvitationInfo {
|
||||
invitation: EHShareInvitation
|
||||
eh: { id: string; title: string; subject: string; niveau: string; year: number } | null
|
||||
}
|
||||
|
||||
export interface SentInvitationInfo {
|
||||
invitation: EHShareInvitation
|
||||
eh: { id: string; title: string; subject: string } | null
|
||||
}
|
||||
|
||||
export interface EHAccessChain {
|
||||
eh_id: string; eh_title: string;
|
||||
owner: { user_id: string; role: string };
|
||||
active_shares: EHKeyShare[];
|
||||
pending_invitations: EHShareInvitation[];
|
||||
revoked_shares: EHKeyShare[]
|
||||
}
|
||||
98
klausur-service/frontend/src/services/api-eh.ts
Normal file
98
klausur-service/frontend/src/services/api-eh.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* BYOEH (Erwartungshorizont) API
|
||||
*
|
||||
* Split from api.ts for file size compliance.
|
||||
*/
|
||||
|
||||
import { apiCall, getAuthToken } from './api'
|
||||
import type {
|
||||
Erwartungshorizont, EHRAGResult, EHAuditEntry, EHKeyShare,
|
||||
SharedEHInfo, LinkedEHInfo, PendingInvitationInfo, SentInvitationInfo,
|
||||
EHAccessChain,
|
||||
} from './api-eh-types'
|
||||
|
||||
export const ehApi = {
|
||||
listEH: (params?: { subject?: string; year?: number }): Promise<Erwartungshorizont[]> => {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.subject) query.append('subject', params.subject)
|
||||
if (params?.year) query.append('year', params.year.toString())
|
||||
const queryStr = query.toString()
|
||||
return apiCall(`/eh${queryStr ? `?${queryStr}` : ''}`)
|
||||
},
|
||||
|
||||
getEH: (id: string): Promise<Erwartungshorizont> => apiCall(`/eh/${id}`),
|
||||
|
||||
uploadEH: async (formData: FormData): Promise<Erwartungshorizont> => {
|
||||
const token = getAuthToken()
|
||||
const headers: Record<string, string> = {}
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
const response = await fetch('/api/v1/eh/upload', { method: 'POST', headers, body: formData })
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Upload failed' }))
|
||||
throw new Error(error.detail || `HTTP ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
|
||||
deleteEH: (id: string): Promise<{ status: string; id: string }> =>
|
||||
apiCall(`/eh/${id}`, { method: 'DELETE' }),
|
||||
|
||||
indexEH: (id: string, passphrase: string): Promise<{ status: string; id: string; chunk_count: number }> =>
|
||||
apiCall(`/eh/${id}/index`, { method: 'POST', body: JSON.stringify({ passphrase }) }),
|
||||
|
||||
ragQuery: (params: { query_text: string; passphrase: string; subject?: string; limit?: number; rerank?: boolean }): Promise<EHRAGResult> =>
|
||||
apiCall('/eh/rag-query', { method: 'POST', body: JSON.stringify({ query_text: params.query_text, passphrase: params.passphrase, subject: params.subject, limit: params.limit ?? 5, rerank: params.rerank ?? false }) }),
|
||||
|
||||
getAuditLog: (ehId?: string, limit?: number): Promise<EHAuditEntry[]> => {
|
||||
const query = new URLSearchParams()
|
||||
if (ehId) query.append('eh_id', ehId)
|
||||
if (limit) query.append('limit', limit.toString())
|
||||
const queryStr = query.toString()
|
||||
return apiCall(`/eh/audit-log${queryStr ? `?${queryStr}` : ''}`)
|
||||
},
|
||||
|
||||
getRightsText: (): Promise<{ text: string; version: string }> => apiCall('/eh/rights-text'),
|
||||
|
||||
getQdrantStatus: (): Promise<{ name: string; vectors_count: number; points_count: number; status: string }> =>
|
||||
apiCall('/eh/qdrant-status'),
|
||||
|
||||
// Key Sharing
|
||||
shareEH: (ehId: string, params: { user_id: string; role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head'; encrypted_passphrase: string; passphrase_hint?: string; klausur_id?: string }): Promise<{ status: string; share_id: string; eh_id: string; shared_with: string; role: string }> =>
|
||||
apiCall(`/eh/${ehId}/share`, { method: 'POST', body: JSON.stringify(params) }),
|
||||
|
||||
listShares: (ehId: string): Promise<EHKeyShare[]> => apiCall(`/eh/${ehId}/shares`),
|
||||
|
||||
revokeShare: (ehId: string, shareId: string): Promise<{ status: string; share_id: string }> =>
|
||||
apiCall(`/eh/${ehId}/shares/${shareId}`, { method: 'DELETE' }),
|
||||
|
||||
getSharedWithMe: (): Promise<SharedEHInfo[]> => apiCall('/eh/shared-with-me'),
|
||||
|
||||
linkToKlausur: (ehId: string, klausurId: string): Promise<{ status: string; link_id: string; eh_id: string; klausur_id: string }> =>
|
||||
apiCall(`/eh/${ehId}/link-klausur`, { method: 'POST', body: JSON.stringify({ klausur_id: klausurId }) }),
|
||||
|
||||
unlinkFromKlausur: (ehId: string, klausurId: string): Promise<{ status: string; eh_id: string; klausur_id: string }> =>
|
||||
apiCall(`/eh/${ehId}/link-klausur/${klausurId}`, { method: 'DELETE' }),
|
||||
|
||||
// Invitation Flow
|
||||
inviteToEH: (ehId: string, params: { invitee_email: string; invitee_id?: string; role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head' | 'fachvorsitz'; klausur_id?: string; message?: string; expires_in_days?: number }): Promise<{ status: string; invitation_id: string; eh_id: string; invitee_email: string; role: string; expires_at: string; eh_title: string }> =>
|
||||
apiCall(`/eh/${ehId}/invite`, { method: 'POST', body: JSON.stringify(params) }),
|
||||
|
||||
getPendingInvitations: (): Promise<PendingInvitationInfo[]> => apiCall('/eh/invitations/pending'),
|
||||
|
||||
getSentInvitations: (): Promise<SentInvitationInfo[]> => apiCall('/eh/invitations/sent'),
|
||||
|
||||
acceptInvitation: (invitationId: string, encryptedPassphrase: string): Promise<{ status: string; share_id: string; eh_id: string; role: string; klausur_id: string | null }> =>
|
||||
apiCall(`/eh/invitations/${invitationId}/accept`, { method: 'POST', body: JSON.stringify({ encrypted_passphrase: encryptedPassphrase }) }),
|
||||
|
||||
declineInvitation: (invitationId: string): Promise<{ status: string; invitation_id: string; eh_id: string }> =>
|
||||
apiCall(`/eh/invitations/${invitationId}/decline`, { method: 'POST' }),
|
||||
|
||||
revokeInvitation: (invitationId: string): Promise<{ status: string; invitation_id: string; eh_id: string }> =>
|
||||
apiCall(`/eh/invitations/${invitationId}`, { method: 'DELETE' }),
|
||||
|
||||
getAccessChain: (ehId: string): Promise<EHAccessChain> => apiCall(`/eh/${ehId}/access-chain`),
|
||||
}
|
||||
|
||||
export const klausurEHApi = {
|
||||
getLinkedEH: (klausurId: string): Promise<LinkedEHInfo[]> => apiCall(`/klausuren/${klausurId}/linked-eh`),
|
||||
}
|
||||
@@ -1,620 +1,123 @@
|
||||
// API Types
|
||||
/**
|
||||
* Klausur Service API - Core types and Klausur/Student API
|
||||
*
|
||||
* Split into:
|
||||
* - api.ts (this file): Core types, auth, base API, klausurApi, uploadStudentWork
|
||||
* - api-eh-types.ts: BYOEH type definitions
|
||||
* - api-eh.ts: ehApi and klausurEHApi
|
||||
*/
|
||||
|
||||
// Re-export EH types and API for backward compatibility
|
||||
export type {
|
||||
Erwartungshorizont, EHRAGResult, EHAuditEntry, EHKeyShare, EHKlausurLink,
|
||||
SharedEHInfo, LinkedEHInfo, EHShareInvitation, PendingInvitationInfo,
|
||||
SentInvitationInfo, EHAccessChain,
|
||||
} from './api-eh-types'
|
||||
|
||||
export { ehApi, klausurEHApi } from './api-eh'
|
||||
|
||||
// ============================================================================
|
||||
// Core Types
|
||||
// ============================================================================
|
||||
|
||||
export interface StudentKlausur {
|
||||
id: string
|
||||
klausur_id: string
|
||||
student_name: string
|
||||
student_id: string | null
|
||||
file_path: string | null
|
||||
ocr_text: string | null
|
||||
status: string
|
||||
criteria_scores: Record<string, { score: number; annotations: string[] }>
|
||||
gutachten: {
|
||||
einleitung: string
|
||||
hauptteil: string
|
||||
fazit: string
|
||||
staerken: string[]
|
||||
schwaechen: string[]
|
||||
} | null
|
||||
raw_points: number
|
||||
grade_points: number
|
||||
created_at: string
|
||||
id: string; klausur_id: string; student_name: string; student_id: string | null;
|
||||
file_path: string | null; ocr_text: string | null; status: string;
|
||||
criteria_scores: Record<string, { score: number; annotations: string[] }>;
|
||||
gutachten: { einleitung: string; hauptteil: string; fazit: string; staerken: string[]; schwaechen: string[] } | null;
|
||||
raw_points: number; grade_points: number; created_at: string
|
||||
}
|
||||
|
||||
export interface Klausur {
|
||||
id: string
|
||||
title: string
|
||||
subject: string
|
||||
modus: 'landes_abitur' | 'vorabitur'
|
||||
class_id: string | null
|
||||
year: number
|
||||
semester: string
|
||||
erwartungshorizont: Record<string, unknown> | null
|
||||
student_count: number
|
||||
students: StudentKlausur[]
|
||||
created_at: string
|
||||
teacher_id: string
|
||||
id: string; title: string; subject: string; modus: 'landes_abitur' | 'vorabitur';
|
||||
class_id: string | null; year: number; semester: string;
|
||||
erwartungshorizont: Record<string, unknown> | null; student_count: number;
|
||||
students: StudentKlausur[]; created_at: string; teacher_id: string
|
||||
}
|
||||
|
||||
export interface GradeInfo {
|
||||
thresholds: Record<number, number>
|
||||
labels: Record<number, string>
|
||||
thresholds: Record<number, number>; labels: Record<number, string>;
|
||||
criteria: Record<string, { weight: number; label: string }>
|
||||
}
|
||||
|
||||
// Get auth token from parent window or localStorage
|
||||
function getAuthToken(): string | null {
|
||||
// Try to get from parent window (iframe scenario)
|
||||
// ============================================================================
|
||||
// Auth & Base API
|
||||
// ============================================================================
|
||||
|
||||
export function getAuthToken(): string | null {
|
||||
try {
|
||||
if (window.parent !== window) {
|
||||
const parentToken = (window.parent as unknown as { authToken?: string }).authToken
|
||||
if (parentToken) return parentToken
|
||||
}
|
||||
} catch {
|
||||
// Cross-origin access denied
|
||||
}
|
||||
|
||||
// Try localStorage
|
||||
} catch { /* Cross-origin */ }
|
||||
return localStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
// Base API call
|
||||
async function apiCall<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
export async function apiCall<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const token = getAuthToken()
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> || {})
|
||||
}
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
})
|
||||
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
const response = await fetch(`/api/v1${endpoint}`, { ...options, headers })
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Request failed' }))
|
||||
throw new Error(error.detail || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Klausuren API
|
||||
// ============================================================================
|
||||
|
||||
export const klausurApi = {
|
||||
listKlausuren: (): Promise<Klausur[]> =>
|
||||
apiCall('/klausuren'),
|
||||
listKlausuren: (): Promise<Klausur[]> => apiCall('/klausuren'),
|
||||
getKlausur: (id: string): Promise<Klausur> => apiCall(`/klausuren/${id}`),
|
||||
createKlausur: (data: Partial<Klausur>): Promise<Klausur> => apiCall('/klausuren', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateKlausur: (id: string, data: Partial<Klausur>): Promise<Klausur> => apiCall(`/klausuren/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||||
deleteKlausur: (id: string): Promise<{ success: boolean }> => apiCall(`/klausuren/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getKlausur: (id: string): Promise<Klausur> =>
|
||||
apiCall(`/klausuren/${id}`),
|
||||
listStudents: (klausurId: string): Promise<StudentKlausur[]> => apiCall(`/klausuren/${klausurId}/students`),
|
||||
deleteStudent: (studentId: string): Promise<{ success: boolean }> => apiCall(`/students/${studentId}`, { method: 'DELETE' }),
|
||||
|
||||
createKlausur: (data: Partial<Klausur>): Promise<Klausur> =>
|
||||
apiCall('/klausuren', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
}),
|
||||
updateCriteria: (studentId: string, criterion: string, score: number, annotations?: string[]): Promise<StudentKlausur> =>
|
||||
apiCall(`/students/${studentId}/criteria`, { method: 'PUT', body: JSON.stringify({ criterion, score, annotations }) }),
|
||||
|
||||
updateKlausur: (id: string, data: Partial<Klausur>): Promise<Klausur> =>
|
||||
apiCall(`/klausuren/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
}),
|
||||
updateGutachten: (studentId: string, gutachten: { einleitung: string; hauptteil: string; fazit: string; staerken?: string[]; schwaechen?: string[] }): Promise<StudentKlausur> =>
|
||||
apiCall(`/students/${studentId}/gutachten`, { method: 'PUT', body: JSON.stringify(gutachten) }),
|
||||
|
||||
deleteKlausur: (id: string): Promise<{ success: boolean }> =>
|
||||
apiCall(`/klausuren/${id}`, { method: 'DELETE' }),
|
||||
finalizeStudent: (studentId: string): Promise<StudentKlausur> => apiCall(`/students/${studentId}/finalize`, { method: 'POST' }),
|
||||
|
||||
// Students
|
||||
listStudents: (klausurId: string): Promise<StudentKlausur[]> =>
|
||||
apiCall(`/klausuren/${klausurId}/students`),
|
||||
generateGutachten: (studentId: string, options: { include_strengths?: boolean; include_weaknesses?: boolean; tone?: 'formal' | 'friendly' | 'constructive' } = {}): Promise<{ einleitung: string; hauptteil: string; fazit: string; staerken: string[]; schwaechen: string[]; generated_at: string; is_ki_generated: boolean; tone: string }> =>
|
||||
apiCall(`/students/${studentId}/gutachten/generate`, { method: 'POST', body: JSON.stringify({ include_strengths: options.include_strengths ?? true, include_weaknesses: options.include_weaknesses ?? true, tone: options.tone ?? 'formal' }) }),
|
||||
|
||||
deleteStudent: (studentId: string): Promise<{ success: boolean }> =>
|
||||
apiCall(`/students/${studentId}`, { method: 'DELETE' }),
|
||||
|
||||
// Grading
|
||||
updateCriteria: (
|
||||
studentId: string,
|
||||
criterion: string,
|
||||
score: number,
|
||||
annotations?: string[]
|
||||
): Promise<StudentKlausur> =>
|
||||
apiCall(`/students/${studentId}/criteria`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ criterion, score, annotations })
|
||||
}),
|
||||
|
||||
updateGutachten: (
|
||||
studentId: string,
|
||||
gutachten: {
|
||||
einleitung: string
|
||||
hauptteil: string
|
||||
fazit: string
|
||||
staerken?: string[]
|
||||
schwaechen?: string[]
|
||||
}
|
||||
): Promise<StudentKlausur> =>
|
||||
apiCall(`/students/${studentId}/gutachten`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(gutachten)
|
||||
}),
|
||||
|
||||
finalizeStudent: (studentId: string): Promise<StudentKlausur> =>
|
||||
apiCall(`/students/${studentId}/finalize`, { method: 'POST' }),
|
||||
|
||||
// KI-Gutachten Generation
|
||||
generateGutachten: (
|
||||
studentId: string,
|
||||
options: {
|
||||
include_strengths?: boolean
|
||||
include_weaknesses?: boolean
|
||||
tone?: 'formal' | 'friendly' | 'constructive'
|
||||
} = {}
|
||||
): Promise<{
|
||||
einleitung: string
|
||||
hauptteil: string
|
||||
fazit: string
|
||||
staerken: string[]
|
||||
schwaechen: string[]
|
||||
generated_at: string
|
||||
is_ki_generated: boolean
|
||||
tone: string
|
||||
}> =>
|
||||
apiCall(`/students/${studentId}/gutachten/generate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
include_strengths: options.include_strengths ?? true,
|
||||
include_weaknesses: options.include_weaknesses ?? true,
|
||||
tone: options.tone ?? 'formal'
|
||||
})
|
||||
}),
|
||||
|
||||
// Fairness Analysis
|
||||
getFairnessAnalysis: (klausurId: string): Promise<{
|
||||
klausur_id: string
|
||||
students_count: number
|
||||
graded_count: number
|
||||
statistics: {
|
||||
average_grade: number
|
||||
average_raw_points: number
|
||||
min_grade: number
|
||||
max_grade: number
|
||||
spread: number
|
||||
standard_deviation: number
|
||||
}
|
||||
criteria_breakdown: Record<string, { average: number; min: number; max: number; count: number }>
|
||||
outliers: Array<{
|
||||
student_id: string
|
||||
student_name: string
|
||||
grade_points: number
|
||||
deviation: number
|
||||
direction: 'above' | 'below'
|
||||
}>
|
||||
fairness_score: number
|
||||
warnings: string[]
|
||||
recommendation: string
|
||||
}> =>
|
||||
getFairnessAnalysis: (klausurId: string): Promise<{ klausur_id: string; students_count: number; graded_count: number; statistics: { average_grade: number; average_raw_points: number; min_grade: number; max_grade: number; spread: number; standard_deviation: number }; criteria_breakdown: Record<string, { average: number; min: number; max: number; count: number }>; outliers: Array<{ student_id: string; student_name: string; grade_points: number; deviation: number; direction: 'above' | 'below' }>; fairness_score: number; warnings: string[]; recommendation: string }> =>
|
||||
apiCall(`/klausuren/${klausurId}/fairness`),
|
||||
|
||||
// Audit Log
|
||||
getStudentAuditLog: (studentId: string): Promise<Array<{
|
||||
id: string
|
||||
timestamp: string
|
||||
user_id: string
|
||||
action: string
|
||||
entity_type: string
|
||||
entity_id: string
|
||||
field: string | null
|
||||
old_value: string | null
|
||||
new_value: string | null
|
||||
details: Record<string, unknown> | null
|
||||
}>> =>
|
||||
getStudentAuditLog: (studentId: string): Promise<Array<{ id: string; timestamp: string; user_id: string; action: string; entity_type: string; entity_id: string; field: string | null; old_value: string | null; new_value: string | null; details: Record<string, unknown> | null }>> =>
|
||||
apiCall(`/students/${studentId}/audit-log`),
|
||||
|
||||
// Utilities
|
||||
getGradeInfo: (): Promise<GradeInfo> =>
|
||||
apiCall('/grade-info')
|
||||
getGradeInfo: (): Promise<GradeInfo> => apiCall('/grade-info'),
|
||||
}
|
||||
|
||||
// File upload (special handling for multipart)
|
||||
export async function uploadStudentWork(
|
||||
klausurId: string,
|
||||
studentName: string,
|
||||
file: File
|
||||
): Promise<StudentKlausur> {
|
||||
const token = getAuthToken()
|
||||
// ============================================================================
|
||||
// File Upload
|
||||
// ============================================================================
|
||||
|
||||
export async function uploadStudentWork(klausurId: string, studentName: string, file: File): Promise<StudentKlausur> {
|
||||
const token = getAuthToken()
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('student_name', studentName)
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/v1/klausuren/${klausurId}/students`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
const response = await fetch(`/api/v1/klausuren/${klausurId}/students`, { method: 'POST', headers, body: formData })
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Upload failed' }))
|
||||
throw new Error(error.detail || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// BYOEH (Erwartungshorizont) Types & API
|
||||
// =============================================
|
||||
|
||||
export interface Erwartungshorizont {
|
||||
id: string
|
||||
tenant_id: string
|
||||
teacher_id: string
|
||||
title: string
|
||||
subject: string
|
||||
niveau: 'eA' | 'gA'
|
||||
year: number
|
||||
aufgaben_nummer: string | null
|
||||
status: 'pending_rights' | 'processing' | 'indexed' | 'error'
|
||||
chunk_count: number
|
||||
rights_confirmed: boolean
|
||||
rights_confirmed_at: string | null
|
||||
indexed_at: string | null
|
||||
file_size_bytes: number
|
||||
original_filename: string
|
||||
training_allowed: boolean
|
||||
created_at: string
|
||||
deleted_at: string | null
|
||||
}
|
||||
|
||||
export interface EHRAGResult {
|
||||
context: string
|
||||
sources: Array<{
|
||||
text: string
|
||||
eh_id: string
|
||||
eh_title: string
|
||||
chunk_index: number
|
||||
score: number
|
||||
reranked?: boolean
|
||||
}>
|
||||
query: string
|
||||
search_info?: {
|
||||
retrieval_time_ms?: number
|
||||
rerank_time_ms?: number
|
||||
total_time_ms?: number
|
||||
reranked?: boolean
|
||||
rerank_applied?: boolean
|
||||
hybrid_search_applied?: boolean
|
||||
embedding_model?: string
|
||||
total_candidates?: number
|
||||
original_count?: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface EHAuditEntry {
|
||||
id: string
|
||||
eh_id: string | null
|
||||
tenant_id: string
|
||||
user_id: string
|
||||
action: string
|
||||
details: Record<string, unknown> | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface EHKeyShare {
|
||||
id: string
|
||||
eh_id: string
|
||||
user_id: string
|
||||
passphrase_hint: string
|
||||
granted_by: string
|
||||
granted_at: string
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head'
|
||||
klausur_id: string | null
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export interface EHKlausurLink {
|
||||
id: string
|
||||
eh_id: string
|
||||
klausur_id: string
|
||||
linked_by: string
|
||||
linked_at: string
|
||||
}
|
||||
|
||||
export interface SharedEHInfo {
|
||||
eh: Erwartungshorizont
|
||||
share: EHKeyShare
|
||||
}
|
||||
|
||||
export interface LinkedEHInfo {
|
||||
eh: Erwartungshorizont
|
||||
link: EHKlausurLink
|
||||
is_owner: boolean
|
||||
share: EHKeyShare | null
|
||||
}
|
||||
|
||||
// Invitation types for Invite/Accept/Revoke flow
|
||||
export interface EHShareInvitation {
|
||||
id: string
|
||||
eh_id: string
|
||||
inviter_id: string
|
||||
invitee_id: string
|
||||
invitee_email: string
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head' | 'fachvorsitz'
|
||||
klausur_id: string | null
|
||||
message: string | null
|
||||
status: 'pending' | 'accepted' | 'declined' | 'expired' | 'revoked'
|
||||
expires_at: string
|
||||
created_at: string
|
||||
accepted_at: string | null
|
||||
declined_at: string | null
|
||||
}
|
||||
|
||||
export interface PendingInvitationInfo {
|
||||
invitation: EHShareInvitation
|
||||
eh: {
|
||||
id: string
|
||||
title: string
|
||||
subject: string
|
||||
niveau: string
|
||||
year: number
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface SentInvitationInfo {
|
||||
invitation: EHShareInvitation
|
||||
eh: {
|
||||
id: string
|
||||
title: string
|
||||
subject: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export interface EHAccessChain {
|
||||
eh_id: string
|
||||
eh_title: string
|
||||
owner: {
|
||||
user_id: string
|
||||
role: string
|
||||
}
|
||||
active_shares: EHKeyShare[]
|
||||
pending_invitations: EHShareInvitation[]
|
||||
revoked_shares: EHKeyShare[]
|
||||
}
|
||||
|
||||
// Erwartungshorizont API
|
||||
export const ehApi = {
|
||||
// List all EH for current teacher
|
||||
listEH: (params?: { subject?: string; year?: number }): Promise<Erwartungshorizont[]> => {
|
||||
const query = new URLSearchParams()
|
||||
if (params?.subject) query.append('subject', params.subject)
|
||||
if (params?.year) query.append('year', params.year.toString())
|
||||
const queryStr = query.toString()
|
||||
return apiCall(`/eh${queryStr ? `?${queryStr}` : ''}`)
|
||||
},
|
||||
|
||||
// Get single EH by ID
|
||||
getEH: (id: string): Promise<Erwartungshorizont> =>
|
||||
apiCall(`/eh/${id}`),
|
||||
|
||||
// Upload encrypted EH (special handling for FormData)
|
||||
uploadEH: async (formData: FormData): Promise<Erwartungshorizont> => {
|
||||
const token = getAuthToken()
|
||||
const headers: Record<string, string> = {}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
const response = await fetch('/api/v1/eh/upload', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Upload failed' }))
|
||||
throw new Error(error.detail || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// Delete EH (soft delete)
|
||||
deleteEH: (id: string): Promise<{ status: string; id: string }> =>
|
||||
apiCall(`/eh/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// Index EH for RAG (requires passphrase)
|
||||
indexEH: (id: string, passphrase: string): Promise<{ status: string; id: string; chunk_count: number }> =>
|
||||
apiCall(`/eh/${id}/index`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ passphrase })
|
||||
}),
|
||||
|
||||
// RAG query against EH
|
||||
ragQuery: (params: {
|
||||
query_text: string
|
||||
passphrase: string
|
||||
subject?: string
|
||||
limit?: number
|
||||
rerank?: boolean
|
||||
}): Promise<EHRAGResult> =>
|
||||
apiCall('/eh/rag-query', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query_text: params.query_text,
|
||||
passphrase: params.passphrase,
|
||||
subject: params.subject,
|
||||
limit: params.limit ?? 5,
|
||||
rerank: params.rerank ?? false
|
||||
})
|
||||
}),
|
||||
|
||||
// Get audit log
|
||||
getAuditLog: (ehId?: string, limit?: number): Promise<EHAuditEntry[]> => {
|
||||
const query = new URLSearchParams()
|
||||
if (ehId) query.append('eh_id', ehId)
|
||||
if (limit) query.append('limit', limit.toString())
|
||||
const queryStr = query.toString()
|
||||
return apiCall(`/eh/audit-log${queryStr ? `?${queryStr}` : ''}`)
|
||||
},
|
||||
|
||||
// Get rights confirmation text
|
||||
getRightsText: (): Promise<{ text: string; version: string }> =>
|
||||
apiCall('/eh/rights-text'),
|
||||
|
||||
// Get Qdrant status (admin only)
|
||||
getQdrantStatus: (): Promise<{
|
||||
name: string
|
||||
vectors_count: number
|
||||
points_count: number
|
||||
status: string
|
||||
}> =>
|
||||
apiCall('/eh/qdrant-status'),
|
||||
|
||||
// =============================================
|
||||
// KEY SHARING
|
||||
// =============================================
|
||||
|
||||
// Share EH with another examiner
|
||||
shareEH: (
|
||||
ehId: string,
|
||||
params: {
|
||||
user_id: string
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head'
|
||||
encrypted_passphrase: string
|
||||
passphrase_hint?: string
|
||||
klausur_id?: string
|
||||
}
|
||||
): Promise<{
|
||||
status: string
|
||||
share_id: string
|
||||
eh_id: string
|
||||
shared_with: string
|
||||
role: string
|
||||
}> =>
|
||||
apiCall(`/eh/${ehId}/share`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params)
|
||||
}),
|
||||
|
||||
// List shares for an EH (owner only)
|
||||
listShares: (ehId: string): Promise<EHKeyShare[]> =>
|
||||
apiCall(`/eh/${ehId}/shares`),
|
||||
|
||||
// Revoke a share
|
||||
revokeShare: (ehId: string, shareId: string): Promise<{ status: string; share_id: string }> =>
|
||||
apiCall(`/eh/${ehId}/shares/${shareId}`, { method: 'DELETE' }),
|
||||
|
||||
// Get EH shared with current user
|
||||
getSharedWithMe: (): Promise<SharedEHInfo[]> =>
|
||||
apiCall('/eh/shared-with-me'),
|
||||
|
||||
// Link EH to a Klausur
|
||||
linkToKlausur: (ehId: string, klausurId: string): Promise<{
|
||||
status: string
|
||||
link_id: string
|
||||
eh_id: string
|
||||
klausur_id: string
|
||||
}> =>
|
||||
apiCall(`/eh/${ehId}/link-klausur`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ klausur_id: klausurId })
|
||||
}),
|
||||
|
||||
// Unlink EH from a Klausur
|
||||
unlinkFromKlausur: (ehId: string, klausurId: string): Promise<{
|
||||
status: string
|
||||
eh_id: string
|
||||
klausur_id: string
|
||||
}> =>
|
||||
apiCall(`/eh/${ehId}/link-klausur/${klausurId}`, { method: 'DELETE' }),
|
||||
|
||||
// =============================================
|
||||
// INVITATION FLOW (Invite / Accept / Revoke)
|
||||
// =============================================
|
||||
|
||||
// Send invitation to share EH
|
||||
inviteToEH: (
|
||||
ehId: string,
|
||||
params: {
|
||||
invitee_email: string
|
||||
invitee_id?: string
|
||||
role: 'second_examiner' | 'third_examiner' | 'supervisor' | 'department_head' | 'fachvorsitz'
|
||||
klausur_id?: string
|
||||
message?: string
|
||||
expires_in_days?: number
|
||||
}
|
||||
): Promise<{
|
||||
status: string
|
||||
invitation_id: string
|
||||
eh_id: string
|
||||
invitee_email: string
|
||||
role: string
|
||||
expires_at: string
|
||||
eh_title: string
|
||||
}> =>
|
||||
apiCall(`/eh/${ehId}/invite`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params)
|
||||
}),
|
||||
|
||||
// Get pending invitations for current user
|
||||
getPendingInvitations: (): Promise<PendingInvitationInfo[]> =>
|
||||
apiCall('/eh/invitations/pending'),
|
||||
|
||||
// Get sent invitations (as inviter)
|
||||
getSentInvitations: (): Promise<SentInvitationInfo[]> =>
|
||||
apiCall('/eh/invitations/sent'),
|
||||
|
||||
// Accept an invitation
|
||||
acceptInvitation: (
|
||||
invitationId: string,
|
||||
encryptedPassphrase: string
|
||||
): Promise<{
|
||||
status: string
|
||||
share_id: string
|
||||
eh_id: string
|
||||
role: string
|
||||
klausur_id: string | null
|
||||
}> =>
|
||||
apiCall(`/eh/invitations/${invitationId}/accept`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ encrypted_passphrase: encryptedPassphrase })
|
||||
}),
|
||||
|
||||
// Decline an invitation
|
||||
declineInvitation: (invitationId: string): Promise<{
|
||||
status: string
|
||||
invitation_id: string
|
||||
eh_id: string
|
||||
}> =>
|
||||
apiCall(`/eh/invitations/${invitationId}/decline`, { method: 'POST' }),
|
||||
|
||||
// Revoke an invitation (as inviter)
|
||||
revokeInvitation: (invitationId: string): Promise<{
|
||||
status: string
|
||||
invitation_id: string
|
||||
eh_id: string
|
||||
}> =>
|
||||
apiCall(`/eh/invitations/${invitationId}`, { method: 'DELETE' }),
|
||||
|
||||
// Get the complete access chain for an EH
|
||||
getAccessChain: (ehId: string): Promise<EHAccessChain> =>
|
||||
apiCall(`/eh/${ehId}/access-chain`)
|
||||
}
|
||||
|
||||
// Get linked EH for a Klausur (separate from ehApi for clarity)
|
||||
export const klausurEHApi = {
|
||||
// Get all EH linked to a Klausur that the user has access to
|
||||
getLinkedEH: (klausurId: string): Promise<LinkedEHInfo[]> =>
|
||||
apiCall(`/klausuren/${klausurId}/linked-eh`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user