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>
237 lines
8.1 KiB
TypeScript
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 →
|
|
</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>
|
|
)
|
|
}
|