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>
146 lines
4.9 KiB
TypeScript
146 lines
4.9 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useRef } from 'react'
|
|
import type { StructureBox, StructureGraphic } from '@/app/(admin)/ai/ocr-kombi/types'
|
|
import type { EditableCell } from './StepReconstructionTypes'
|
|
import { colTypeColor } from './StepReconstructionTypes'
|
|
import { StructureLayer } from './StructureLayer'
|
|
|
|
interface ReconstructionSimpleViewProps {
|
|
cells: EditableCell[]
|
|
dewarpedUrl: string
|
|
zoom: number
|
|
imageNaturalSize: { w: number; h: number } | null
|
|
imageNaturalH: number
|
|
emptyCellIds: Set<string>
|
|
showEmptyHighlight: boolean
|
|
structureBoxes: StructureBox[]
|
|
structureGraphics: StructureGraphic[]
|
|
showStructure: boolean
|
|
onTextChange: (cellId: string, newText: string) => void
|
|
onKeyDown: (e: React.KeyboardEvent, cellId: string) => void
|
|
onResetCell: (cellId: string) => void
|
|
onImageLoad: () => void
|
|
getDisplayText: (cell: EditableCell) => string
|
|
isEdited: (cell: EditableCell) => boolean
|
|
imageRef: React.RefObject<HTMLImageElement | null>
|
|
}
|
|
|
|
export function ReconstructionSimpleView({
|
|
cells,
|
|
dewarpedUrl,
|
|
zoom,
|
|
imageNaturalSize,
|
|
imageNaturalH,
|
|
emptyCellIds,
|
|
showEmptyHighlight,
|
|
structureBoxes,
|
|
structureGraphics,
|
|
showStructure,
|
|
onTextChange,
|
|
onKeyDown,
|
|
onResetCell,
|
|
onImageLoad,
|
|
getDisplayText,
|
|
isEdited,
|
|
imageRef,
|
|
}: ReconstructionSimpleViewProps) {
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Font size based on image natural height (not container) scaled by zoom
|
|
const getFontSize = useCallback((bboxH: number): number => {
|
|
const baseH = imageNaturalH || 800
|
|
const px = (bboxH / 100) * baseH * 0.55
|
|
return Math.max(8, Math.min(18, px * (zoom / 100)))
|
|
}, [imageNaturalH, zoom])
|
|
|
|
return (
|
|
<div className="border rounded-lg overflow-auto dark:border-gray-700 bg-gray-100 dark:bg-gray-900" style={{ maxHeight: '75vh' }}>
|
|
<div
|
|
ref={containerRef}
|
|
className="relative inline-block"
|
|
style={{ transform: `scale(${zoom / 100})`, transformOrigin: 'top left' }}
|
|
>
|
|
{/* Background image at reduced opacity */}
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
ref={imageRef}
|
|
src={dewarpedUrl}
|
|
alt="Dewarped"
|
|
className="block"
|
|
style={{ opacity: 0.3 }}
|
|
onLoad={onImageLoad}
|
|
/>
|
|
|
|
{/* Structure elements (boxes, graphics) */}
|
|
{imageNaturalSize && (
|
|
<StructureLayer
|
|
boxes={structureBoxes}
|
|
graphics={structureGraphics}
|
|
imgW={imageNaturalSize.w}
|
|
imgH={imageNaturalSize.h}
|
|
show={showStructure}
|
|
/>
|
|
)}
|
|
|
|
{/* Empty field markers */}
|
|
{showEmptyHighlight && cells
|
|
.filter(c => emptyCellIds.has(c.cellId))
|
|
.map(cell => (
|
|
<div
|
|
key={`empty-${cell.cellId}`}
|
|
className="absolute border-2 border-dashed border-red-400/60 rounded pointer-events-none"
|
|
style={{
|
|
left: `${cell.bboxPct.x}%`,
|
|
top: `${cell.bboxPct.y}%`,
|
|
width: `${cell.bboxPct.w}%`,
|
|
height: `${cell.bboxPct.h}%`,
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* Editable text fields at bbox positions */}
|
|
{cells.map((cell) => {
|
|
const displayText = getDisplayText(cell)
|
|
const edited = isEdited(cell)
|
|
|
|
return (
|
|
<div key={cell.cellId} className="absolute group" style={{
|
|
left: `${cell.bboxPct.x}%`,
|
|
top: `${cell.bboxPct.y}%`,
|
|
width: `${cell.bboxPct.w}%`,
|
|
height: `${cell.bboxPct.h}%`,
|
|
}}>
|
|
<input
|
|
id={`cell-${cell.cellId}`}
|
|
type="text"
|
|
value={displayText}
|
|
onChange={(e) => onTextChange(cell.cellId, e.target.value)}
|
|
onKeyDown={(e) => onKeyDown(e, cell.cellId)}
|
|
className={`w-full h-full bg-transparent text-black dark:text-white border px-0.5 outline-none transition-colors ${
|
|
colTypeColor(cell.colType)
|
|
} ${edited ? 'border-green-500 bg-green-50/30 dark:bg-green-900/20' : ''}`}
|
|
style={{
|
|
fontSize: `${getFontSize(cell.bboxPct.h)}px`,
|
|
lineHeight: '1',
|
|
}}
|
|
title={`${cell.cellId} (${cell.colType})`}
|
|
/>
|
|
{/* Per-cell reset button (X) — only shown for edited cells on hover */}
|
|
{edited && (
|
|
<button
|
|
onClick={() => onResetCell(cell.cellId)}
|
|
className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 text-white rounded-full text-[9px] leading-none opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
|
title="Zuruecksetzen"
|
|
>
|
|
×
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|