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>
255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import type { ExcludeRegion, StructureResult } from '@/app/(admin)/ai/ocr-kombi/types'
|
|
import { COLOR_HEX, getDocLayoutColor } from './structure-detection-utils'
|
|
|
|
interface StructureResultDetailsProps {
|
|
result: StructureResult
|
|
excludeRegions: ExcludeRegion[]
|
|
}
|
|
|
|
export function StructureResultDetails({ result, excludeRegions }: StructureResultDetailsProps) {
|
|
return (
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 space-y-3">
|
|
{/* Summary badges */}
|
|
<div className="flex flex-wrap items-center gap-3 text-sm">
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-teal-50 dark:bg-teal-900/20 text-teal-700 dark:text-teal-400 text-xs font-medium">
|
|
{result.zones.length} Zone(n)
|
|
</span>
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 text-xs font-medium">
|
|
{result.boxes.length} Box(en)
|
|
</span>
|
|
{result.layout_regions && result.layout_regions.length > 0 && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-400 text-xs font-medium">
|
|
{result.layout_regions.length} Layout-Region(en)
|
|
</span>
|
|
)}
|
|
{result.graphics && result.graphics.length > 0 && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-400 text-xs font-medium">
|
|
{result.graphics.length} Grafik(en)
|
|
</span>
|
|
)}
|
|
{result.has_words && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 text-xs font-medium">
|
|
{result.word_count} Woerter
|
|
</span>
|
|
)}
|
|
{excludeRegions.length > 0 && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 text-xs font-medium">
|
|
{excludeRegions.length} Ausschluss
|
|
</span>
|
|
)}
|
|
{(result.border_ghosts_removed ?? 0) > 0 && (
|
|
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 text-xs font-medium">
|
|
{result.border_ghosts_removed} Rahmenlinien entfernt
|
|
</span>
|
|
)}
|
|
<span className="text-gray-400 text-xs ml-auto">
|
|
{result.detection_method && (
|
|
<span className="mr-1.5">
|
|
{result.detection_method === 'ppdoclayout' ? 'PP-DocLayout' : 'OpenCV'} |
|
|
</span>
|
|
)}
|
|
{result.image_width}x{result.image_height}px | {result.duration_seconds}s
|
|
</span>
|
|
</div>
|
|
|
|
{/* Boxes detail */}
|
|
{result.boxes.length > 0 && (
|
|
<BoxesDetail boxes={result.boxes} />
|
|
)}
|
|
|
|
{/* PP-DocLayout regions detail */}
|
|
{result.layout_regions && result.layout_regions.length > 0 && (
|
|
<LayoutRegionsDetail regions={result.layout_regions} />
|
|
)}
|
|
|
|
{/* Zones detail */}
|
|
<ZonesDetail zones={result.zones} />
|
|
|
|
{/* Graphics / visual elements */}
|
|
{result.graphics && result.graphics.length > 0 && (
|
|
<GraphicsDetail graphics={result.graphics} />
|
|
)}
|
|
|
|
{/* Color regions */}
|
|
{Object.keys(result.color_pixel_counts).length > 0 && (
|
|
<ColorRegionsDetail colorPixelCounts={result.color_pixel_counts} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Sub-sections */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function BoxesDetail({ boxes }: { boxes: StructureResult['boxes'] }) {
|
|
return (
|
|
<div>
|
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Erkannte Boxen</h4>
|
|
<div className="space-y-1.5">
|
|
{boxes.map((box, i) => (
|
|
<div key={i} className="flex items-center gap-3 text-xs">
|
|
<span
|
|
className="w-3 h-3 rounded-sm flex-shrink-0 border border-gray-300 dark:border-gray-600"
|
|
style={{ backgroundColor: box.bg_color_hex || '#6b7280' }}
|
|
/>
|
|
<span className="text-gray-600 dark:text-gray-400">
|
|
Box {i + 1}:
|
|
</span>
|
|
<span className="font-mono text-gray-500">
|
|
{box.w}x{box.h}px @ ({box.x}, {box.y})
|
|
</span>
|
|
{box.bg_color_name && box.bg_color_name !== 'unknown' && box.bg_color_name !== 'white' && (
|
|
<span className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-500">
|
|
{box.bg_color_name}
|
|
</span>
|
|
)}
|
|
{box.border_thickness > 0 && (
|
|
<span className="text-gray-400">
|
|
Rahmen: {box.border_thickness}px
|
|
</span>
|
|
)}
|
|
<span className="text-gray-400">
|
|
{Math.round(box.confidence * 100)}%
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LayoutRegionsDetail({ regions }: { regions: NonNullable<StructureResult['layout_regions']> }) {
|
|
return (
|
|
<div>
|
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
|
|
PP-DocLayout Regionen ({regions.length})
|
|
</h4>
|
|
<div className="space-y-1.5">
|
|
{regions.map((region, i) => {
|
|
const color = getDocLayoutColor(region.class_name)
|
|
return (
|
|
<div key={i} className="flex items-center gap-3 text-xs">
|
|
<span
|
|
className="w-3 h-3 rounded-sm flex-shrink-0 border"
|
|
style={{ backgroundColor: `${color}40`, borderColor: color }}
|
|
/>
|
|
<span className="text-gray-600 dark:text-gray-400 font-medium min-w-[60px]">
|
|
{region.class_name}
|
|
</span>
|
|
<span className="font-mono text-gray-500">
|
|
{region.w}x{region.h}px @ ({region.x}, {region.y})
|
|
</span>
|
|
<span className="text-gray-400">
|
|
{Math.round(region.confidence * 100)}%
|
|
</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ZonesDetail({ zones }: { zones: StructureResult['zones'] }) {
|
|
return (
|
|
<div>
|
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Seitenzonen</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{zones.map((zone) => (
|
|
<span
|
|
key={zone.index}
|
|
className={`inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] font-medium ${
|
|
zone.zone_type === 'box'
|
|
? 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800'
|
|
: 'bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700'
|
|
}`}
|
|
>
|
|
{zone.zone_type === 'box' ? 'Box' : 'Inhalt'} {zone.index}
|
|
<span className="text-[10px] font-normal opacity-70">
|
|
({zone.w}x{zone.h})
|
|
</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function GraphicsDetail({ graphics }: { graphics: StructureResult['graphics'] }) {
|
|
// Summary by shape
|
|
const shapeCounts: Record<string, number> = {}
|
|
for (const g of graphics) {
|
|
shapeCounts[g.shape] = (shapeCounts[g.shape] || 0) + 1
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">
|
|
Graphische Elemente ({graphics.length})
|
|
</h4>
|
|
{/* Summary by shape */}
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
{Object.entries(shapeCounts)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.map(([shape, count]) => (
|
|
<span
|
|
key={shape}
|
|
className="inline-flex items-center gap-1 px-2 py-1 rounded text-[11px] bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 border border-purple-200 dark:border-purple-800"
|
|
>
|
|
{shape === 'arrow' ? '\u2192' : shape === 'circle' ? '\u25CF' : shape === 'line' ? '\u2500' : shape === 'exclamation' ? '\u2757' : shape === 'dot' ? '\u2022' : shape === 'illustration' ? '\uD83D\uDDBC' : '\u25C6'}
|
|
{' '}{shape} <span className="font-semibold">x{count}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
{/* Individual graphics list */}
|
|
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
|
{graphics.map((g, i) => (
|
|
<div key={i} className="flex items-center gap-3 text-xs">
|
|
<span
|
|
className="w-3 h-3 rounded-full flex-shrink-0 border border-gray-300 dark:border-gray-600"
|
|
style={{ backgroundColor: g.color_hex || '#6b7280' }}
|
|
/>
|
|
<span className="text-gray-600 dark:text-gray-400 font-medium min-w-[60px]">
|
|
{g.shape}
|
|
</span>
|
|
<span className="font-mono text-gray-500">
|
|
{g.w}x{g.h}px @ ({g.x}, {g.y})
|
|
</span>
|
|
<span className="text-gray-400">
|
|
{g.color_name}
|
|
</span>
|
|
<span className="text-gray-400">
|
|
{Math.round(g.confidence * 100)}%
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ColorRegionsDetail({ colorPixelCounts }: { colorPixelCounts: Record<string, number> }) {
|
|
return (
|
|
<div>
|
|
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-2">Erkannte Farben</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{Object.entries(colorPixelCounts)
|
|
.sort(([, a], [, b]) => b - a)
|
|
.map(([name, count]) => (
|
|
<span key={name} className="inline-flex items-center gap-1.5 px-2 py-1 rounded text-[11px] bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
|
<span
|
|
className="w-2.5 h-2.5 rounded-full"
|
|
style={{ backgroundColor: COLOR_HEX[name] || '#6b7280' }}
|
|
/>
|
|
<span className="text-gray-600 dark:text-gray-400">{name}</span>
|
|
<span className="text-gray-400 text-[10px]">{count.toLocaleString()}px</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|