'use client' /** * OCR Labeling Admin Page * * Labeling interface for handwriting training data collection. * DSGVO-konform: Alle Verarbeitung lokal auf Mac Mini (Ollama). */ import { useState, useEffect, useCallback } from 'react' import AdminLayout from '@/components/admin/AdminLayout' import type { OCRSession, OCRItem, OCRStats } from './types' import { API_BASE, tabs } from './constants' import type { TabId } from './constants' import LabelingTab from './_components/LabelingTab' import SessionsTab from './_components/SessionsTab' import UploadTab from './_components/UploadTab' import StatsTab from './_components/StatsTab' import ExportTab from './_components/ExportTab' export default function OCRLabelingPage() { const [activeTab, setActiveTab] = useState('labeling') const [sessions, setSessions] = useState([]) const [selectedSession, setSelectedSession] = useState(null) const [queue, setQueue] = useState([]) const [currentItem, setCurrentItem] = useState(null) const [currentIndex, setCurrentIndex] = useState(0) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [correctedText, setCorrectedText] = useState('') const [labelStartTime, setLabelStartTime] = useState(null) const fetchSessions = useCallback(async () => { try { const r = await fetch(`${API_BASE}/api/v1/ocr-label/sessions`); if (r.ok) setSessions(await r.json()) } catch (e) { console.error('Failed to fetch sessions:', e) } }, []) const fetchQueue = useCallback(async () => { try { const url = selectedSession ? `${API_BASE}/api/v1/ocr-label/queue?session_id=${selectedSession}&limit=20` : `${API_BASE}/api/v1/ocr-label/queue?limit=20` const r = await fetch(url) if (r.ok) { const data = await r.json(); setQueue(data) if (data.length > 0 && !currentItem) { setCurrentItem(data[0]); setCurrentIndex(0); setCorrectedText(data[0].ocr_text || ''); setLabelStartTime(Date.now()) } } } catch (e) { console.error('Failed to fetch queue:', e) } }, [selectedSession, currentItem]) const fetchStats = useCallback(async () => { try { const url = selectedSession ? `${API_BASE}/api/v1/ocr-label/stats?session_id=${selectedSession}` : `${API_BASE}/api/v1/ocr-label/stats` const r = await fetch(url); if (r.ok) setStats(await r.json()) } catch (e) { console.error('Failed to fetch stats:', e) } }, [selectedSession]) useEffect(() => { setLoading(true); Promise.all([fetchSessions(), fetchQueue(), fetchStats()]).then(() => setLoading(false)) }, [fetchSessions, fetchQueue, fetchStats]) useEffect(() => { setCurrentItem(null); setCurrentIndex(0); fetchQueue(); fetchStats() }, [selectedSession, fetchQueue, fetchStats]) const getLabelTime = () => labelStartTime ? Math.round((Date.now() - labelStartTime) / 1000) : undefined const setItem = (item: OCRItem, idx: number) => { setCurrentIndex(idx); setCurrentItem(item); setCorrectedText(item.ocr_text || ''); setLabelStartTime(Date.now()) } const goToNext = () => { if (currentIndex < queue.length - 1) setItem(queue[currentIndex + 1], currentIndex + 1) else fetchQueue() } const goToPrev = () => { if (currentIndex > 0) setItem(queue[currentIndex - 1], currentIndex - 1) } const postAction = async (endpoint: string, body: object) => { const r = await fetch(`${API_BASE}/api/v1/ocr-label/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (r.ok) { setQueue(prev => prev.filter(i => i.id !== currentItem?.id)); goToNext(); fetchStats() } else setError(`${endpoint} fehlgeschlagen`) } const confirmItem = () => { if (currentItem) postAction('confirm', { item_id: currentItem.id, label_time_seconds: getLabelTime() }).catch(() => setError('Netzwerkfehler')) } const correctItem = () => { if (currentItem && correctedText.trim()) postAction('correct', { item_id: currentItem.id, ground_truth: correctedText.trim(), label_time_seconds: getLabelTime() }).catch(() => setError('Netzwerkfehler')) } const skipItem = () => { if (currentItem) postAction('skip', { item_id: currentItem.id }).catch(() => setError('Netzwerkfehler')) } useEffect(() => { const h = (e: KeyboardEvent) => { if (e.target instanceof HTMLTextAreaElement) return if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirmItem() } else if (e.key === 'ArrowRight') goToNext() else if (e.key === 'ArrowLeft') goToPrev() else if (e.key === 's' && !e.ctrlKey && !e.metaKey) skipItem() } window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h) }, [currentItem, correctedText]) return ( {error && (
{error}
)}
{loading ? (
) : ( <> {activeTab === 'labeling' && } {activeTab === 'sessions' && } {activeTab === 'upload' && { fetchQueue(); fetchStats() }} onError={setError} />} {activeTab === 'stats' && } {activeTab === 'export' && } )} ) }