[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>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user