Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
7.6 KiB
TypeScript
198 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Labeling tab: image viewer, OCR text, correction input, and queue preview.
|
|
*/
|
|
|
|
import { API_BASE } from '../constants'
|
|
import type { OCRItem } from '../types'
|
|
|
|
interface LabelingTabProps {
|
|
queue: OCRItem[]
|
|
currentItem: OCRItem | null
|
|
currentIndex: number
|
|
correctedText: string
|
|
setCorrectedText: (text: string) => void
|
|
goToNext: () => void
|
|
goToPrev: () => void
|
|
selectQueueItem: (idx: number) => void
|
|
confirmItem: () => void
|
|
correctItem: () => void
|
|
skipItem: () => void
|
|
}
|
|
|
|
export function LabelingTab({
|
|
queue,
|
|
currentItem,
|
|
currentIndex,
|
|
correctedText,
|
|
setCorrectedText,
|
|
goToNext,
|
|
goToPrev,
|
|
selectQueueItem,
|
|
confirmItem,
|
|
correctItem,
|
|
skipItem,
|
|
}: LabelingTabProps) {
|
|
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={goToPrev}
|
|
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={goToNext}
|
|
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={confirmItem}
|
|
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={correctItem}
|
|
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={skipItem}
|
|
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={() => selectQueueItem(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>
|
|
)
|
|
}
|