klausur-service (11 files): - cv_gutter_repair, ocr_pipeline_regression, upload_api - ocr_pipeline_sessions, smart_spell, nru_worksheet_generator - ocr_pipeline_overlays, mail/aggregator, zeugnis_api - cv_syllable_detect, self_rag backend-lehrer (17 files): - classroom_engine/suggestions, generators/quiz_generator - worksheets_api, llm_gateway/comparison, state_engine_api - classroom/models (→ 4 submodules), services/file_processor - alerts_agent/api/wizard+digests+routes, content_generators/pdf - classroom/routes/sessions, llm_gateway/inference - classroom_engine/analytics, auth/keycloak_auth - alerts_agent/processing/rule_engine, ai_processor/print_versions agent-core (5 files): - brain/memory_store, brain/knowledge_graph, brain/context_manager - orchestrator/supervisor, sessions/session_manager admin-lehrer (5 components): - GridOverlay, StepGridReview, DevOpsPipelineSidebar - DataFlowDiagram, sbom/wizard/page website (2 files): - DependencyMap, lehrer/abitur-archiv Other: nibis_ingestion, grid_detection_service, export-doclayout-onnx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
181 lines
7.2 KiB
TypeScript
181 lines
7.2 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* StepGridReview Stats Bar & OCR Quality Controls
|
|
*
|
|
* Extracted from StepGridReview.tsx to stay under 500 LOC.
|
|
*/
|
|
|
|
import type { GridZone } from '@/components/grid-editor/types'
|
|
|
|
interface GridSummary {
|
|
total_zones: number
|
|
total_columns: number
|
|
total_rows: number
|
|
total_cells: number
|
|
}
|
|
|
|
interface DictionaryDetection {
|
|
is_dictionary: boolean
|
|
confidence: number
|
|
}
|
|
|
|
interface PageNumber {
|
|
text?: string
|
|
number?: number | null
|
|
}
|
|
|
|
interface ReviewStatsBarProps {
|
|
summary: GridSummary
|
|
dictionaryDetection?: DictionaryDetection | null
|
|
pageNumber?: PageNumber | null
|
|
lowConfCount: number
|
|
acceptedCount: number
|
|
totalRows: number
|
|
ocrEnhance: boolean
|
|
ocrMaxCols: number
|
|
ocrMinConf: number
|
|
visionFusion: boolean
|
|
documentCategory: string
|
|
durationSeconds: number
|
|
showImage: boolean
|
|
onOcrEnhanceChange: (v: boolean) => void
|
|
onOcrMaxColsChange: (v: number) => void
|
|
onOcrMinConfChange: (v: number) => void
|
|
onVisionFusionChange: (v: boolean) => void
|
|
onDocumentCategoryChange: (v: string) => void
|
|
onAcceptAll: () => void
|
|
onAutoCorrect: () => number
|
|
onToggleImage: () => void
|
|
}
|
|
|
|
export function ReviewStatsBar({
|
|
summary,
|
|
dictionaryDetection,
|
|
pageNumber,
|
|
lowConfCount,
|
|
acceptedCount,
|
|
totalRows,
|
|
ocrEnhance,
|
|
ocrMaxCols,
|
|
ocrMinConf,
|
|
visionFusion,
|
|
documentCategory,
|
|
durationSeconds,
|
|
showImage,
|
|
onOcrEnhanceChange,
|
|
onOcrMaxColsChange,
|
|
onOcrMinConfChange,
|
|
onVisionFusionChange,
|
|
onDocumentCategoryChange,
|
|
onAcceptAll,
|
|
onAutoCorrect,
|
|
onToggleImage,
|
|
}: ReviewStatsBarProps) {
|
|
return (
|
|
<div className="flex items-center gap-4 text-xs flex-wrap">
|
|
<span className="text-gray-500 dark:text-gray-400">
|
|
{summary.total_zones} Zone(n), {summary.total_columns} Spalten,{' '}
|
|
{summary.total_rows} Zeilen, {summary.total_cells} Zellen
|
|
</span>
|
|
{dictionaryDetection?.is_dictionary && (
|
|
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
|
|
Woerterbuch ({Math.round(dictionaryDetection.confidence * 100)}%)
|
|
</span>
|
|
)}
|
|
{pageNumber?.text && (
|
|
<span className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600">
|
|
S. {pageNumber.number ?? pageNumber.text}
|
|
</span>
|
|
)}
|
|
{lowConfCount > 0 && (
|
|
<span className="px-2 py-0.5 rounded-full bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
|
|
{lowConfCount} niedrige Konfidenz
|
|
</span>
|
|
)}
|
|
<span className="text-gray-400 dark:text-gray-500">
|
|
{acceptedCount}/{totalRows} Zeilen akzeptiert
|
|
</span>
|
|
{acceptedCount < totalRows && (
|
|
<button
|
|
onClick={onAcceptAll}
|
|
className="text-teal-600 dark:text-teal-400 hover:text-teal-700 dark:hover:text-teal-300"
|
|
>
|
|
Alle akzeptieren
|
|
</button>
|
|
)}
|
|
|
|
{/* OCR Quality Steps */}
|
|
<span className="text-gray-400 dark:text-gray-500">|</span>
|
|
<label className="flex items-center gap-1 cursor-pointer" title="Step 3: CLAHE + Bilateral-Filter Enhancement">
|
|
<input type="checkbox" checked={ocrEnhance} onChange={(e) => onOcrEnhanceChange(e.target.checked)} className="rounded w-3 h-3" />
|
|
<span className="text-gray-500 dark:text-gray-400">CLAHE</span>
|
|
</label>
|
|
<label className="flex items-center gap-1" title="Step 2: Max Spaltenanzahl (0=unbegrenzt)">
|
|
<span className="text-gray-500 dark:text-gray-400">MaxCol:</span>
|
|
<select value={ocrMaxCols} onChange={(e) => onOcrMaxColsChange(Number(e.target.value))} className="px-1 py-0.5 text-xs rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
<option value={0}>off</option>
|
|
<option value={2}>2</option>
|
|
<option value={3}>3</option>
|
|
<option value={4}>4</option>
|
|
<option value={5}>5</option>
|
|
</select>
|
|
</label>
|
|
<label className="flex items-center gap-1" title="Step 1: Min OCR Confidence (0=auto)">
|
|
<span className="text-gray-500 dark:text-gray-400">MinConf:</span>
|
|
<select value={ocrMinConf} onChange={(e) => onOcrMinConfChange(Number(e.target.value))} className="px-1 py-0.5 text-xs rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
<option value={0}>auto</option>
|
|
<option value={20}>20</option>
|
|
<option value={30}>30</option>
|
|
<option value={40}>40</option>
|
|
<option value={50}>50</option>
|
|
<option value={60}>60</option>
|
|
</select>
|
|
</label>
|
|
|
|
<span className="text-gray-400 dark:text-gray-500">|</span>
|
|
<label className="flex items-center gap-1 cursor-pointer" title="Step 4: Vision-LLM Fusion">
|
|
<input type="checkbox" checked={visionFusion} onChange={(e) => onVisionFusionChange(e.target.checked)} className="rounded w-3 h-3 accent-orange-500" />
|
|
<span className={`${visionFusion ? 'text-orange-500 dark:text-orange-400 font-medium' : 'text-gray-500 dark:text-gray-400'}`}>Vision-LLM</span>
|
|
</label>
|
|
<label className="flex items-center gap-1" title="Dokumenttyp fuer Vision-LLM Prompt">
|
|
<span className="text-gray-500 dark:text-gray-400">Typ:</span>
|
|
<select value={documentCategory} onChange={(e) => onDocumentCategoryChange(e.target.value)} className="px-1 py-0.5 text-xs rounded border border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300">
|
|
<option value="vokabelseite">Vokabelseite</option>
|
|
<option value="woerterbuch">Woerterbuch</option>
|
|
<option value="arbeitsblatt">Arbeitsblatt</option>
|
|
<option value="buchseite">Buchseite</option>
|
|
<option value="sonstiges">Sonstiges</option>
|
|
</select>
|
|
</label>
|
|
|
|
<div className="ml-auto flex items-center gap-2">
|
|
<button
|
|
onClick={() => {
|
|
const n = onAutoCorrect()
|
|
if (n === 0) alert('Keine Muster-Korrekturen gefunden.')
|
|
else alert(`${n} Zelle(n) korrigiert (Muster-Vervollstaendigung).`)
|
|
}}
|
|
className="px-2.5 py-1 rounded text-xs border border-purple-200 dark:border-purple-700 bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/40 transition-colors"
|
|
title="Erkennt Muster wie p.70, p.71 und vervollstaendigt partielle Eintraege wie .65 zu p.65"
|
|
>
|
|
Auto-Korrektur
|
|
</button>
|
|
<button
|
|
onClick={onToggleImage}
|
|
className={`px-2.5 py-1 rounded text-xs border transition-colors ${
|
|
showImage
|
|
? 'bg-teal-50 dark:bg-teal-900/30 border-teal-200 dark:border-teal-700 text-teal-700 dark:text-teal-300'
|
|
: 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
{showImage ? 'Bild ausblenden' : 'Bild einblenden'}
|
|
</button>
|
|
<span className="text-gray-400 dark:text-gray-500">
|
|
{durationSeconds.toFixed(1)}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|