'use client' /** * Custom hook encapsulating all state and API logic for the OCR Labeling page. */ import { useState, useEffect, useCallback } from 'react' import { API_BASE } from './constants' import type { OCRSession, OCRItem, OCRStats } from './types' export function useOcrLabeling() { 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) // Fetch sessions const fetchSessions = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/v1/ocr-label/sessions`) if (res.ok) { const data = await res.json() setSessions(data) } } catch (err) { console.error('Failed to fetch sessions:', err) } }, []) // Fetch queue 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 res = await fetch(url) if (res.ok) { const data = await res.json() setQueue(data) if (data.length > 0 && !currentItem) { setCurrentItem(data[0]) setCurrentIndex(0) setCorrectedText(data[0].ocr_text || '') setLabelStartTime(Date.now()) } } } catch (err) { console.error('Failed to fetch queue:', err) } }, [selectedSession, currentItem]) // Fetch stats 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 res = await fetch(url) if (res.ok) { const data = await res.json() setStats(data) } } catch (err) { console.error('Failed to fetch stats:', err) } }, [selectedSession]) // Initial data load useEffect(() => { const loadData = async () => { setLoading(true) await Promise.all([fetchSessions(), fetchQueue(), fetchStats()]) setLoading(false) } loadData() }, [fetchSessions, fetchQueue, fetchStats]) // Refresh queue when session changes useEffect(() => { setCurrentItem(null) setCurrentIndex(0) fetchQueue() fetchStats() }, [selectedSession, fetchQueue, fetchStats]) // Navigate to next item const goToNext = useCallback(() => { if (currentIndex < queue.length - 1) { const nextIndex = currentIndex + 1 setCurrentIndex(nextIndex) setCurrentItem(queue[nextIndex]) setCorrectedText(queue[nextIndex].ocr_text || '') setLabelStartTime(Date.now()) } else { fetchQueue() } }, [currentIndex, queue, fetchQueue]) // Navigate to previous item const goToPrev = useCallback(() => { if (currentIndex > 0) { const prevIndex = currentIndex - 1 setCurrentIndex(prevIndex) setCurrentItem(queue[prevIndex]) setCorrectedText(queue[prevIndex].ocr_text || '') setLabelStartTime(Date.now()) } }, [currentIndex, queue]) // Calculate label time const getLabelTime = useCallback((): number | undefined => { if (!labelStartTime) return undefined return Math.round((Date.now() - labelStartTime) / 1000) }, [labelStartTime]) // Select a queue item by index const selectQueueItem = useCallback((idx: number) => { if (idx >= 0 && idx < queue.length) { setCurrentIndex(idx) setCurrentItem(queue[idx]) setCorrectedText(queue[idx].ocr_text || '') setLabelStartTime(Date.now()) } }, [queue]) // Confirm item const confirmItem = useCallback(async () => { if (!currentItem) return try { const res = await fetch(`${API_BASE}/api/v1/ocr-label/confirm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: currentItem.id, label_time_seconds: getLabelTime(), }), }) if (res.ok) { setQueue(prev => prev.filter(item => item.id !== currentItem.id)) goToNext() fetchStats() } else { setError('Bestaetigung fehlgeschlagen') } } catch { setError('Netzwerkfehler') } }, [currentItem, getLabelTime, goToNext, fetchStats]) // Correct item const correctItem = useCallback(async () => { if (!currentItem || !correctedText.trim()) return try { const res = await fetch(`${API_BASE}/api/v1/ocr-label/correct`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: currentItem.id, ground_truth: correctedText.trim(), label_time_seconds: getLabelTime(), }), }) if (res.ok) { setQueue(prev => prev.filter(item => item.id !== currentItem.id)) goToNext() fetchStats() } else { setError('Korrektur fehlgeschlagen') } } catch { setError('Netzwerkfehler') } }, [currentItem, correctedText, getLabelTime, goToNext, fetchStats]) // Skip item const skipItem = useCallback(async () => { if (!currentItem) return try { const res = await fetch(`${API_BASE}/api/v1/ocr-label/skip`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item_id: currentItem.id }), }) if (res.ok) { setQueue(prev => prev.filter(item => item.id !== currentItem.id)) goToNext() fetchStats() } else { setError('Ueberspringen fehlgeschlagen') } } catch { setError('Netzwerkfehler') } }, [currentItem, goToNext, fetchStats]) // Keyboard shortcuts useEffect(() => { const handleKeyDown = (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', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [confirmItem, goToNext, goToPrev, skipItem]) return { // State sessions, selectedSession, setSelectedSession, queue, currentItem, currentIndex, stats, loading, error, setError, correctedText, setCorrectedText, // Actions fetchSessions, fetchQueue, fetchStats, goToNext, goToPrev, selectQueueItem, confirmItem, correctItem, skipItem, } }