Files
breakpilot-lehrer/admin-lehrer/components/ocr-pipeline/StepStructureDetection.tsx
Benjamin Admin b681ddb131 [split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
Phase 1 — Python (klausur-service): 5 monoliths → 36 files
- dsfa_corpus_ingestion.py (1,828 LOC → 5 files)
- cv_ocr_engines.py (2,102 LOC → 7 files)
- cv_layout.py (3,653 LOC → 10 files)
- vocab_worksheet_api.py (2,783 LOC → 8 files)
- grid_build_core.py (1,958 LOC → 6 files)

Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files
- staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3)
- policy_handlers.go (700 → 2), repository.go (684 → 2)
- search.go (592 → 2), ai_extraction_handlers.go (554 → 2)
- seed_data.go (591 → 2), grade_service.go (646 → 2)

Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files
- sdk/types.ts (2,108 → 16 domain files)
- ai/rag/page.tsx (2,686 → 14 files)
- 22 page.tsx files split into _components/ + _hooks/
- 11 component files split into sub-components
- 10 SDK data catalogs added to loc-exceptions
- Deleted dead backup index_original.ts (4,899 LOC)

All original public APIs preserved via re-export facades.
Zero new errors: Python imports verified, Go builds clean,
TypeScript tsc --noEmit shows only pre-existing errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:28:57 +02:00

237 lines
8.1 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import type { ExcludeRegion, StructureResult } from '@/app/(admin)/ai/ocr-kombi/types'
import { KLAUSUR_API, type DetectionMethod } from './structure-detection-utils'
import { StructureImageComparison } from './StructureImageComparison'
import { StructureResultDetails } from './StructureResultDetails'
import { ExcludeRegionsList } from './ExcludeRegionsList'
interface StepStructureDetectionProps {
sessionId: string | null
onNext: () => void
}
export function StepStructureDetection({ sessionId, onNext }: StepStructureDetectionProps) {
const [result, setResult] = useState<StructureResult | null>(null)
const [detecting, setDetecting] = useState(false)
const [error, setError] = useState<string | null>(null)
const [hasRun, setHasRun] = useState(false)
const [overlayTs, setOverlayTs] = useState(0)
const [detectionMethod, setDetectionMethod] = useState<DetectionMethod>('auto')
// Exclude region state
const [excludeRegions, setExcludeRegions] = useState<ExcludeRegion[]>([])
const [saving, setSaving] = useState(false)
const [drawMode, setDrawMode] = useState(false)
// Auto-trigger detection on mount
useEffect(() => {
if (!sessionId || hasRun) return
setHasRun(true)
const runDetection = async () => {
setDetecting(true)
setError(null)
try {
const params = detectionMethod !== 'auto' ? `?method=${detectionMethod}` : ''
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/detect-structure${params}`, {
method: 'POST',
})
if (!res.ok) {
throw new Error('Strukturerkennung fehlgeschlagen')
}
const data = await res.json()
setResult(data)
setExcludeRegions(data.exclude_regions || [])
setOverlayTs(Date.now())
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setDetecting(false)
}
}
runDetection()
}, [sessionId, hasRun])
const handleRerun = async () => {
if (!sessionId) return
setDetecting(true)
setError(null)
try {
const params = detectionMethod !== 'auto' ? `?method=${detectionMethod}` : ''
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/detect-structure${params}`, {
method: 'POST',
})
if (!res.ok) throw new Error('Erneute Erkennung fehlgeschlagen')
const data = await res.json()
setResult(data)
setExcludeRegions(data.exclude_regions || [])
setOverlayTs(Date.now())
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setDetecting(false)
}
}
// Save exclude regions to backend
const saveExcludeRegions = useCallback(async (regions: ExcludeRegion[]) => {
if (!sessionId) return
setSaving(true)
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/exclude-regions`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ regions }),
})
if (!res.ok) throw new Error('Speichern fehlgeschlagen')
} catch (e) {
setError(e instanceof Error ? e.message : 'Speichern fehlgeschlagen')
} finally {
setSaving(false)
}
}, [sessionId])
const handleAddRegion = useCallback((region: ExcludeRegion) => {
const updated = [...excludeRegions, region]
setExcludeRegions(updated)
saveExcludeRegions(updated)
}, [excludeRegions, saveExcludeRegions])
const handleDeleteRegion = useCallback(async (index: number) => {
if (!sessionId) return
setSaving(true)
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/exclude-regions/${index}`, {
method: 'DELETE',
})
if (!res.ok) throw new Error('Loeschen fehlgeschlagen')
const updated = excludeRegions.filter((_, i) => i !== index)
setExcludeRegions(updated)
} catch (e) {
setError(e instanceof Error ? e.message : 'Loeschen fehlgeschlagen')
} finally {
setSaving(false)
}
}, [sessionId, excludeRegions])
if (!sessionId) {
return <div className="text-sm text-gray-400">Keine Session ausgewaehlt.</div>
}
return (
<div className="space-y-4">
{/* Loading indicator */}
{detecting && (
<div className="flex items-center gap-2 text-teal-600 dark:text-teal-400 text-sm">
<div className="animate-spin w-4 h-4 border-2 border-teal-500 border-t-transparent rounded-full" />
Dokumentstruktur wird analysiert...
</div>
)}
{/* Detection method toggle */}
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-gray-500 dark:text-gray-400">Methode:</span>
{(['auto', 'opencv', 'ppdoclayout'] as DetectionMethod[]).map((method) => (
<button
key={method}
onClick={() => setDetectionMethod(method)}
className={`px-3 py-1.5 text-xs rounded-md font-medium transition-colors ${
detectionMethod === method
? 'bg-teal-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{method === 'auto' ? 'Auto' : method === 'opencv' ? 'OpenCV' : 'PP-DocLayout'}
</button>
))}
<span className="text-[10px] text-gray-400 dark:text-gray-500 ml-1">
{detectionMethod === 'auto'
? 'PP-DocLayout wenn verfuegbar, sonst OpenCV'
: detectionMethod === 'ppdoclayout'
? 'ONNX-basierte Layouterkennung mit Klassifikation'
: 'Klassische OpenCV-Konturerkennung'}
</span>
</div>
{/* Draw mode toggle */}
{result && (
<div className="flex items-center gap-3">
<button
onClick={() => setDrawMode(!drawMode)}
className={`px-4 py-2 text-sm rounded-lg font-medium transition-colors ${
drawMode
? 'bg-red-600 text-white hover:bg-red-700'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{drawMode ? 'Zeichnen beenden' : 'Ausschlussbereich zeichnen'}
</button>
{drawMode && (
<span className="text-xs text-red-600 dark:text-red-400">
Rechteck auf dem Bild zeichnen um Bereiche von der OCR-Erkennung auszuschliessen
</span>
)}
{saving && (
<span className="text-xs text-gray-400">Speichern...</span>
)}
</div>
)}
{/* Two-column image comparison */}
{result && (
<StructureImageComparison
sessionId={sessionId}
result={result}
overlayTs={overlayTs}
excludeRegions={excludeRegions}
drawMode={drawMode}
onAddRegion={handleAddRegion}
onDeleteRegion={handleDeleteRegion}
/>
)}
{/* Exclude regions list */}
<ExcludeRegionsList
regions={excludeRegions}
onDeleteRegion={handleDeleteRegion}
/>
{/* Result info */}
{result && (
<StructureResultDetails result={result} excludeRegions={excludeRegions} />
)}
{/* Action buttons */}
{result && (
<div className="flex justify-between">
<button
onClick={handleRerun}
disabled={detecting}
className="px-4 py-2 text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors disabled:opacity-50"
>
Erneut erkennen
</button>
<button
onClick={onNext}
className="px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 font-medium transition-colors"
>
Weiter &rarr;
</button>
</div>
)}
{error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
{error}
</div>
)}
</div>
)
}