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>
170 lines
5.7 KiB
TypeScript
170 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import type { OverlayReconstructionProps } from './overlay-reconstruction-types'
|
|
import { KLAUSUR_API } from './overlay-reconstruction-types'
|
|
import { useOverlayReconstructionState } from './useOverlayReconstructionState'
|
|
import { OverlayToolbar } from './OverlayToolbar'
|
|
import { OverlayCellRenderer } from './OverlayCellRenderer'
|
|
|
|
export function OverlayReconstruction({ sessionId, onNext, wordResultOverride }: OverlayReconstructionProps) {
|
|
const state = useOverlayReconstructionState(sessionId, wordResultOverride)
|
|
|
|
const {
|
|
status,
|
|
error,
|
|
cells,
|
|
rows,
|
|
imageNaturalSize,
|
|
textOpacity,
|
|
textColor,
|
|
changedCount,
|
|
medianCellHeightPx,
|
|
fontScale,
|
|
globalBold,
|
|
cellWordPositions,
|
|
reconRef,
|
|
handleTextChange,
|
|
handleKeyDown,
|
|
getDisplayText,
|
|
isEdited,
|
|
resetCell,
|
|
saveReconstruction,
|
|
loadSessionData,
|
|
setError,
|
|
setImageNaturalSize,
|
|
} = state
|
|
|
|
if (!sessionId) {
|
|
return <div className="text-center py-12 text-gray-400">Bitte zuerst eine Session auswaehlen.</div>
|
|
}
|
|
|
|
if (status === 'loading') {
|
|
return (
|
|
<div className="flex items-center gap-3 justify-center py-12">
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-teal-500" />
|
|
<span className="text-gray-500">Overlay-Daten werden geladen...</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === 'error') {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="text-5xl mb-4">⚠️</div>
|
|
<h3 className="text-lg font-medium text-red-600 dark:text-red-400 mb-2">Fehler</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-lg mb-4">{error}</p>
|
|
<div className="flex gap-3">
|
|
<button onClick={() => { setError(''); loadSessionData() }}
|
|
className="px-5 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors text-sm">
|
|
Erneut versuchen
|
|
</button>
|
|
<button onClick={onNext}
|
|
className="px-5 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm">
|
|
Ueberspringen →
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (status === 'saved') {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<div className="text-5xl mb-4">✅</div>
|
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">Overlay gespeichert</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{changedCount > 0 ? `${changedCount} Zellen wurden aktualisiert.` : 'Keine Aenderungen vorgenommen.'}
|
|
</p>
|
|
<button onClick={onNext}
|
|
className="px-6 py-2.5 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors font-medium">
|
|
Fertig
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const imgW = imageNaturalSize?.w || 1
|
|
const imgH = imageNaturalSize?.h || 1
|
|
const colorValue = textColor === 'black' ? '#1a1a1a' : textColor
|
|
const dewarpedUrl = sessionId
|
|
? `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped`
|
|
: ''
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Toolbar */}
|
|
<OverlayToolbar state={state} />
|
|
|
|
{/* True overlay: text layer on top of original image */}
|
|
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden bg-gray-50 dark:bg-gray-900">
|
|
<div
|
|
ref={reconRef}
|
|
className="relative"
|
|
style={{ aspectRatio: `${imgW} / ${imgH}` }}
|
|
>
|
|
{/* Background: original image */}
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={dewarpedUrl}
|
|
alt="Original"
|
|
className="absolute inset-0 w-full h-full object-contain"
|
|
onLoad={(e) => {
|
|
const img = e.target as HTMLImageElement
|
|
setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
|
|
}}
|
|
/>
|
|
|
|
{/* Text overlay layer */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{ opacity: textOpacity / 100 }}
|
|
>
|
|
{/* Row lines */}
|
|
{rows.map((row, i) => (
|
|
<div
|
|
key={`row-${i}`}
|
|
className="absolute left-0 right-0 border-t border-cyan-400/40"
|
|
style={{ top: `${(row.y / imgH) * 100}%` }}
|
|
/>
|
|
))}
|
|
|
|
{/* Cells */}
|
|
{cells.map((cell) => (
|
|
<OverlayCellRenderer
|
|
key={cell.cellId}
|
|
cell={cell}
|
|
displayText={getDisplayText(cell)}
|
|
edited={isEdited(cell)}
|
|
wordPositions={cellWordPositions.get(cell.cellId)}
|
|
medianCellHeightPx={medianCellHeightPx}
|
|
fontScale={fontScale}
|
|
globalBold={globalBold}
|
|
colorValue={colorValue}
|
|
handleTextChange={handleTextChange}
|
|
handleKeyDown={handleKeyDown}
|
|
resetCell={resetCell}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bottom action */}
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => {
|
|
if (changedCount > 0) {
|
|
saveReconstruction()
|
|
} else {
|
|
onNext()
|
|
}
|
|
}}
|
|
className="px-6 py-2.5 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors font-medium text-sm"
|
|
>
|
|
{changedCount > 0 ? 'Speichern & Fertig' : 'Fertig'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|