Files
breakpilot-lehrer/admin-lehrer/components/ocr-pipeline/StructureImageComparison.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

265 lines
9.6 KiB
TypeScript

'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import type { ExcludeRegion, StructureResult } from '@/app/(admin)/ai/ocr-kombi/types'
import {
KLAUSUR_API,
getDocLayoutColor,
imageToOverlayPct,
mouseToImageCoords,
} from './structure-detection-utils'
interface StructureImageComparisonProps {
sessionId: string
result: StructureResult
overlayTs: number
excludeRegions: ExcludeRegion[]
drawMode: boolean
onAddRegion: (region: ExcludeRegion) => void
onDeleteRegion: (index: number) => void
}
export function StructureImageComparison({
sessionId,
result,
overlayTs,
excludeRegions,
drawMode,
onAddRegion,
onDeleteRegion,
}: StructureImageComparisonProps) {
// Exclude region drawing state
const [drawing, setDrawing] = useState(false)
const [drawStart, setDrawStart] = useState<{ x: number; y: number } | null>(null)
const [drawCurrent, setDrawCurrent] = useState<{ x: number; y: number } | null>(null)
const containerRef = useRef<HTMLDivElement>(null)
const overlayContainerRef = useRef<HTMLDivElement>(null)
const [containerSize, setContainerSize] = useState({ w: 0, h: 0 })
const [overlayContainerSize, setOverlayContainerSize] = useState({ w: 0, h: 0 })
// Track container size for overlay positioning
useEffect(() => {
const el = containerRef.current
if (!el) return
const obs = new ResizeObserver((entries) => {
for (const entry of entries) {
setContainerSize({ w: entry.contentRect.width, h: entry.contentRect.height })
}
})
obs.observe(el)
return () => obs.disconnect()
}, [])
// Track overlay container size for PP-DocLayout region overlays
useEffect(() => {
const el = overlayContainerRef.current
if (!el) return
const obs = new ResizeObserver((entries) => {
for (const entry of entries) {
setOverlayContainerSize({ w: entry.contentRect.width, h: entry.contentRect.height })
}
})
obs.observe(el)
return () => obs.disconnect()
}, [])
// Mouse handlers for drawing exclude rectangles
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (!drawMode || !containerRef.current) return
const coords = mouseToImageCoords(e, containerRef.current, result.image_width, result.image_height)
if (coords) {
setDrawing(true)
setDrawStart(coords)
setDrawCurrent(coords)
}
}, [drawMode, result])
const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!drawing || !containerRef.current) return
const coords = mouseToImageCoords(e, containerRef.current, result.image_width, result.image_height)
if (coords) {
setDrawCurrent(coords)
}
}, [drawing, result])
const handleMouseUp = useCallback(() => {
if (!drawing || !drawStart || !drawCurrent) {
setDrawing(false)
return
}
const x = Math.min(drawStart.x, drawCurrent.x)
const y = Math.min(drawStart.y, drawCurrent.y)
const w = Math.abs(drawCurrent.x - drawStart.x)
const h = Math.abs(drawCurrent.y - drawStart.y)
// Minimum size to avoid accidental clicks
if (w > 10 && h > 10) {
const newRegion: ExcludeRegion = { x, y, w, h, label: `Bereich ${excludeRegions.length + 1}` }
onAddRegion(newRegion)
}
setDrawing(false)
setDrawStart(null)
setDrawCurrent(null)
}, [drawing, drawStart, drawCurrent, excludeRegions.length, onAddRegion])
const croppedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped`
const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/structure-overlay${overlayTs ? `?t=${overlayTs}` : ''}`
// Current drag rectangle in image coords
const dragRect = drawing && drawStart && drawCurrent
? {
x: Math.min(drawStart.x, drawCurrent.x),
y: Math.min(drawStart.y, drawCurrent.y),
w: Math.abs(drawCurrent.x - drawStart.x),
h: Math.abs(drawCurrent.y - drawStart.y),
}
: null
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: Original document with exclude region drawing */}
<div className="space-y-2">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Original {excludeRegions.length > 0 && `(${excludeRegions.length} Ausschlussbereich${excludeRegions.length !== 1 ? 'e' : ''})`}
</div>
<div
ref={containerRef}
className={`relative bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden ${
drawMode ? 'cursor-crosshair' : ''
}`}
style={{ aspectRatio: '210/297' }}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={() => {
if (drawing) {
handleMouseUp()
}
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={croppedUrl}
alt="Originaldokument"
className="w-full h-full object-contain pointer-events-none"
draggable={false}
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'
}}
/>
{/* Saved exclude regions overlay */}
{containerSize.w > 0 && excludeRegions.map((region, i) => {
const pos = imageToOverlayPct(region, containerSize.w, containerSize.h, result.image_width, result.image_height)
return (
<div
key={i}
className="absolute border-2 border-red-500 bg-red-500/20 group"
style={pos}
>
<div className="absolute -top-5 left-0 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<span className="text-[10px] bg-red-600 text-white px-1 rounded whitespace-nowrap">
{region.label || `Bereich ${i + 1}`}
</span>
<button
onClick={(e) => { e.stopPropagation(); onDeleteRegion(i) }}
className="w-4 h-4 bg-red-600 text-white rounded-full text-[10px] flex items-center justify-center hover:bg-red-700"
>
x
</button>
</div>
</div>
)
})}
{/* Current drag rectangle */}
{dragRect && containerSize.w > 0 && (() => {
const pos = imageToOverlayPct(dragRect, containerSize.w, containerSize.h, result.image_width, result.image_height)
return (
<div
className="absolute border-2 border-red-500 border-dashed bg-red-500/15"
style={pos}
/>
)
})()}
</div>
</div>
{/* Right: Structure overlay */}
<div className="space-y-2">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Erkannte Struktur
{result.detection_method && (
<span className="ml-2 text-[10px] font-normal normal-case">
({result.detection_method === 'ppdoclayout' ? 'PP-DocLayout' : 'OpenCV'})
</span>
)}
</div>
<div
ref={overlayContainerRef}
className="relative bg-gray-100 dark:bg-gray-800 rounded-lg overflow-hidden"
style={{ aspectRatio: '210/297' }}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={overlayUrl}
alt="Strukturerkennung"
className="w-full h-full object-contain"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none'
}}
/>
{/* PP-DocLayout region overlays with class colors and labels */}
{result.layout_regions && overlayContainerSize.w > 0 && result.layout_regions.map((region, i) => {
const pos = imageToOverlayPct(region, overlayContainerSize.w, overlayContainerSize.h, result.image_width, result.image_height)
const color = getDocLayoutColor(region.class_name)
return (
<div
key={`layout-${i}`}
className="absolute border-2 pointer-events-none"
style={{
...pos,
borderColor: color,
backgroundColor: `${color}18`,
}}
>
<span
className="absolute -top-4 left-0 px-1 py-px text-[9px] font-medium text-white rounded-sm whitespace-nowrap leading-tight"
style={{ backgroundColor: color }}
>
{region.class_name} {Math.round(region.confidence * 100)}%
</span>
</div>
)
})}
</div>
{/* PP-DocLayout legend */}
{result.layout_regions && result.layout_regions.length > 0 && (() => {
const usedClasses = [...new Set(result.layout_regions!.map((r) => r.class_name.toLowerCase()))]
return (
<div className="flex flex-wrap gap-x-3 gap-y-1 px-1">
{usedClasses.sort().map((cls) => (
<span key={cls} className="inline-flex items-center gap-1 text-[10px] text-gray-500 dark:text-gray-400">
<span
className="w-2.5 h-2.5 rounded-sm border"
style={{
backgroundColor: `${getDocLayoutColor(cls)}30`,
borderColor: getDocLayoutColor(cls),
}}
/>
{cls}
</span>
))}
</div>
)
})()}
</div>
</div>
)
}