// API 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 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 | null student_count: number students: StudentKlausur[] created_at: string teacher_id: string } export interface GradeInfo { thresholds: Record labels: Record criteria: Record } // Get auth token from parent window or localStorage function getAuthToken(): string | null { // Try to get from parent window (iframe scenario) 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 return localStorage.getItem('auth_token') } // Base API call async function apiCall( endpoint: string, options: RequestInit = {} ): Promise { const token = getAuthToken() const headers: Record = { 'Content-Type': 'application/json', ...(options.headers as Record || {}) } 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 => apiCall('/klausuren'), getKlausur: (id: string): Promise => apiCall(`/klausuren/${id}`), createKlausur: (data: Partial): Promise => apiCall('/klausuren', { method: 'POST', body: JSON.stringify(data) }), updateKlausur: (id: string, data: Partial): Promise => apiCall(`/klausuren/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteKlausur: (id: string): Promise<{ success: boolean }> => apiCall(`/klausuren/${id}`, { method: 'DELETE' }), // Students listStudents: (klausurId: string): Promise => apiCall(`/klausuren/${klausurId}/students`), deleteStudent: (studentId: string): Promise<{ success: boolean }> => apiCall(`/students/${studentId}`, { method: 'DELETE' }), // Grading updateCriteria: ( studentId: string, criterion: string, score: number, annotations?: string[] ): Promise => 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 => apiCall(`/students/${studentId}/gutachten`, { method: 'PUT', body: JSON.stringify(gutachten) }), finalizeStudent: (studentId: string): Promise => 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 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 | null }>> => apiCall(`/students/${studentId}/audit-log`), // Utilities getGradeInfo: (): Promise => apiCall('/grade-info') } // File upload (special handling for multipart) export async function uploadStudentWork( klausurId: string, studentName: string, file: File ): Promise { const token = getAuthToken() const formData = new FormData() formData.append('file', file) formData.append('student_name', studentName) const headers: Record = {} 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 | 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 => { 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 => apiCall(`/eh/${id}`), // Upload encrypted EH (special handling for FormData) uploadEH: async (formData: FormData): Promise => { const token = getAuthToken() const headers: Record = {} 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 => 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 => { 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 => 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 => 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 => apiCall('/eh/invitations/pending'), // Get sent invitations (as inviter) getSentInvitations: (): Promise => 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 => 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 => apiCall(`/klausuren/${klausurId}/linked-eh`) }