backend-lehrer (5 files): - alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3) - teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3) - mail/mail_db.py (987 → 6) klausur-service (5 files): - legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4) - ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2) - KorrekturPage.tsx (956 → 6) website (5 pages): - mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7) - ocr-labeling (946 → 7), audit-workspace (871 → 4) studio-v2 (5 files + 1 deleted): - page.tsx (946 → 5), MessagesContext.tsx (925 → 4) - korrektur (914 → 6), worksheet-cleanup (899 → 6) - useVocabWorksheet.ts (888 → 3) - Deleted dead page-original.tsx (934 LOC) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
157 lines
6.8 KiB
TypeScript
157 lines
6.8 KiB
TypeScript
import type {
|
|
VocabularyEntry, Session, StoredDocument, OcrPrompts, IpaMode, SyllableMode,
|
|
} from './types'
|
|
import { getApiBase } from './constants'
|
|
|
|
/**
|
|
* Start a new session: create on server, upload document, process first page or PDF.
|
|
*/
|
|
export async function startSessionFlow(params: {
|
|
sessionName: string
|
|
selectedDocumentId: string | null
|
|
directFile: File | null
|
|
selectedMobileFile: { dataUrl: string; type: string; name: string } | null
|
|
storedDocuments: StoredDocument[]
|
|
ocrPrompts: OcrPrompts
|
|
startActivity: (type: string, meta: any) => void
|
|
setSession: (s: Session | null | ((prev: Session | null) => Session | null)) => void
|
|
setWorksheetTitle: (t: string) => void
|
|
setExtractionStatus: (s: string) => void
|
|
setPdfPageCount: (n: number) => void
|
|
setSelectedPages: (p: number[]) => void
|
|
setPagesThumbnails: (t: string[]) => void
|
|
setIsLoadingThumbnails: (l: boolean) => void
|
|
setVocabulary: (v: VocabularyEntry[]) => void
|
|
setActiveTab: (t: 'upload' | 'pages' | 'vocabulary' | 'spreadsheet' | 'worksheet' | 'export' | 'settings') => void
|
|
setError: (e: string | null) => void
|
|
}): Promise<Session | null> {
|
|
const {
|
|
sessionName, selectedDocumentId, directFile, selectedMobileFile, storedDocuments,
|
|
ocrPrompts, startActivity, setSession, setWorksheetTitle, setExtractionStatus,
|
|
setPdfPageCount, setSelectedPages, setPagesThumbnails, setIsLoadingThumbnails,
|
|
setVocabulary, setActiveTab, setError,
|
|
} = params
|
|
|
|
setError(null)
|
|
setExtractionStatus('Session wird erstellt...')
|
|
|
|
const API_BASE = getApiBase()
|
|
|
|
const sessionRes = await fetch(`${API_BASE}/api/v1/vocab/sessions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: sessionName, ocr_prompts: ocrPrompts }),
|
|
})
|
|
|
|
if (!sessionRes.ok) throw new Error('Session konnte nicht erstellt werden')
|
|
|
|
const sessionData = await sessionRes.json()
|
|
setSession(sessionData)
|
|
setWorksheetTitle(sessionName)
|
|
startActivity('vocab_extraction', { description: sessionName })
|
|
|
|
let file: File
|
|
let isPdf = false
|
|
|
|
if (directFile) {
|
|
file = directFile
|
|
isPdf = directFile.type === 'application/pdf'
|
|
} else if (selectedMobileFile) {
|
|
isPdf = selectedMobileFile.type === 'application/pdf'
|
|
const base64Data = selectedMobileFile.dataUrl.split(',')[1]
|
|
const byteCharacters = atob(base64Data)
|
|
const byteNumbers = new Array(byteCharacters.length)
|
|
for (let i = 0; i < byteCharacters.length; i++) byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
const blob = new Blob([new Uint8Array(byteNumbers)], { type: selectedMobileFile.type })
|
|
file = new File([blob], selectedMobileFile.name, { type: selectedMobileFile.type })
|
|
} else {
|
|
const selectedDoc = storedDocuments.find(d => d.id === selectedDocumentId)
|
|
if (!selectedDoc || !selectedDoc.url) throw new Error('Das ausgewaehlte Dokument ist nicht verfuegbar.')
|
|
isPdf = selectedDoc.type === 'application/pdf'
|
|
const base64Data = selectedDoc.url.split(',')[1]
|
|
const byteCharacters = atob(base64Data)
|
|
const byteNumbers = new Array(byteCharacters.length)
|
|
for (let i = 0; i < byteCharacters.length; i++) byteNumbers[i] = byteCharacters.charCodeAt(i)
|
|
const blob = new Blob([new Uint8Array(byteNumbers)], { type: selectedDoc.type })
|
|
file = new File([blob], selectedDoc.name, { type: selectedDoc.type })
|
|
}
|
|
|
|
if (isPdf) {
|
|
setExtractionStatus('PDF wird hochgeladen...')
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
const pdfInfoRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${sessionData.id}/upload-pdf-info`, {
|
|
method: 'POST', body: formData,
|
|
})
|
|
if (!pdfInfoRes.ok) throw new Error('PDF konnte nicht verarbeitet werden')
|
|
const pdfInfo = await pdfInfoRes.json()
|
|
setPdfPageCount(pdfInfo.page_count)
|
|
setSelectedPages(Array.from({ length: pdfInfo.page_count }, (_, i) => i))
|
|
setActiveTab('pages')
|
|
setExtractionStatus(`${pdfInfo.page_count} Seiten erkannt. Vorschau wird geladen...`)
|
|
setIsLoadingThumbnails(true)
|
|
const thumbnails: string[] = []
|
|
for (let i = 0; i < pdfInfo.page_count; i++) {
|
|
try {
|
|
const thumbRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${sessionData.id}/pdf-thumbnail/${i}?hires=true`)
|
|
if (thumbRes.ok) { const blob = await thumbRes.blob(); thumbnails.push(URL.createObjectURL(blob)) }
|
|
} catch (e) { console.error(`Failed to load thumbnail for page ${i}`) }
|
|
}
|
|
setPagesThumbnails(thumbnails)
|
|
setIsLoadingThumbnails(false)
|
|
setExtractionStatus(`${pdfInfo.page_count} Seiten bereit. Waehlen Sie die zu verarbeitenden Seiten.`)
|
|
} else {
|
|
setExtractionStatus('KI analysiert das Bild... (kann 30-60 Sekunden dauern)')
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
const uploadRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${sessionData.id}/upload`, {
|
|
method: 'POST', body: formData,
|
|
})
|
|
if (!uploadRes.ok) throw new Error('Bild konnte nicht verarbeitet werden')
|
|
const uploadData = await uploadRes.json()
|
|
setSession(prev => prev ? { ...prev, status: 'extracted', vocabulary_count: uploadData.vocabulary_count } : null)
|
|
const vocabRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${sessionData.id}/vocabulary`)
|
|
if (vocabRes.ok) {
|
|
const vocabData = await vocabRes.json()
|
|
setVocabulary(vocabData.vocabulary || [])
|
|
setExtractionStatus(`${vocabData.vocabulary?.length || 0} Vokabeln gefunden!`)
|
|
}
|
|
await new Promise(r => setTimeout(r, 1000))
|
|
setActiveTab('vocabulary')
|
|
}
|
|
|
|
return sessionData
|
|
}
|
|
|
|
/**
|
|
* Resume an existing session from the API.
|
|
*/
|
|
export async function resumeSessionFlow(
|
|
existingSession: Session,
|
|
setSession: (s: Session) => void,
|
|
setWorksheetTitle: (t: string) => void,
|
|
setVocabulary: (v: VocabularyEntry[]) => void,
|
|
setActiveTab: (t: 'upload' | 'pages' | 'vocabulary' | 'spreadsheet' | 'worksheet' | 'export' | 'settings') => void,
|
|
setExtractionStatus: (s: string) => void,
|
|
): Promise<void> {
|
|
const API_BASE = getApiBase()
|
|
const sessionRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${existingSession.id}`)
|
|
if (!sessionRes.ok) throw new Error('Session nicht gefunden')
|
|
const sessionData = await sessionRes.json()
|
|
setSession(sessionData)
|
|
setWorksheetTitle(sessionData.name)
|
|
|
|
if (sessionData.status === 'extracted' || sessionData.status === 'completed') {
|
|
const vocabRes = await fetch(`${API_BASE}/api/v1/vocab/sessions/${existingSession.id}/vocabulary`)
|
|
if (vocabRes.ok) { const vd = await vocabRes.json(); setVocabulary(vd.vocabulary || []) }
|
|
setActiveTab('vocabulary')
|
|
setExtractionStatus('')
|
|
} else if (sessionData.status === 'pending') {
|
|
setActiveTab('upload')
|
|
setExtractionStatus('Diese Session hat noch keine Vokabeln. Bitte laden Sie ein Dokument hoch.')
|
|
} else {
|
|
setActiveTab('vocabulary')
|
|
setExtractionStatus('')
|
|
}
|
|
}
|