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>
193 lines
7.6 KiB
TypeScript
193 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import type { OCRItem } from '../types'
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
|
|
|
export default function LabelingTab({
|
|
queue,
|
|
currentItem,
|
|
currentIndex,
|
|
correctedText,
|
|
setCorrectedText,
|
|
onGoToPrev,
|
|
onGoToNext,
|
|
onConfirm,
|
|
onCorrect,
|
|
onSkip,
|
|
onSelectItem,
|
|
}: {
|
|
queue: OCRItem[]
|
|
currentItem: OCRItem | null
|
|
currentIndex: number
|
|
correctedText: string
|
|
setCorrectedText: (text: string) => void
|
|
onGoToPrev: () => void
|
|
onGoToNext: () => void
|
|
onConfirm: () => void
|
|
onCorrect: () => void
|
|
onSkip: () => void
|
|
onSelectItem: (item: OCRItem, index: number) => void
|
|
}) {
|
|
return (
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Left: Image Viewer */}
|
|
<div className="lg:col-span-2 bg-white rounded-lg shadow p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold">Bild</h3>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={onGoToPrev}
|
|
disabled={currentIndex === 0}
|
|
className="p-2 rounded hover:bg-slate-100 disabled:opacity-50"
|
|
title="Zurueck (Pfeiltaste links)"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<span className="text-sm text-slate-600">
|
|
{currentIndex + 1} / {queue.length}
|
|
</span>
|
|
<button
|
|
onClick={onGoToNext}
|
|
disabled={currentIndex >= queue.length - 1}
|
|
className="p-2 rounded hover:bg-slate-100 disabled:opacity-50"
|
|
title="Weiter (Pfeiltaste rechts)"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{currentItem ? (
|
|
<div className="relative bg-slate-100 rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
|
|
<img
|
|
src={currentItem.image_url || `${API_BASE}${currentItem.image_path}`}
|
|
alt="OCR Bild"
|
|
className="w-full h-auto max-h-[600px] object-contain"
|
|
onError={(e) => {
|
|
const target = e.target as HTMLImageElement
|
|
target.style.display = 'none'
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center h-64 bg-slate-100 rounded-lg">
|
|
<p className="text-slate-500">Keine Bilder in der Warteschlange</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right: OCR Text & Actions */}
|
|
<div className="bg-white rounded-lg shadow p-4">
|
|
<div className="space-y-4">
|
|
{/* OCR Result */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-lg font-semibold">OCR-Ergebnis</h3>
|
|
{currentItem?.ocr_confidence && (
|
|
<span className={`text-sm px-2 py-1 rounded ${
|
|
currentItem.ocr_confidence > 0.8
|
|
? 'bg-green-100 text-green-800'
|
|
: currentItem.ocr_confidence > 0.5
|
|
? 'bg-yellow-100 text-yellow-800'
|
|
: 'bg-red-100 text-red-800'
|
|
}`}>
|
|
{Math.round(currentItem.ocr_confidence * 100)}% Konfidenz
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="bg-slate-50 p-3 rounded-lg min-h-[100px] text-sm">
|
|
{currentItem?.ocr_text || <span className="text-slate-400">Kein OCR-Text</span>}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Correction Input */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-2">Korrektur</h3>
|
|
<textarea
|
|
value={correctedText}
|
|
onChange={(e) => setCorrectedText(e.target.value)}
|
|
placeholder="Korrigierter Text..."
|
|
className="w-full h-32 p-3 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex flex-col gap-2">
|
|
<button
|
|
onClick={onConfirm}
|
|
disabled={!currentItem}
|
|
className="w-full px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Korrekt (Enter)
|
|
</button>
|
|
<button
|
|
onClick={onCorrect}
|
|
disabled={!currentItem || !correctedText.trim() || correctedText === currentItem?.ocr_text}
|
|
className="w-full px-4 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
</svg>
|
|
Korrektur speichern
|
|
</button>
|
|
<button
|
|
onClick={onSkip}
|
|
disabled={!currentItem}
|
|
className="w-full px-4 py-2 bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
|
|
</svg>
|
|
Ueberspringen (S)
|
|
</button>
|
|
</div>
|
|
|
|
{/* Keyboard Shortcuts */}
|
|
<div className="text-xs text-slate-500 mt-4">
|
|
<p className="font-medium mb-1">Tastaturkuerzel:</p>
|
|
<p>Enter = Bestaetigen | S = Ueberspringen</p>
|
|
<p>Pfeiltasten = Navigation</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom: Queue Preview */}
|
|
<div className="lg:col-span-3 bg-white rounded-lg shadow p-4">
|
|
<h3 className="text-lg font-semibold mb-4">Warteschlange ({queue.length} Items)</h3>
|
|
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
{queue.slice(0, 10).map((item, idx) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => onSelectItem(item, idx)}
|
|
className={`flex-shrink-0 w-24 h-24 rounded-lg overflow-hidden border-2 ${
|
|
idx === currentIndex
|
|
? 'border-primary-500'
|
|
: 'border-transparent hover:border-slate-300'
|
|
}`}
|
|
>
|
|
<img
|
|
src={item.image_url || `${API_BASE}${item.image_path}`}
|
|
alt=""
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</button>
|
|
))}
|
|
{queue.length > 10 && (
|
|
<div className="flex-shrink-0 w-24 h-24 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
|
|
+{queue.length - 10} mehr
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|