backend-lehrer (10 files): - game/database.py (785 → 5), correction_api.py (683 → 4) - classroom_engine/antizipation.py (676 → 5) - llm_gateway schools/edu_search already done in prior batch klausur-service (12 files): - orientation_crop_api.py (694 → 5), pdf_export.py (677 → 4) - zeugnis_crawler.py (676 → 5), grid_editor_api.py (671 → 5) - eh_templates.py (658 → 5), mail/api.py (651 → 5) - qdrant_service.py (638 → 5), training_api.py (625 → 4) website (6 pages): - middleware (696 → 8), mail (733 → 6), consent (628 → 8) - compliance/risks (622 → 5), export (502 → 5), brandbook (629 → 7) studio-v2 (3 components): - B2BMigrationWizard (848 → 3), CleanupPanel (765 → 2) - dashboard-experimental (739 → 2) admin-lehrer (4 files): - uebersetzungen (769 → 4), manager (670 → 2) - ChunkBrowserQA (675 → 6), dsfa/page (674 → 5) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
391 lines
15 KiB
TypeScript
391 lines
15 KiB
TypeScript
'use client'
|
||
|
||
import type { PreviewResult, PipelineResult } from './CleanupPanel'
|
||
|
||
// =============================================================================
|
||
// Step 1: Upload
|
||
// =============================================================================
|
||
|
||
interface UploadStepProps {
|
||
isDark: boolean
|
||
labelStyle: string
|
||
cardStyle: string
|
||
file: File | null
|
||
previewUrl: string | null
|
||
handleDrop: (e: React.DragEvent) => void
|
||
handleFileSelect: (f: File) => void
|
||
removeHandwriting: boolean
|
||
setRemoveHandwriting: (v: boolean) => void
|
||
reconstructLayout: boolean
|
||
setReconstructLayout: (v: boolean) => void
|
||
inpaintingMethod: string
|
||
setInpaintingMethod: (v: string) => void
|
||
lamaAvailable: boolean
|
||
}
|
||
|
||
export function UploadStep({
|
||
isDark, labelStyle, cardStyle, file, previewUrl,
|
||
handleDrop, handleFileSelect,
|
||
removeHandwriting, setRemoveHandwriting,
|
||
reconstructLayout, setReconstructLayout,
|
||
inpaintingMethod, setInpaintingMethod,
|
||
lamaAvailable,
|
||
}: UploadStepProps) {
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Dropzone */}
|
||
<div
|
||
className={`border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all ${
|
||
isDark
|
||
? 'border-white/20 hover:border-purple-400/50 hover:bg-white/5'
|
||
: 'border-slate-300 hover:border-purple-400 hover:bg-purple-50/30'
|
||
}`}
|
||
onDrop={handleDrop}
|
||
onDragOver={(e) => e.preventDefault()}
|
||
onClick={() => document.getElementById('file-input')?.click()}
|
||
>
|
||
<input
|
||
id="file-input"
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={(e) => e.target.files?.[0] && handleFileSelect(e.target.files[0])}
|
||
className="hidden"
|
||
/>
|
||
{previewUrl ? (
|
||
<div className="space-y-4">
|
||
<img
|
||
src={previewUrl}
|
||
alt="Preview"
|
||
className="max-h-64 mx-auto rounded-xl shadow-lg"
|
||
/>
|
||
<p className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{file?.name}
|
||
</p>
|
||
<p className={`text-sm ${labelStyle}`}>
|
||
Klicke zum Ändern oder ziehe eine andere Datei hierher
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<svg className={`w-16 h-16 mx-auto mb-4 ${isDark ? 'text-white/30' : 'text-slate-300'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||
</svg>
|
||
<p className={`text-lg font-medium mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Bild hochladen
|
||
</p>
|
||
<p className={labelStyle}>
|
||
Ziehe ein Bild hierher oder klicke zum Auswählen
|
||
</p>
|
||
<p className={`text-xs mt-2 ${labelStyle}`}>
|
||
Unterstützt: PNG, JPG, JPEG
|
||
</p>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* Options */}
|
||
{file && (
|
||
<div className="space-y-4">
|
||
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Optionen
|
||
</h3>
|
||
|
||
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${cardStyle}`}>
|
||
<input
|
||
type="checkbox"
|
||
checked={removeHandwriting}
|
||
onChange={(e) => setRemoveHandwriting(e.target.checked)}
|
||
className="w-5 h-5 rounded"
|
||
/>
|
||
<div>
|
||
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Handschrift entfernen
|
||
</span>
|
||
<p className={`text-sm ${labelStyle}`}>
|
||
Erkennt und entfernt handgeschriebene Inhalte
|
||
</p>
|
||
</div>
|
||
</label>
|
||
|
||
<label className={`flex items-center gap-3 p-4 rounded-xl border cursor-pointer transition-all ${cardStyle}`}>
|
||
<input
|
||
type="checkbox"
|
||
checked={reconstructLayout}
|
||
onChange={(e) => setReconstructLayout(e.target.checked)}
|
||
className="w-5 h-5 rounded"
|
||
/>
|
||
<div>
|
||
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Layout rekonstruieren
|
||
</span>
|
||
<p className={`text-sm ${labelStyle}`}>
|
||
Erstellt bearbeitbare Fabric.js Objekte
|
||
</p>
|
||
</div>
|
||
</label>
|
||
|
||
{removeHandwriting && (
|
||
<div className="space-y-2">
|
||
<label className={`block text-sm font-medium ${labelStyle}`}>
|
||
Inpainting-Methode
|
||
</label>
|
||
<select
|
||
value={inpaintingMethod}
|
||
onChange={(e) => setInpaintingMethod(e.target.value)}
|
||
className={`w-full p-3 rounded-xl border ${
|
||
isDark
|
||
? 'bg-white/10 border-white/20 text-white'
|
||
: 'bg-white border-slate-200 text-slate-900'
|
||
}`}
|
||
>
|
||
<option value="auto">Automatisch (empfohlen)</option>
|
||
<option value="opencv_telea">OpenCV Telea (schnell)</option>
|
||
<option value="opencv_ns">OpenCV NS (glatter)</option>
|
||
{lamaAvailable && (
|
||
<option value="lama">LaMa (beste Qualität)</option>
|
||
)}
|
||
</select>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// Step 2: Preview
|
||
// =============================================================================
|
||
|
||
interface PreviewStepProps {
|
||
isDark: boolean
|
||
labelStyle: string
|
||
cardStyle: string
|
||
previewResult: PreviewResult
|
||
previewUrl: string | null
|
||
maskUrl: string | null
|
||
removeHandwriting: boolean
|
||
reconstructLayout: boolean
|
||
handleGetMask: () => void
|
||
}
|
||
|
||
export function PreviewStep({
|
||
isDark, labelStyle, cardStyle,
|
||
previewResult, previewUrl, maskUrl,
|
||
removeHandwriting, reconstructLayout,
|
||
handleGetMask,
|
||
}: PreviewStepProps) {
|
||
return (
|
||
<div className="space-y-6">
|
||
<div className="grid grid-cols-2 gap-6">
|
||
{/* Detection Result */}
|
||
<div className={`p-4 rounded-xl border ${cardStyle}`}>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Erkennungsergebnis
|
||
</h3>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Handschrift gefunden:</span>
|
||
<span className={previewResult.has_handwriting
|
||
? isDark ? 'text-orange-300' : 'text-orange-600'
|
||
: isDark ? 'text-green-300' : 'text-green-600'
|
||
}>
|
||
{previewResult.has_handwriting ? 'Ja' : 'Nein'}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Konfidenz:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
{(previewResult.confidence * 100).toFixed(0)}%
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Handschrift-Anteil:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
{(previewResult.handwriting_ratio * 100).toFixed(1)}%
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Bildgröße:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
{previewResult.image_width} × {previewResult.image_height}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Time Estimates */}
|
||
<div className={`p-4 rounded-xl border ${cardStyle}`}>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Geschätzte Zeit
|
||
</h3>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Erkennung:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
~{Math.round(previewResult.estimated_times_ms.detection / 1000)}s
|
||
</span>
|
||
</div>
|
||
{removeHandwriting && previewResult.has_handwriting && (
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Bereinigung:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
~{Math.round(previewResult.estimated_times_ms.inpainting / 1000)}s
|
||
</span>
|
||
</div>
|
||
)}
|
||
{reconstructLayout && (
|
||
<div className="flex justify-between">
|
||
<span className={labelStyle}>Layout:</span>
|
||
<span className={isDark ? 'text-white' : 'text-slate-900'}>
|
||
~{Math.round(previewResult.estimated_times_ms.reconstruction / 1000)}s
|
||
</span>
|
||
</div>
|
||
)}
|
||
<div className={`flex justify-between pt-2 border-t ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
|
||
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>Gesamt:</span>
|
||
<span className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
~{Math.round(previewResult.estimated_times_ms.total / 1000)}s
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview Images */}
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Original
|
||
</h3>
|
||
{previewUrl && <img src={previewUrl} alt="Original" className="w-full rounded-xl shadow-lg" />}
|
||
</div>
|
||
<div>
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>Maske</h3>
|
||
<button
|
||
onClick={handleGetMask}
|
||
className={`text-sm px-3 py-1 rounded-lg ${
|
||
isDark ? 'bg-white/10 text-white/70 hover:bg-white/20' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
Maske laden
|
||
</button>
|
||
</div>
|
||
{maskUrl ? (
|
||
<img src={maskUrl} alt="Mask" className="w-full rounded-xl shadow-lg" />
|
||
) : (
|
||
<div className={`aspect-video rounded-xl flex items-center justify-center ${isDark ? 'bg-white/5' : 'bg-slate-100'}`}>
|
||
<span className={labelStyle}>Klicke "Maske laden" zum Anzeigen</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// Step 3: Result
|
||
// =============================================================================
|
||
|
||
interface ResultStepProps {
|
||
isDark: boolean
|
||
labelStyle: string
|
||
cardStyle: string
|
||
pipelineResult: PipelineResult
|
||
previewUrl: string | null
|
||
cleanedUrl: string | null
|
||
}
|
||
|
||
export function ResultStep({
|
||
isDark, labelStyle, cardStyle,
|
||
pipelineResult, previewUrl, cleanedUrl,
|
||
}: ResultStepProps) {
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Status */}
|
||
<div className={`p-4 rounded-xl ${
|
||
pipelineResult.success
|
||
? isDark ? 'bg-green-500/20' : 'bg-green-50'
|
||
: isDark ? 'bg-red-500/20' : 'bg-red-50'
|
||
}`}>
|
||
<div className="flex items-center gap-3">
|
||
{pipelineResult.success ? (
|
||
<svg className={`w-6 h-6 ${isDark ? 'text-green-300' : 'text-green-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
) : (
|
||
<svg className={`w-6 h-6 ${isDark ? 'text-red-300' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
)}
|
||
<div>
|
||
<h3 className={`font-medium ${
|
||
pipelineResult.success
|
||
? isDark ? 'text-green-300' : 'text-green-700'
|
||
: isDark ? 'text-red-300' : 'text-red-700'
|
||
}`}>
|
||
{pipelineResult.success ? 'Erfolgreich bereinigt' : 'Bereinigung fehlgeschlagen'}
|
||
</h3>
|
||
<p className={`text-sm ${labelStyle}`}>
|
||
{pipelineResult.handwriting_detected
|
||
? pipelineResult.handwriting_removed
|
||
? 'Handschrift wurde erkannt und entfernt'
|
||
: 'Handschrift erkannt, aber nicht entfernt'
|
||
: 'Keine Handschrift gefunden'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Result Images */}
|
||
<div className="grid grid-cols-2 gap-6">
|
||
<div>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>Original</h3>
|
||
{previewUrl && <img src={previewUrl} alt="Original" className="w-full rounded-xl shadow-lg" />}
|
||
</div>
|
||
<div>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>Bereinigt</h3>
|
||
{cleanedUrl ? (
|
||
<img src={cleanedUrl} alt="Cleaned" className="w-full rounded-xl shadow-lg" />
|
||
) : (
|
||
<div className={`aspect-video rounded-xl flex items-center justify-center ${isDark ? 'bg-white/5' : 'bg-slate-100'}`}>
|
||
<span className={labelStyle}>Kein Bild</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Layout Info */}
|
||
{pipelineResult.layout_reconstructed && pipelineResult.metadata?.layout && (
|
||
<div className={`p-4 rounded-xl border ${cardStyle}`}>
|
||
<h3 className={`font-medium mb-3 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
Layout-Rekonstruktion
|
||
</h3>
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div>
|
||
<span className={labelStyle}>Elemente:</span>
|
||
<p className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{pipelineResult.metadata.layout.element_count}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className={labelStyle}>Tabellen:</span>
|
||
<p className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{pipelineResult.metadata.layout.table_count}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<span className={labelStyle}>Größe:</span>
|
||
<p className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||
{pipelineResult.metadata.layout.page_width} × {pipelineResult.metadata.layout.page_height}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|