Files
breakpilot-lehrer/studio-v2/app/worksheet-cleanup/page.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

900 lines
38 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { useTheme } from '@/lib/ThemeContext'
import { Sidebar } from '@/components/Sidebar'
import { ThemeToggle } from '@/components/ThemeToggle'
import { LanguageDropdown } from '@/components/LanguageDropdown'
import { QRCodeUpload, UploadedFile } from '@/components/QRCodeUpload'
// LocalStorage Key for upload session
const SESSION_ID_KEY = 'bp_cleanup_session'
/**
* Worksheet Cleanup Page - Apple Weather Dashboard Style
*
* Design principles:
* - Dark gradient background
* - Ultra-translucent glass cards (~8% opacity)
* - White text, monochrome palette
* - Step-by-step cleanup wizard
*/
// =============================================================================
// GLASS CARD - Ultra Transparent
// =============================================================================
interface GlassCardProps {
children: React.ReactNode
className?: string
onClick?: () => void
size?: 'sm' | 'md' | 'lg'
delay?: number
}
function GlassCard({ children, className = '', onClick, size = 'md', delay = 0, isDark = true }: GlassCardProps & { isDark?: boolean }) {
const [isVisible, setIsVisible] = useState(false)
const [isHovered, setIsHovered] = useState(false)
useEffect(() => {
const timer = setTimeout(() => setIsVisible(true), delay)
return () => clearTimeout(timer)
}, [delay])
const sizeClasses = {
sm: 'p-4',
md: 'p-5',
lg: 'p-6',
}
return (
<div
className={`
rounded-3xl
${sizeClasses[size]}
${onClick ? 'cursor-pointer' : ''}
${className}
`}
style={{
background: isDark
? (isHovered ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255, 255, 255, 0.08)')
: (isHovered ? 'rgba(255, 255, 255, 0.9)' : 'rgba(255, 255, 255, 0.7)'),
backdropFilter: 'blur(24px) saturate(180%)',
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
border: isDark ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid rgba(0, 0, 0, 0.1)',
boxShadow: isDark
? '0 4px 24px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.05)'
: '0 4px 24px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.5)',
opacity: isVisible ? 1 : 0,
transform: isVisible
? `translateY(0) scale(${isHovered ? 1.01 : 1})`
: 'translateY(20px)',
transition: 'all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1)',
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
>
{children}
</div>
)
}
// =============================================================================
// PROGRESS RING
// =============================================================================
interface ProgressRingProps {
progress: number
size?: number
strokeWidth?: number
label: string
value: string
color?: string
}
function ProgressRing({
progress,
size = 80,
strokeWidth = 6,
label,
value,
color = '#a78bfa'
}: ProgressRingProps) {
const radius = (size - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (progress / 100) * circumference
return (
<div className="flex flex-col items-center">
<div className="relative" style={{ width: size, height: size }}>
<svg className="transform -rotate-90" width={size} height={size}>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="rgba(255, 255, 255, 0.1)"
strokeWidth={strokeWidth}
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
style={{ transition: 'stroke-dashoffset 0.5s ease' }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-lg font-bold text-white">{value}</span>
</div>
</div>
<span className="mt-2 text-xs text-white/50">{label}</span>
</div>
)
}
// =============================================================================
// TYPES
// =============================================================================
interface PreviewResult {
has_handwriting: boolean
confidence: number
handwriting_ratio: number
image_width: number
image_height: number
estimated_times_ms: {
detection: number
inpainting: number
reconstruction: number
total: number
}
}
interface PipelineResult {
success: boolean
handwriting_detected: boolean
handwriting_removed: boolean
layout_reconstructed: boolean
cleaned_image_base64?: string
fabric_json?: any
metadata: any
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function WorksheetCleanupPage() {
const { isDark } = useTheme()
const router = useRouter()
// File state
const [file, setFile] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [cleanedUrl, setCleanedUrl] = useState<string | null>(null)
const [maskUrl, setMaskUrl] = useState<string | null>(null)
// Loading states
const [isPreviewing, setIsPreviewing] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState<string | null>(null)
// Results
const [previewResult, setPreviewResult] = useState<PreviewResult | null>(null)
const [pipelineResult, setPipelineResult] = useState<PipelineResult | null>(null)
// Options
const [removeHandwriting, setRemoveHandwriting] = useState(true)
const [reconstructLayout, setReconstructLayout] = useState(true)
const [inpaintingMethod, setInpaintingMethod] = useState<string>('auto')
// Step tracking
const [currentStep, setCurrentStep] = useState<'upload' | 'preview' | 'processing' | 'result'>('upload')
// QR Code Upload
const [showQRModal, setShowQRModal] = useState(false)
const [uploadSessionId, setUploadSessionId] = useState('')
const [mobileUploadedFiles, setMobileUploadedFiles] = useState<UploadedFile[]>([])
// Format file size
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
// Initialize upload session ID
useEffect(() => {
let storedSessionId = localStorage.getItem(SESSION_ID_KEY)
if (!storedSessionId) {
storedSessionId = `cleanup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
localStorage.setItem(SESSION_ID_KEY, storedSessionId)
}
setUploadSessionId(storedSessionId)
}, [])
const getApiUrl = useCallback(() => {
if (typeof window === 'undefined') return 'http://localhost:8086'
const { hostname, protocol } = window.location
return hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
}, [])
// Handle file selection
const handleFileSelect = useCallback((selectedFile: File) => {
setFile(selectedFile)
setError(null)
setPreviewResult(null)
setPipelineResult(null)
setCleanedUrl(null)
setMaskUrl(null)
const url = URL.createObjectURL(selectedFile)
setPreviewUrl(url)
setCurrentStep('upload')
}, [])
// Handle mobile file selection - convert to File and trigger handleFileSelect
const handleMobileFileSelect = useCallback(async (uploadedFile: UploadedFile) => {
try {
const base64Data = uploadedFile.dataUrl.split(',')[1]
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: uploadedFile.type })
const file = new File([blob], uploadedFile.name, { type: uploadedFile.type })
handleFileSelect(file)
setShowQRModal(false)
} catch (error) {
console.error('Failed to convert mobile file:', error)
setError('Fehler beim Laden der Datei vom Handy')
}
}, [handleFileSelect])
// Handle drop
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
const droppedFile = e.dataTransfer.files[0]
if (droppedFile && droppedFile.type.startsWith('image/')) {
handleFileSelect(droppedFile)
}
}, [handleFileSelect])
// Preview cleanup
const handlePreview = useCallback(async () => {
if (!file) return
setIsPreviewing(true)
setError(null)
try {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/preview-cleanup`, {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const result = await response.json()
setPreviewResult(result)
setCurrentStep('preview')
} catch (err) {
console.error('Preview failed:', err)
setError(err instanceof Error ? err.message : 'Vorschau fehlgeschlagen')
} finally {
setIsPreviewing(false)
}
}, [file, getApiUrl])
// Run full cleanup pipeline
const handleCleanup = useCallback(async () => {
if (!file) return
setIsProcessing(true)
setCurrentStep('processing')
setError(null)
try {
const formData = new FormData()
formData.append('image', file)
formData.append('remove_handwriting', String(removeHandwriting))
formData.append('reconstruct', String(reconstructLayout))
formData.append('inpainting_method', inpaintingMethod)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/cleanup-pipeline`, {
method: 'POST',
body: formData
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(errorData.detail || `HTTP ${response.status}`)
}
const result: PipelineResult = await response.json()
setPipelineResult(result)
// Create cleaned image URL
if (result.cleaned_image_base64) {
const cleanedBlob = await fetch(`data:image/png;base64,${result.cleaned_image_base64}`).then(r => r.blob())
setCleanedUrl(URL.createObjectURL(cleanedBlob))
}
setCurrentStep('result')
} catch (err) {
console.error('Cleanup failed:', err)
setError(err instanceof Error ? err.message : 'Bereinigung fehlgeschlagen')
setCurrentStep('preview')
} finally {
setIsProcessing(false)
}
}, [file, removeHandwriting, reconstructLayout, inpaintingMethod, getApiUrl])
// Get detection mask
const handleGetMask = useCallback(async () => {
if (!file) return
try {
const formData = new FormData()
formData.append('image', file)
const response = await fetch(`${getApiUrl()}/api/v1/worksheet/detect-handwriting/mask`, {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const blob = await response.blob()
setMaskUrl(URL.createObjectURL(blob))
} catch (err) {
console.error('Mask fetch failed:', err)
}
}, [file, getApiUrl])
// Open in worksheet editor
const handleOpenInEditor = useCallback(() => {
if (pipelineResult?.fabric_json) {
// Store the fabric JSON in sessionStorage
sessionStorage.setItem('worksheetCleanupResult', JSON.stringify(pipelineResult.fabric_json))
router.push('/worksheet-editor')
}
}, [pipelineResult, router])
// Reset to start
const handleReset = useCallback(() => {
setFile(null)
setPreviewUrl(null)
setCleanedUrl(null)
setMaskUrl(null)
setPreviewResult(null)
setPipelineResult(null)
setError(null)
setCurrentStep('upload')
}, [])
return (
<div className={`min-h-screen flex relative overflow-hidden ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-blue-50 to-indigo-100'
}`}>
{/* Animated Background Blobs */}
<div className={`absolute -top-40 -right-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${isDark ? 'bg-purple-500 opacity-30' : 'bg-purple-300 opacity-40'}`} />
<div className={`absolute top-1/2 -left-40 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${isDark ? 'bg-pink-500 opacity-30' : 'bg-pink-300 opacity-40'}`} />
<div className={`absolute -bottom-40 right-1/3 w-96 h-96 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${isDark ? 'bg-blue-500 opacity-30' : 'bg-blue-300 opacity-40'}`} />
{/* Sidebar */}
<div className="relative z-10 p-4">
<Sidebar />
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col relative z-10 p-6 overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className={`text-3xl font-bold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>Arbeitsblatt bereinigen</h1>
<p className={isDark ? 'text-white/50' : 'text-slate-500'}>Handschrift entfernen und Layout rekonstruieren</p>
</div>
<div className="flex items-center gap-3">
<ThemeToggle />
<LanguageDropdown />
</div>
</div>
{/* Step Indicator */}
<div className="flex items-center justify-center gap-4 mb-8">
{['upload', 'preview', 'processing', 'result'].map((step, idx) => (
<div key={step} className="flex items-center">
<div className={`
w-10 h-10 rounded-full flex items-center justify-center font-medium transition-all
${currentStep === step
? 'bg-purple-500 text-white shadow-lg shadow-purple-500/50'
: ['upload', 'preview', 'processing', 'result'].indexOf(currentStep) > idx
? 'bg-green-500 text-white'
: isDark ? 'bg-white/10 text-white/40' : 'bg-slate-200 text-slate-400'
}
`}>
{['upload', 'preview', 'processing', 'result'].indexOf(currentStep) > idx ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
idx + 1
)}
</div>
{idx < 3 && (
<div className={`w-16 h-0.5 mx-2 ${
['upload', 'preview', 'processing', 'result'].indexOf(currentStep) > idx
? 'bg-green-500'
: isDark ? 'bg-white/20' : 'bg-slate-300'
}`} />
)}
</div>
))}
</div>
{/* Error Display */}
{error && (
<GlassCard className="mb-6" size="sm" isDark={isDark}>
<div className="flex items-center gap-3 text-red-400">
<svg className="w-6 h-6" 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>
<span>{error}</span>
</div>
</GlassCard>
)}
{/* Content based on step */}
<div className="flex-1">
{/* Step 1: Upload */}
{currentStep === 'upload' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upload Options - File and QR Code side by side */}
<GlassCard className="col-span-1" delay={100}>
<div
className="border-2 border-dashed border-white/20 rounded-2xl p-8 text-center cursor-pointer transition-all hover:border-purple-400/50 hover:bg-white/5 min-h-[280px] flex flex-col items-center justify-center"
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-40 mx-auto rounded-xl shadow-2xl"
/>
<p className="text-white font-medium text-sm">{file?.name}</p>
<p className="text-white/50 text-xs">Klicke zum Ändern</p>
</div>
) : (
<>
<svg className="w-16 h-16 mx-auto mb-4 text-white/30" 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-xl font-semibold text-white mb-2">Datei auswählen</p>
<p className="text-white/50 text-sm mb-2">Ziehe ein Bild hierher oder klicke</p>
<p className="text-white/30 text-xs">PNG, JPG, JPEG</p>
</>
)}
</div>
</GlassCard>
{/* QR Code Upload */}
<GlassCard className="col-span-1" delay={150}>
<div
className="border-2 border-dashed border-white/20 rounded-2xl p-8 text-center cursor-pointer transition-all hover:border-purple-400/50 hover:bg-white/5 min-h-[280px] flex flex-col items-center justify-center"
onClick={() => setShowQRModal(true)}
>
<div className="w-16 h-16 mx-auto mb-4 rounded-xl bg-purple-500/20 flex items-center justify-center">
<span className="text-3xl">📱</span>
</div>
<p className="text-xl font-semibold text-white mb-2">Mit Handy scannen</p>
<p className="text-white/50 text-sm mb-2">QR-Code scannen um Foto hochzuladen</p>
<p className="text-white/30 text-xs">Im lokalen Netzwerk</p>
</div>
</GlassCard>
{/* Options */}
{file && (
<>
<GlassCard delay={200}>
<h3 className="text-lg font-semibold text-white mb-4">Optionen</h3>
<div className="space-y-4">
<label className="flex items-center gap-4 cursor-pointer group">
<input
type="checkbox"
checked={removeHandwriting}
onChange={(e) => setRemoveHandwriting(e.target.checked)}
className="w-5 h-5 rounded bg-white/10 border-white/20 text-purple-500 focus:ring-purple-500"
/>
<div>
<span className="text-white font-medium group-hover:text-purple-300 transition-colors">
Handschrift entfernen
</span>
<p className="text-white/40 text-sm">Erkennt und entfernt handgeschriebene Inhalte</p>
</div>
</label>
<label className="flex items-center gap-4 cursor-pointer group">
<input
type="checkbox"
checked={reconstructLayout}
onChange={(e) => setReconstructLayout(e.target.checked)}
className="w-5 h-5 rounded bg-white/10 border-white/20 text-purple-500 focus:ring-purple-500"
/>
<div>
<span className="text-white font-medium group-hover:text-purple-300 transition-colors">
Layout rekonstruieren
</span>
<p className="text-white/40 text-sm">Erstellt bearbeitbare Textblöcke</p>
</div>
</label>
</div>
</GlassCard>
<GlassCard delay={300}>
<h3 className="text-lg font-semibold text-white mb-4">Methode</h3>
<select
value={inpaintingMethod}
onChange={(e) => setInpaintingMethod(e.target.value)}
className="w-full p-3 rounded-xl bg-white/10 border border-white/20 text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="auto">Automatisch (empfohlen)</option>
<option value="opencv_telea">OpenCV Telea (schnell)</option>
<option value="opencv_ns">OpenCV NS (glatter)</option>
</select>
<p className="text-white/40 text-sm mt-3">
Die automatische Methode wählt die beste Option basierend auf dem Bildinhalt.
</p>
</GlassCard>
{/* Action Button */}
<div className="col-span-1 lg:col-span-2 flex justify-center">
<button
onClick={handlePreview}
disabled={isPreviewing}
className="px-8 py-4 rounded-2xl font-semibold text-lg bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg hover:shadow-purple-500/30 transition-all disabled:opacity-50 flex items-center gap-3"
>
{isPreviewing ? (
<>
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
Analysiere...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Vorschau
</>
)}
</button>
</div>
</>
)}
</div>
)}
{/* Step 2: Preview */}
{currentStep === 'preview' && previewResult && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Stats */}
<GlassCard delay={100}>
<h3 className="text-lg font-semibold text-white mb-6">Analyse</h3>
<div className="flex justify-around">
<ProgressRing
progress={previewResult.confidence * 100}
label="Konfidenz"
value={`${Math.round(previewResult.confidence * 100)}%`}
color={previewResult.has_handwriting ? '#f97316' : '#22c55e'}
/>
<ProgressRing
progress={previewResult.handwriting_ratio * 100 * 10}
label="Handschrift"
value={`${(previewResult.handwriting_ratio * 100).toFixed(1)}%`}
color="#a78bfa"
/>
</div>
<div className={`mt-6 p-4 rounded-xl text-center ${
previewResult.has_handwriting
? 'bg-orange-500/20 text-orange-300'
: 'bg-green-500/20 text-green-300'
}`}>
{previewResult.has_handwriting
? 'Handschrift erkannt'
: 'Keine Handschrift gefunden'}
</div>
</GlassCard>
{/* Time Estimates */}
<GlassCard delay={200}>
<h3 className="text-lg font-semibold text-white mb-4">Geschätzte Zeit</h3>
<div className="space-y-3">
<div className="flex justify-between text-white/70">
<span>Erkennung</span>
<span className="text-white">~{Math.round(previewResult.estimated_times_ms.detection / 1000)}s</span>
</div>
{removeHandwriting && previewResult.has_handwriting && (
<div className="flex justify-between text-white/70">
<span>Bereinigung</span>
<span className="text-white">~{Math.round(previewResult.estimated_times_ms.inpainting / 1000)}s</span>
</div>
)}
{reconstructLayout && (
<div className="flex justify-between text-white/70">
<span>Layout</span>
<span className="text-white">~{Math.round(previewResult.estimated_times_ms.reconstruction / 1000)}s</span>
</div>
)}
<div className="flex justify-between pt-3 border-t border-white/10 font-medium">
<span className="text-white">Gesamt</span>
<span className="text-purple-300">~{Math.round(previewResult.estimated_times_ms.total / 1000)}s</span>
</div>
</div>
</GlassCard>
{/* Image Info */}
<GlassCard delay={300}>
<h3 className="text-lg font-semibold text-white mb-4">Bild-Info</h3>
<div className="space-y-3">
<div className="flex justify-between text-white/70">
<span>Breite</span>
<span className="text-white">{previewResult.image_width}px</span>
</div>
<div className="flex justify-between text-white/70">
<span>Höhe</span>
<span className="text-white">{previewResult.image_height}px</span>
</div>
<div className="flex justify-between text-white/70">
<span>Pixel</span>
<span className="text-white">{(previewResult.image_width * previewResult.image_height / 1000000).toFixed(1)}MP</span>
</div>
</div>
<button
onClick={handleGetMask}
className="w-full mt-4 px-4 py-2 rounded-xl bg-white/10 text-white/70 hover:bg-white/20 hover:text-white transition-all text-sm"
>
Maske anzeigen
</button>
</GlassCard>
{/* Preview Images */}
<GlassCard className="col-span-1 lg:col-span-2" delay={400}>
<h3 className="text-lg font-semibold text-white mb-4">Original</h3>
{previewUrl && (
<img
src={previewUrl}
alt="Original"
className="w-full max-h-96 object-contain rounded-xl"
/>
)}
</GlassCard>
{maskUrl && (
<GlassCard delay={500}>
<h3 className="text-lg font-semibold text-white mb-4">Maske</h3>
<img
src={maskUrl}
alt="Mask"
className="w-full max-h-96 object-contain rounded-xl"
/>
</GlassCard>
)}
{/* Actions */}
<div className="col-span-1 lg:col-span-3 flex justify-center gap-4">
<button
onClick={() => setCurrentStep('upload')}
className="px-6 py-3 rounded-xl bg-white/10 text-white hover:bg-white/20 transition-all flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Zurück
</button>
<button
onClick={handleCleanup}
disabled={isProcessing}
className="px-8 py-3 rounded-xl font-semibold bg-gradient-to-r from-orange-500 to-red-500 text-white hover:shadow-lg hover:shadow-orange-500/30 transition-all disabled:opacity-50 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
Bereinigen starten
</button>
</div>
</div>
)}
{/* Step 3: Processing */}
{currentStep === 'processing' && (
<div className="flex flex-col items-center justify-center py-20">
<GlassCard className="text-center max-w-md" delay={0}>
<div className="w-20 h-20 mx-auto mb-6 relative">
<div className="absolute inset-0 rounded-full border-4 border-white/10"></div>
<div className="absolute inset-0 rounded-full border-4 border-purple-500 border-t-transparent animate-spin"></div>
</div>
<h3 className="text-xl font-semibold text-white mb-2">Verarbeite Bild...</h3>
<p className="text-white/50">
{removeHandwriting ? 'Handschrift wird erkannt und entfernt' : 'Bild wird analysiert'}
</p>
</GlassCard>
</div>
)}
{/* Step 4: Result */}
{currentStep === 'result' && pipelineResult && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Status */}
<GlassCard className="col-span-1 lg:col-span-2" delay={100}>
<div className={`flex items-center gap-4 ${
pipelineResult.success ? 'text-green-300' : 'text-red-300'
}`}>
<div className={`w-12 h-12 rounded-full flex items-center justify-center ${
pipelineResult.success ? 'bg-green-500/20' : 'bg-red-500/20'
}`}>
{pipelineResult.success ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div>
<h3 className="text-xl font-semibold">
{pipelineResult.success ? 'Erfolgreich bereinigt!' : 'Verarbeitung fehlgeschlagen'}
</h3>
<p className="text-white/50">
{pipelineResult.handwriting_removed
? `Handschrift wurde entfernt. ${pipelineResult.metadata?.layout?.element_count || 0} Elemente erkannt.`
: pipelineResult.handwriting_detected
? 'Handschrift erkannt, aber nicht entfernt'
: 'Keine Handschrift im Bild gefunden'}
</p>
</div>
</div>
</GlassCard>
{/* Original */}
<GlassCard delay={200}>
<h3 className="text-lg font-semibold text-white mb-4">Original</h3>
{previewUrl && (
<img
src={previewUrl}
alt="Original"
className="w-full rounded-xl"
/>
)}
</GlassCard>
{/* Cleaned */}
<GlassCard delay={300}>
<h3 className="text-lg font-semibold text-white mb-4">Bereinigt</h3>
{cleanedUrl ? (
<img
src={cleanedUrl}
alt="Cleaned"
className="w-full rounded-xl"
/>
) : (
<div className="aspect-video rounded-xl bg-white/5 flex items-center justify-center text-white/40">
Kein Bild
</div>
)}
</GlassCard>
{/* Actions */}
<div className="col-span-1 lg:col-span-2 flex justify-center gap-4">
<button
onClick={handleReset}
className="px-6 py-3 rounded-xl bg-white/10 text-white hover:bg-white/20 transition-all flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
Neues Bild
</button>
{cleanedUrl && (
<a
href={cleanedUrl}
download="bereinigt.png"
className="px-6 py-3 rounded-xl bg-white/10 text-white hover:bg-white/20 transition-all flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Download
</a>
)}
{pipelineResult.layout_reconstructed && pipelineResult.fabric_json && (
<button
onClick={handleOpenInEditor}
className="px-8 py-3 rounded-xl font-semibold bg-gradient-to-r from-green-500 to-emerald-500 text-white hover:shadow-lg hover:shadow-green-500/30 transition-all flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Im Editor öffnen
</button>
)}
</div>
</div>
)}
</div>
</div>
{/* QR Code Modal */}
{showQRModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowQRModal(false)} />
<div className="relative w-full max-w-md rounded-3xl bg-slate-900">
<QRCodeUpload
sessionId={uploadSessionId}
onClose={() => setShowQRModal(false)}
onFilesChanged={(files) => {
setMobileUploadedFiles(files)
}}
/>
{/* Select button for mobile files */}
{mobileUploadedFiles.length > 0 && (
<div className="p-4 border-t border-white/10">
<p className="text-white/60 text-sm mb-3">Datei auswählen:</p>
<div className="space-y-2 max-h-40 overflow-y-auto">
{mobileUploadedFiles.map((file) => (
<button
key={file.id}
onClick={() => handleMobileFileSelect(file)}
className="w-full flex items-center gap-3 p-3 rounded-xl text-left transition-all bg-white/5 hover:bg-white/10 border border-white/10"
>
<span className="text-xl">{file.type.startsWith('image/') ? '🖼️' : '📄'}</span>
<div className="flex-1 min-w-0">
<p className="text-white font-medium truncate">{file.name}</p>
<p className="text-white/50 text-xs">{formatFileSize(file.size)}</p>
</div>
<span className="text-purple-400 text-sm">Verwenden </span>
</button>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
)
}