[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:
@@ -1,7 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { DeskewResult, DewarpResult, DewarpDetection, DewarpGroundTruth } from '@/app/(admin)/ai/ocr-kombi/types'
|
||||
import type { DeskewResult, DewarpResult, DewarpGroundTruth } from '@/app/(admin)/ai/ocr-kombi/types'
|
||||
import { DewarpSummaryBanner } from './DewarpSummaryBanner'
|
||||
import { DewarpFineTunePanel } from './DewarpFineTunePanel'
|
||||
import { DewarpGroundTruthPanel } from './DewarpGroundTruthPanel'
|
||||
|
||||
interface DewarpControlsProps {
|
||||
dewarpResult: DewarpResult | null
|
||||
@@ -15,93 +18,6 @@ interface DewarpControlsProps {
|
||||
isApplying: boolean
|
||||
}
|
||||
|
||||
const METHOD_LABELS: Record<string, string> = {
|
||||
vertical_edge: 'A: Vertikale Kanten',
|
||||
projection: 'B: Projektions-Varianz',
|
||||
hough_lines: 'C: Hough-Linien',
|
||||
text_lines: 'D: Textzeilenanalyse',
|
||||
manual: 'Manuell',
|
||||
manual_combined: 'Manuell (kombiniert)',
|
||||
none: 'Keine Korrektur',
|
||||
}
|
||||
|
||||
const SHEAR_METHOD_KEYS = ['vertical_edge', 'projection', 'hough_lines', 'text_lines'] as const
|
||||
|
||||
/** Colour for a confidence value (0-1). */
|
||||
function confColor(conf: number): string {
|
||||
if (conf >= 0.7) return 'text-green-600 dark:text-green-400'
|
||||
if (conf >= 0.5) return 'text-yellow-600 dark:text-yellow-400'
|
||||
return 'text-gray-400'
|
||||
}
|
||||
|
||||
/** Short confidence bar (visual). */
|
||||
function ConfBar({ value }: { value: number }) {
|
||||
const pct = Math.round(value * 100)
|
||||
const bg = value >= 0.7 ? 'bg-green-500' : value >= 0.5 ? 'bg-yellow-500' : 'bg-gray-400'
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-16 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${bg}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className={`text-xs font-mono ${confColor(value)}`}>{pct}%</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** A single slider row for fine-tuning. */
|
||||
function FineTuneSlider({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
unit = '\u00B0',
|
||||
radioName,
|
||||
radioChecked,
|
||||
onRadioChange,
|
||||
}: {
|
||||
label: string
|
||||
value: number
|
||||
onChange: (v: number) => void
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
unit?: string
|
||||
radioName?: string
|
||||
radioChecked?: boolean
|
||||
onRadioChange?: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{radioName !== undefined && (
|
||||
<input
|
||||
type="radio"
|
||||
name={radioName}
|
||||
checked={radioChecked}
|
||||
onChange={onRadioChange}
|
||||
className="w-3.5 h-3.5 accent-teal-500"
|
||||
/>
|
||||
)}
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 w-36 shrink-0">{label}</span>
|
||||
<span className="text-xs text-gray-400 w-8 text-right">{min}{unit}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={min * 100}
|
||||
max={max * 100}
|
||||
step={step * 100}
|
||||
value={Math.round(value * 100)}
|
||||
onChange={(e) => onChange(parseInt(e.target.value) / 100)}
|
||||
className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500"
|
||||
/>
|
||||
<span className="text-xs text-gray-400 w-8">+{max}{unit}</span>
|
||||
<span className="font-mono text-xs w-14 text-right tabular-nums">
|
||||
{value >= 0 ? '+' : ''}{value.toFixed(2)}{unit}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DewarpControls({
|
||||
dewarpResult,
|
||||
deskewResult,
|
||||
@@ -114,26 +30,9 @@ export function DewarpControls({
|
||||
isApplying,
|
||||
}: DewarpControlsProps) {
|
||||
const [manualShear, setManualShear] = useState(0)
|
||||
const [gtFeedback, setGtFeedback] = useState<'correct' | 'incorrect' | null>(null)
|
||||
const [gtNotes, setGtNotes] = useState('')
|
||||
const [gtSaved, setGtSaved] = useState(false)
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [showFineTune, setShowFineTune] = useState(false)
|
||||
|
||||
// Fine-tuning rotation sliders (3 passes)
|
||||
const [p1Iterative, setP1Iterative] = useState(0)
|
||||
const [p2Residual, setP2Residual] = useState(0)
|
||||
const [p3Textline, setP3Textline] = useState(0)
|
||||
|
||||
// Fine-tuning shear sliders (4 methods) + selected method
|
||||
const [shearValues, setShearValues] = useState<Record<string, number>>({
|
||||
vertical_edge: 0,
|
||||
projection: 0,
|
||||
hough_lines: 0,
|
||||
text_lines: 0,
|
||||
})
|
||||
const [selectedShearMethod, setSelectedShearMethod] = useState<string>('vertical_edge')
|
||||
|
||||
// Initialize slider to auto-detected value when result arrives
|
||||
useEffect(() => {
|
||||
if (dewarpResult && dewarpResult.shear_degrees !== undefined) {
|
||||
@@ -141,192 +40,15 @@ export function DewarpControls({
|
||||
}
|
||||
}, [dewarpResult?.shear_degrees])
|
||||
|
||||
// Initialize fine-tuning sliders from deskew result
|
||||
useEffect(() => {
|
||||
if (deskewResult) {
|
||||
setP1Iterative(deskewResult.angle_iterative ?? 0)
|
||||
setP2Residual(deskewResult.angle_residual ?? 0)
|
||||
setP3Textline(deskewResult.angle_textline ?? 0)
|
||||
}
|
||||
}, [deskewResult])
|
||||
|
||||
// Initialize shear sliders from dewarp detections
|
||||
useEffect(() => {
|
||||
if (dewarpResult?.detections) {
|
||||
const newValues = { ...shearValues }
|
||||
let bestMethod = selectedShearMethod
|
||||
let bestConf = -1
|
||||
for (const d of dewarpResult.detections) {
|
||||
if (d.method in newValues) {
|
||||
newValues[d.method] = d.shear_degrees
|
||||
if (d.confidence > bestConf) {
|
||||
bestConf = d.confidence
|
||||
bestMethod = d.method
|
||||
}
|
||||
}
|
||||
}
|
||||
setShearValues(newValues)
|
||||
// Select the method that was actually used, or the highest confidence
|
||||
if (dewarpResult.method_used && dewarpResult.method_used in newValues) {
|
||||
setSelectedShearMethod(dewarpResult.method_used)
|
||||
} else {
|
||||
setSelectedShearMethod(bestMethod)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dewarpResult?.detections])
|
||||
|
||||
const rotationSum = p1Iterative + p2Residual + p3Textline
|
||||
const activeShear = shearValues[selectedShearMethod] ?? 0
|
||||
|
||||
const handleGroundTruth = (isCorrect: boolean) => {
|
||||
setGtFeedback(isCorrect ? 'correct' : 'incorrect')
|
||||
if (isCorrect) {
|
||||
onGroundTruth({ is_correct: true })
|
||||
setGtSaved(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleGroundTruthIncorrect = () => {
|
||||
onGroundTruth({
|
||||
is_correct: false,
|
||||
corrected_shear: manualShear !== 0 ? manualShear : undefined,
|
||||
notes: gtNotes || undefined,
|
||||
})
|
||||
setGtSaved(true)
|
||||
}
|
||||
|
||||
const handleShearValueChange = (method: string, value: number) => {
|
||||
setShearValues((prev) => ({ ...prev, [method]: value }))
|
||||
}
|
||||
|
||||
const handleFineTunePreview = () => {
|
||||
if (onCombinedAdjust) {
|
||||
onCombinedAdjust(rotationSum, activeShear)
|
||||
}
|
||||
}
|
||||
|
||||
const wasRejected = dewarpResult && dewarpResult.method_used === 'none' && (dewarpResult.detections || []).length > 0
|
||||
const wasApplied = dewarpResult && dewarpResult.method_used !== 'none' && dewarpResult.method_used !== 'manual' && dewarpResult.method_used !== 'manual_combined'
|
||||
const detections = dewarpResult?.detections || []
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary banner */}
|
||||
{dewarpResult && (
|
||||
<div className={`rounded-lg border p-4 ${
|
||||
wasRejected
|
||||
? 'bg-amber-50 border-amber-200 dark:bg-amber-900/20 dark:border-amber-700'
|
||||
: wasApplied
|
||||
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-700'
|
||||
: 'bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700'
|
||||
}`}>
|
||||
{/* Status line */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className={`text-lg ${wasRejected ? '' : wasApplied ? '' : ''}`}>
|
||||
{wasRejected ? '\u26A0\uFE0F' : wasApplied ? '\u2705' : '\u2796'}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
{wasRejected
|
||||
? 'Quality Gate: Korrektur verworfen (Projektion nicht verbessert)'
|
||||
: wasApplied
|
||||
? `Korrektur angewendet: ${dewarpResult.shear_degrees.toFixed(2)}\u00B0`
|
||||
: dewarpResult.method_used === 'manual' || dewarpResult.method_used === 'manual_combined'
|
||||
? `Manuelle Korrektur: ${dewarpResult.shear_degrees.toFixed(2)}\u00B0`
|
||||
: 'Keine Korrektur noetig'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Key metrics */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">Scherung:</span>{' '}
|
||||
<span className="font-mono font-medium">{dewarpResult.shear_degrees.toFixed(2)}\u00B0</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
|
||||
<div>
|
||||
<span className="text-gray-500">Methode:</span>{' '}
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300">
|
||||
{dewarpResult.method_used.includes('+')
|
||||
? `Ensemble (${dewarpResult.method_used.split('+').map(m => METHOD_LABELS[m] || m).join(' + ')})`
|
||||
: METHOD_LABELS[dewarpResult.method_used] || dewarpResult.method_used}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-4 w-px bg-gray-300 dark:bg-gray-600" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-gray-500">Konfidenz:</span>
|
||||
<ConfBar value={dewarpResult.confidence} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toggles row */}
|
||||
<div className="flex gap-2 mt-3">
|
||||
<button
|
||||
onClick={onToggleGrid}
|
||||
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
||||
showGrid
|
||||
? 'bg-teal-100 border-teal-300 text-teal-700 dark:bg-teal-900/40 dark:border-teal-600 dark:text-teal-300'
|
||||
: 'border-gray-300 text-gray-500 dark:border-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Raster
|
||||
</button>
|
||||
{detections.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowDetails(v => !v)}
|
||||
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
||||
showDetails
|
||||
? 'bg-blue-100 border-blue-300 text-blue-700 dark:bg-blue-900/40 dark:border-blue-600 dark:text-blue-300'
|
||||
: 'border-gray-300 text-gray-500 dark:border-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Details ({detections.length} Methoden)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detailed detections */}
|
||||
{showDetails && detections.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-500 mb-2">Einzelne Detektoren:</div>
|
||||
<div className="space-y-1.5">
|
||||
{detections.map((d: DewarpDetection) => {
|
||||
const isUsed = dewarpResult.method_used.includes(d.method)
|
||||
const aboveThreshold = d.confidence >= 0.5
|
||||
return (
|
||||
<div
|
||||
key={d.method}
|
||||
className={`flex items-center gap-3 text-xs px-2 py-1.5 rounded ${
|
||||
isUsed
|
||||
? 'bg-teal-50 dark:bg-teal-900/20'
|
||||
: 'bg-gray-50 dark:bg-gray-800'
|
||||
}`}
|
||||
>
|
||||
<span className="w-4 text-center">
|
||||
{isUsed ? '\u2713' : aboveThreshold ? '\u2012' : '\u2717'}
|
||||
</span>
|
||||
<span className={`w-40 ${isUsed ? 'font-medium text-gray-800 dark:text-gray-200' : 'text-gray-500'}`}>
|
||||
{METHOD_LABELS[d.method] || d.method}
|
||||
</span>
|
||||
<span className="font-mono w-16 text-right">
|
||||
{d.shear_degrees.toFixed(2)}\u00B0
|
||||
</span>
|
||||
<ConfBar value={d.confidence} />
|
||||
{!aboveThreshold && (
|
||||
<span className="text-gray-400 ml-1">(unter Schwelle)</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{wasRejected && (
|
||||
<div className="mt-2 text-xs text-amber-600 dark:text-amber-400">
|
||||
Die Korrektur wurde verworfen, weil die horizontale Projektions-Varianz nach Anwendung nicht besser war als vorher.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DewarpSummaryBanner
|
||||
dewarpResult={dewarpResult}
|
||||
showGrid={showGrid}
|
||||
onToggleGrid={onToggleGrid}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Manual shear angle slider */}
|
||||
@@ -334,7 +56,7 @@ export function DewarpControls({
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Scherwinkel (manuell)</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-10 text-right">-2.0\u00B0</span>
|
||||
<span className="text-xs text-gray-400 w-10 text-right">-2.0{'\u00B0'}</span>
|
||||
<input
|
||||
type="range"
|
||||
min={-200}
|
||||
@@ -344,8 +66,8 @@ export function DewarpControls({
|
||||
onChange={(e) => setManualShear(parseInt(e.target.value) / 100)}
|
||||
className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500"
|
||||
/>
|
||||
<span className="text-xs text-gray-400 w-10">+2.0\u00B0</span>
|
||||
<span className="font-mono text-sm w-16 text-right">{manualShear.toFixed(2)}\u00B0</span>
|
||||
<span className="text-xs text-gray-400 w-10">+2.0{'\u00B0'}</span>
|
||||
<span className="font-mono text-sm w-16 text-right">{manualShear.toFixed(2)}{'\u00B0'}</span>
|
||||
<button
|
||||
onClick={() => onManualDewarp(manualShear)}
|
||||
disabled={isApplying}
|
||||
@@ -362,179 +84,27 @@ export function DewarpControls({
|
||||
|
||||
{/* Fine-tuning panel */}
|
||||
{dewarpResult && onCombinedAdjust && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setShowFineTune(v => !v)}
|
||||
className="w-full flex items-center justify-between p-4 text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm">⚙️</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Feinabstimmung</span>
|
||||
<span className="text-xs text-gray-400">(7 Regler)</span>
|
||||
</div>
|
||||
<span className="text-gray-400 text-sm">{showFineTune ? '\u25B2' : '\u25BC'}</span>
|
||||
</button>
|
||||
|
||||
{showFineTune && (
|
||||
<div className="px-4 pb-4 space-y-5">
|
||||
{/* Rotation section */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||||
Rotation (Begradigung)
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<FineTuneSlider
|
||||
label="P1 Iterative Projection"
|
||||
value={p1Iterative}
|
||||
onChange={setP1Iterative}
|
||||
min={-5}
|
||||
max={5}
|
||||
step={0.05}
|
||||
/>
|
||||
<FineTuneSlider
|
||||
label="P2 Word-Alignment"
|
||||
value={p2Residual}
|
||||
onChange={setP2Residual}
|
||||
min={-3}
|
||||
max={3}
|
||||
step={0.05}
|
||||
/>
|
||||
<FineTuneSlider
|
||||
label="P3 Textline-Regression"
|
||||
value={p3Textline}
|
||||
onChange={setP3Textline}
|
||||
min={-3}
|
||||
max={3}
|
||||
step={0.05}
|
||||
/>
|
||||
<div className="flex items-center gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 w-36 shrink-0">Summe Rotation</span>
|
||||
<span className="font-mono text-sm font-medium text-teal-600 dark:text-teal-400">
|
||||
{rotationSum >= 0 ? '+' : ''}{rotationSum.toFixed(2)}\u00B0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shear section */}
|
||||
<div>
|
||||
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
|
||||
Scherung (Entzerrung) — einen Wert waehlen
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{SHEAR_METHOD_KEYS.map((method) => (
|
||||
<FineTuneSlider
|
||||
key={method}
|
||||
label={METHOD_LABELS[method] || method}
|
||||
value={shearValues[method]}
|
||||
onChange={(v) => handleShearValueChange(method, v)}
|
||||
min={-5}
|
||||
max={5}
|
||||
step={0.05}
|
||||
radioName="shear-method"
|
||||
radioChecked={selectedShearMethod === method}
|
||||
onRadioChange={() => setSelectedShearMethod(method)}
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-center gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 w-36 shrink-0">Gewaehlte Scherung</span>
|
||||
<span className="font-mono text-sm font-medium text-teal-600 dark:text-teal-400">
|
||||
{activeShear >= 0 ? '+' : ''}{activeShear.toFixed(2)}\u00B0
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 ml-1">
|
||||
({METHOD_LABELS[selectedShearMethod]})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview + Save */}
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleFineTunePreview}
|
||||
disabled={isApplying}
|
||||
className="px-4 py-2 text-sm bg-teal-600 text-white rounded-md hover:bg-teal-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isApplying ? 'Wird angewendet...' : 'Vorschau'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onGroundTruth({
|
||||
is_correct: false,
|
||||
corrected_shear: activeShear,
|
||||
notes: `Fine-tuned: rotation=${rotationSum.toFixed(3)}, shear=${activeShear.toFixed(3)} (${selectedShearMethod})`,
|
||||
})
|
||||
setGtSaved(true)
|
||||
}}
|
||||
disabled={gtSaved}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{gtSaved ? 'Gespeichert' : 'Als Ground Truth speichern'}
|
||||
</button>
|
||||
<span className="text-xs text-gray-400">
|
||||
Rotation: {rotationSum >= 0 ? '+' : ''}{rotationSum.toFixed(2)}\u00B0 + Scherung: {activeShear >= 0 ? '+' : ''}{activeShear.toFixed(2)}\u00B0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DewarpFineTunePanel
|
||||
dewarpResult={dewarpResult}
|
||||
deskewResult={deskewResult}
|
||||
showFineTune={showFineTune}
|
||||
onToggleFineTune={() => setShowFineTune(v => !v)}
|
||||
onCombinedAdjust={onCombinedAdjust}
|
||||
onGroundTruth={onGroundTruth}
|
||||
isApplying={isApplying}
|
||||
gtSaved={gtSaved}
|
||||
onGtSaved={() => setGtSaved(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Ground Truth */}
|
||||
{dewarpResult && !showFineTune && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Spalten vertikal ausgerichtet?
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mb-2">Pruefen ob die Spaltenraender jetzt senkrecht zum Raster stehen.</p>
|
||||
{!gtSaved ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleGroundTruth(true)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
gtFeedback === 'correct'
|
||||
? 'bg-green-100 text-green-700 ring-2 ring-green-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-green-50 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Ja
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleGroundTruth(false)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-medium transition-colors ${
|
||||
gtFeedback === 'incorrect'
|
||||
? 'bg-red-100 text-red-700 ring-2 ring-red-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-red-50 dark:bg-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Nein
|
||||
</button>
|
||||
</div>
|
||||
{gtFeedback === 'incorrect' && (
|
||||
<div className="space-y-2">
|
||||
<textarea
|
||||
value={gtNotes}
|
||||
onChange={(e) => setGtNotes(e.target.value)}
|
||||
placeholder="Notizen zur Korrektur..."
|
||||
className="w-full text-sm border border-gray-300 dark:border-gray-600 rounded-md p-2 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200"
|
||||
rows={2}
|
||||
/>
|
||||
<button
|
||||
onClick={handleGroundTruthIncorrect}
|
||||
className="text-sm px-3 py-1 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Feedback speichern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-green-600 dark:text-green-400">
|
||||
Feedback gespeichert
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DewarpGroundTruthPanel
|
||||
manualShear={manualShear}
|
||||
onGroundTruth={onGroundTruth}
|
||||
gtSaved={gtSaved}
|
||||
onGtSaved={() => setGtSaved(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Next button */}
|
||||
|
||||
Reference in New Issue
Block a user