Add GT button to OCR overlay, prominent category picker, track pipeline
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m51s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 18s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m51s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 18s
- Ground Truth button on last step of Pipeline/Kombi modes in ocr-overlay - Prominent category picker in active session info bar (pulses when unset) - GT badge shown when session has ground truth reference - Backend: auto-detect pipeline from ocr_engine, store in GT snapshot - Pipeline info shown in GT session list and regression reports - Also pass pipeline param from ocr-pipeline StepGroundTruth Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,10 @@ export default function OcrOverlayPage() {
|
||||
const [editNameValue, setEditNameValue] = useState('')
|
||||
const [editingCategory, setEditingCategory] = useState<string | null>(null)
|
||||
const [activeCategory, setActiveCategory] = useState<DocumentCategory | undefined>(undefined)
|
||||
const [editingActiveCategory, setEditingActiveCategory] = useState(false)
|
||||
const [isGroundTruth, setIsGroundTruth] = useState(false)
|
||||
const [gtSaving, setGtSaving] = useState(false)
|
||||
const [gtMessage, setGtMessage] = useState('')
|
||||
const [steps, setSteps] = useState<PipelineStep[]>(
|
||||
OVERLAY_PIPELINE_STEPS.map((s, i) => ({
|
||||
...s,
|
||||
@@ -64,6 +68,8 @@ export default function OcrOverlayPage() {
|
||||
setSessionId(sid)
|
||||
setSessionName(data.name || data.filename || '')
|
||||
setActiveCategory(data.document_category || undefined)
|
||||
setIsGroundTruth(!!data.ground_truth?.build_grid_reference)
|
||||
setGtMessage('')
|
||||
|
||||
// Check if this session was processed with paddle_direct, kombi, or rapid_kombi
|
||||
const ocrEngine = data.word_result?.ocr_engine
|
||||
@@ -239,6 +245,33 @@ export default function OcrOverlayPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sessionId, goToStep])
|
||||
|
||||
const handleMarkGroundTruth = async () => {
|
||||
if (!sessionId) return
|
||||
setGtSaving(true)
|
||||
setGtMessage('')
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=${mode}`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
if (!resp.ok) {
|
||||
const body = await resp.text().catch(() => '')
|
||||
throw new Error(`Ground Truth fehlgeschlagen (${resp.status}): ${body}`)
|
||||
}
|
||||
const data = await resp.json()
|
||||
setIsGroundTruth(true)
|
||||
setGtMessage(`Ground Truth gespeichert (${data.cells_saved} Zellen)`)
|
||||
setTimeout(() => setGtMessage(''), 5000)
|
||||
} catch (e) {
|
||||
setGtMessage(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setGtSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isLastStep = currentStep === steps.length - 1
|
||||
const showGtButton = isLastStep && sessionId != null
|
||||
|
||||
const renderStep = () => {
|
||||
if (mode === 'paddle-direct' || mode === 'kombi') {
|
||||
switch (currentStep) {
|
||||
@@ -472,14 +505,48 @@ export default function OcrOverlayPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Active session info */}
|
||||
{/* Active session info + category picker */}
|
||||
{sessionId && sessionName && (
|
||||
<div className="flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="relative flex items-center gap-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span>Aktive Session: <span className="font-medium text-gray-700 dark:text-gray-300">{sessionName}</span></span>
|
||||
{activeCategory && (() => {
|
||||
const cat = DOCUMENT_CATEGORIES.find(c => c.value === activeCategory)
|
||||
return cat ? <span className="text-xs px-2 py-0.5 rounded-full bg-teal-50 dark:bg-teal-900/30 border border-teal-200 dark:border-teal-700 text-teal-700 dark:text-teal-300">{cat.icon} {cat.label}</span> : null
|
||||
})()}
|
||||
<button
|
||||
onClick={() => setEditingActiveCategory(!editingActiveCategory)}
|
||||
className={`text-xs px-2.5 py-1 rounded-full border transition-colors ${
|
||||
activeCategory
|
||||
? 'bg-teal-50 dark:bg-teal-900/30 border-teal-200 dark:border-teal-700 text-teal-700 dark:text-teal-300 hover:bg-teal-100 dark:hover:bg-teal-900/50'
|
||||
: 'bg-amber-50 dark:bg-amber-900/20 border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/40 animate-pulse'
|
||||
}`}
|
||||
>
|
||||
{activeCategory ? (() => {
|
||||
const cat = DOCUMENT_CATEGORIES.find(c => c.value === activeCategory)
|
||||
return cat ? `${cat.icon} ${cat.label}` : activeCategory
|
||||
})() : 'Kategorie setzen'}
|
||||
</button>
|
||||
{isGroundTruth && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-amber-50 dark:bg-amber-900/20 border border-amber-300 dark:border-amber-700 text-amber-700 dark:text-amber-300">
|
||||
GT
|
||||
</span>
|
||||
)}
|
||||
{editingActiveCategory && (
|
||||
<div className="absolute left-0 top-full mt-1 z-20 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg p-2 grid grid-cols-2 gap-1 w-64">
|
||||
{DOCUMENT_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat.value}
|
||||
onClick={() => {
|
||||
updateCategory(sessionId, cat.value)
|
||||
setEditingActiveCategory(false)
|
||||
}}
|
||||
className={`text-xs px-2 py-1.5 rounded-md text-left transition-colors ${
|
||||
activeCategory === cat.value
|
||||
? 'bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300'
|
||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{cat.icon} {cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -543,6 +610,26 @@ export default function OcrOverlayPage() {
|
||||
/>
|
||||
|
||||
<div className="min-h-[400px]">{renderStep()}</div>
|
||||
|
||||
{/* Ground Truth button bar — visible on last step */}
|
||||
{showGtButton && (
|
||||
<div className="sticky bottom-0 bg-white dark:bg-gray-900 border-t dark:border-gray-700 py-3 px-4 -mx-1 flex items-center justify-between rounded-b-xl">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{gtMessage && (
|
||||
<span className={gtMessage.includes('fehlgeschlagen') ? 'text-red-500' : 'text-amber-600 dark:text-amber-400'}>
|
||||
{gtMessage}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleMarkGroundTruth}
|
||||
disabled={gtSaving}
|
||||
className="px-4 py-2 text-sm bg-amber-600 text-white rounded hover:bg-amber-700 disabled:opacity-50"
|
||||
>
|
||||
{gtSaving ? 'Speichere...' : isGroundTruth ? 'Ground Truth aktualisieren' : 'Als Ground Truth markieren'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ export function StepGroundTruth({ sessionId, onNext }: StepGroundTruthProps) {
|
||||
setGtMessage('')
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth`,
|
||||
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=ocr-pipeline`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
if (!resp.ok) {
|
||||
|
||||
Reference in New Issue
Block a user