'use client' /** * Ground-Truth Queue & Progress * * Overview page showing all sessions with their GT status. * Clicking a session opens it in the Kombi Pipeline (/ai/ocr-overlay) * where the actual review (split-view, inline edit, GT marking) happens. */ import { useState, useEffect, useCallback } from 'react' import { useRouter } from 'next/navigation' import { PagePurpose } from '@/components/common/PagePurpose' const KLAUSUR_API = '/klausur-api' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface Session { id: string name: string filename: string status: string created_at: string document_category: string | null has_ground_truth: boolean } interface GTSession { session_id: string name: string filename: string document_category: string | null pipeline: string | null saved_at: string | null summary: { total_zones: number total_columns: number total_rows: number total_cells: number } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function GroundTruthQueuePage() { const router = useRouter() const [allSessions, setAllSessions] = useState([]) const [gtSessions, setGtSessions] = useState([]) const [filter, setFilter] = useState<'all' | 'unreviewed' | 'reviewed'>('all') const [loading, setLoading] = useState(true) const [selectedSessions, setSelectedSessions] = useState>(new Set()) const [marking, setMarking] = useState(false) const [markResult, setMarkResult] = useState(null) // Load sessions + GT sessions const loadData = useCallback(async () => { setLoading(true) try { const [sessRes, gtRes] = await Promise.all([ fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions?limit=200`), fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/ground-truth-sessions`), ]) if (sessRes.ok) { const data = await sessRes.json() const gtSet = new Set() if (gtRes.ok) { const gtData = await gtRes.json() const gts: GTSession[] = gtData.sessions || [] setGtSessions(gts) for (const g of gts) gtSet.add(g.session_id) } const sessions: Session[] = (data.sessions || []) .filter((s: any) => !s.parent_session_id) .map((s: any) => ({ id: s.id, name: s.name || '', filename: s.filename || '', status: s.status || 'active', created_at: s.created_at || '', document_category: s.document_category || null, has_ground_truth: gtSet.has(s.id), })) setAllSessions(sessions) } } catch (e) { console.error('Failed to load data:', e) } finally { setLoading(false) } }, []) useEffect(() => { loadData() }, [loadData]) // Filtered sessions const filteredSessions = allSessions.filter((s) => { if (filter === 'unreviewed') return !s.has_ground_truth if (filter === 'reviewed') return s.has_ground_truth return true }) const reviewedCount = allSessions.filter((s) => s.has_ground_truth).length const totalCount = allSessions.length const pct = totalCount > 0 ? Math.round((reviewedCount / totalCount) * 100) : 0 // Open session in Kombi pipeline const openInPipeline = (sessionId: string) => { router.push(`/ai/ocr-overlay?session=${sessionId}&mode=kombi`) } // Batch mark as GT const batchMark = async () => { setMarking(true) let success = 0 for (const sid of selectedSessions) { try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}/mark-ground-truth?pipeline=kombi`, { method: 'POST' }, ) if (res.ok) success++ } catch { /* skip */ } } setSelectedSessions(new Set()) setMarking(false) setMarkResult(`${success} Sessions als Ground Truth markiert`) setTimeout(() => setMarkResult(null), 3000) loadData() } const toggleSelect = (id: string) => { setSelectedSessions((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const selectAll = () => { if (selectedSessions.size === filteredSessions.length) { setSelectedSessions(new Set()) } else { setSelectedSessions(new Set(filteredSessions.map((s) => s.id))) } } return (
{/* Progress Bar */}

Ground Truth Fortschritt

{reviewedCount} von {totalCount} markiert ({pct}%)
{reviewedCount} Ground Truth {totalCount - reviewedCount} offen {gtSessions.reduce((sum, g) => sum + g.summary.total_cells, 0)}{' '} Referenz-Zellen gesamt
{/* Filter + Actions */}
{(['all', 'unreviewed', 'reviewed'] as const).map((f) => ( ))}
{selectedSessions.size > 0 && ( )}
{/* Toast */} {markResult && (
{markResult}
)} {/* Session List */} {loading ? (
Lade Sessions...
) : filteredSessions.length === 0 ? (

Keine Sessions in dieser Ansicht

) : (
{filteredSessions.map((s) => { const gt = gtSessions.find((g) => g.session_id === s.id) return ( ) })}
0 } onChange={selectAll} className="rounded border-slate-300" /> Status Session Kategorie Erstellt Aktion
toggleSelect(s.id)} className="rounded border-slate-300" /> {s.has_ground_truth ? ( GT ) : ( Offen )}
{/* eslint-disable-next-line @next/next/no-img-element */} { ;(e.target as HTMLImageElement).style.display = 'none' }} />
{s.name || s.filename || s.id.slice(0, 8)}
{gt && (
{gt.summary.total_cells} Zellen,{' '} {gt.summary.total_zones} Zonen
)}
{s.document_category ? ( {s.document_category} ) : ( )} {new Date(s.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', })}
)}
) }