'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(null) const overlayContainerRef = useRef(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 (
{/* Left: Original document with exclude region drawing */}
Original {excludeRegions.length > 0 && `(${excludeRegions.length} Ausschlussbereich${excludeRegions.length !== 1 ? 'e' : ''})`}
{ if (drawing) { handleMouseUp() } }} > {/* eslint-disable-next-line @next/next/no-img-element */} Originaldokument { (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 (
{region.label || `Bereich ${i + 1}`}
) })} {/* Current drag rectangle */} {dragRect && containerSize.w > 0 && (() => { const pos = imageToOverlayPct(dragRect, containerSize.w, containerSize.h, result.image_width, result.image_height) return (
) })()}
{/* Right: Structure overlay */}
Erkannte Struktur {result.detection_method && ( ({result.detection_method === 'ppdoclayout' ? 'PP-DocLayout' : 'OpenCV'}) )}
{/* eslint-disable-next-line @next/next/no-img-element */} Strukturerkennung { (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 (
{region.class_name} {Math.round(region.confidence * 100)}%
) })}
{/* 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 (
{usedClasses.sort().map((cls) => ( {cls} ))}
) })()}
) }