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>
161 lines
6.6 KiB
TypeScript
161 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import type { OCRSession, CreateSessionRequest, OCRModel } from '../types'
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
|
|
|
export default function SessionsTab({
|
|
sessions,
|
|
selectedSession,
|
|
setSelectedSession,
|
|
onSessionCreated,
|
|
onError,
|
|
}: {
|
|
sessions: OCRSession[]
|
|
selectedSession: string | null
|
|
setSelectedSession: (id: string | null) => void
|
|
onSessionCreated: () => void
|
|
onError: (msg: string) => void
|
|
}) {
|
|
const [newSession, setNewSession] = useState<CreateSessionRequest>({
|
|
name: '',
|
|
source_type: 'klausur',
|
|
description: '',
|
|
ocr_model: 'llama3.2-vision:11b',
|
|
})
|
|
|
|
const createSession = async () => {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api/v1/ocr-label/sessions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newSession),
|
|
})
|
|
if (res.ok) {
|
|
setNewSession({ name: '', source_type: 'klausur', description: '', ocr_model: 'llama3.2-vision:11b' })
|
|
onSessionCreated()
|
|
} else {
|
|
onError('Session erstellen fehlgeschlagen')
|
|
}
|
|
} catch (err) {
|
|
onError('Netzwerkfehler')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Create Session */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h3 className="text-lg font-semibold mb-4">Neue Session erstellen</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label>
|
|
<input
|
|
type="text"
|
|
value={newSession.name}
|
|
onChange={(e) => setNewSession(prev => ({ ...prev, name: e.target.value }))}
|
|
placeholder="z.B. Mathe Klausur Q1 2025"
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Typ</label>
|
|
<select
|
|
value={newSession.source_type}
|
|
onChange={(e) => setNewSession(prev => ({ ...prev, source_type: e.target.value as 'klausur' | 'handwriting_sample' | 'scan' }))}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
<option value="klausur">Klausur</option>
|
|
<option value="handwriting_sample">Handschriftprobe</option>
|
|
<option value="scan">Scan</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">OCR Modell</label>
|
|
<select
|
|
value={newSession.ocr_model}
|
|
onChange={(e) => setNewSession(prev => ({ ...prev, ocr_model: e.target.value as OCRModel }))}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
<option value="llama3.2-vision:11b">llama3.2-vision:11b - Vision LLM (Standard)</option>
|
|
<option value="trocr">TrOCR - Microsoft Transformer (schnell)</option>
|
|
<option value="paddleocr">PaddleOCR + LLM (4x schneller)</option>
|
|
<option value="donut">Donut - Document Understanding (strukturiert)</option>
|
|
</select>
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
{newSession.ocr_model === 'paddleocr' && 'PaddleOCR erkennt Text schnell, LLM strukturiert die Ergebnisse.'}
|
|
{newSession.ocr_model === 'donut' && 'Speziell fuer Dokumente mit Tabellen und Formularen.'}
|
|
{newSession.ocr_model === 'trocr' && 'Schnelles Transformer-Modell fuer gedruckten Text.'}
|
|
{newSession.ocr_model === 'llama3.2-vision:11b' && 'Beste Qualitaet bei Handschrift, aber langsamer.'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
|
<input
|
|
type="text"
|
|
value={newSession.description}
|
|
onChange={(e) => setNewSession(prev => ({ ...prev, description: e.target.value }))}
|
|
placeholder="Optional..."
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={createSession}
|
|
disabled={!newSession.name}
|
|
className="mt-4 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
|
>
|
|
Session erstellen
|
|
</button>
|
|
</div>
|
|
|
|
{/* Sessions List */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-slate-200">
|
|
<h3 className="text-lg font-semibold">Sessions ({sessions.length})</h3>
|
|
</div>
|
|
<div className="divide-y divide-slate-200">
|
|
{sessions.map((session) => (
|
|
<div
|
|
key={session.id}
|
|
className={`p-4 hover:bg-slate-50 cursor-pointer ${
|
|
selectedSession === session.id ? 'bg-primary-50 border-l-4 border-primary-500' : ''
|
|
}`}
|
|
onClick={() => setSelectedSession(session.id === selectedSession ? null : session.id)}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="font-medium">{session.name}</h4>
|
|
<p className="text-sm text-slate-500">
|
|
{session.source_type} | {session.ocr_model}
|
|
</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-sm font-medium">
|
|
{session.labeled_items}/{session.total_items} gelabelt
|
|
</p>
|
|
<div className="w-32 bg-slate-200 rounded-full h-2 mt-1">
|
|
<div
|
|
className="bg-primary-600 rounded-full h-2"
|
|
style={{
|
|
width: `${session.total_items > 0 ? (session.labeled_items / session.total_items) * 100 : 0}%`
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{session.description && (
|
|
<p className="text-sm text-slate-600 mt-2">{session.description}</p>
|
|
)}
|
|
</div>
|
|
))}
|
|
{sessions.length === 0 && (
|
|
<p className="p-4 text-slate-500 text-center">Keine Sessions vorhanden</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|