'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 (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} onClick={onClick} > {children}
) } // ============================================================================= // 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 (
{value}
{label}
) } // ============================================================================= // 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(null) const [previewUrl, setPreviewUrl] = useState(null) const [cleanedUrl, setCleanedUrl] = useState(null) const [maskUrl, setMaskUrl] = useState(null) // Loading states const [isPreviewing, setIsPreviewing] = useState(false) const [isProcessing, setIsProcessing] = useState(false) const [error, setError] = useState(null) // Results const [previewResult, setPreviewResult] = useState(null) const [pipelineResult, setPipelineResult] = useState(null) // Options const [removeHandwriting, setRemoveHandwriting] = useState(true) const [reconstructLayout, setReconstructLayout] = useState(true) const [inpaintingMethod, setInpaintingMethod] = useState('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([]) // 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 (
{/* Animated Background Blobs */}
{/* Sidebar */}
{/* Main Content */}
{/* Header */}

Arbeitsblatt bereinigen

Handschrift entfernen und Layout rekonstruieren

{/* Step Indicator */}
{['upload', 'preview', 'processing', 'result'].map((step, idx) => (
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 ? ( ) : ( idx + 1 )}
{idx < 3 && (
idx ? 'bg-green-500' : isDark ? 'bg-white/20' : 'bg-slate-300' }`} /> )}
))}
{/* Error Display */} {error && (
{error}
)} {/* Content based on step */}
{/* Step 1: Upload */} {currentStep === 'upload' && (
{/* Upload Options - File and QR Code side by side */}
e.preventDefault()} onClick={() => document.getElementById('file-input')?.click()} > e.target.files?.[0] && handleFileSelect(e.target.files[0])} className="hidden" /> {previewUrl ? (
Preview

{file?.name}

Klicke zum Ändern

) : ( <>

Datei auswählen

Ziehe ein Bild hierher oder klicke

PNG, JPG, JPEG

)}
{/* QR Code Upload */}
setShowQRModal(true)} >
📱

Mit Handy scannen

QR-Code scannen um Foto hochzuladen

Im lokalen Netzwerk

{/* Options */} {file && ( <>

Optionen

Methode

Die automatische Methode wählt die beste Option basierend auf dem Bildinhalt.

{/* Action Button */}
)}
)} {/* Step 2: Preview */} {currentStep === 'preview' && previewResult && (
{/* Stats */}

Analyse

{previewResult.has_handwriting ? 'Handschrift erkannt' : 'Keine Handschrift gefunden'}
{/* Time Estimates */}

Geschätzte Zeit

Erkennung ~{Math.round(previewResult.estimated_times_ms.detection / 1000)}s
{removeHandwriting && previewResult.has_handwriting && (
Bereinigung ~{Math.round(previewResult.estimated_times_ms.inpainting / 1000)}s
)} {reconstructLayout && (
Layout ~{Math.round(previewResult.estimated_times_ms.reconstruction / 1000)}s
)}
Gesamt ~{Math.round(previewResult.estimated_times_ms.total / 1000)}s
{/* Image Info */}

Bild-Info

Breite {previewResult.image_width}px
Höhe {previewResult.image_height}px
Pixel {(previewResult.image_width * previewResult.image_height / 1000000).toFixed(1)}MP
{/* Preview Images */}

Original

{previewUrl && ( Original )}
{maskUrl && (

Maske

Mask
)} {/* Actions */}
)} {/* Step 3: Processing */} {currentStep === 'processing' && (

Verarbeite Bild...

{removeHandwriting ? 'Handschrift wird erkannt und entfernt' : 'Bild wird analysiert'}

)} {/* Step 4: Result */} {currentStep === 'result' && pipelineResult && (
{/* Status */}
{pipelineResult.success ? ( ) : ( )}

{pipelineResult.success ? 'Erfolgreich bereinigt!' : 'Verarbeitung fehlgeschlagen'}

{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'}

{/* Original */}

Original

{previewUrl && ( Original )}
{/* Cleaned */}

Bereinigt

{cleanedUrl ? ( Cleaned ) : (
Kein Bild
)}
{/* Actions */}
{cleanedUrl && ( Download )} {pipelineResult.layout_reconstructed && pipelineResult.fabric_json && ( )}
)}
{/* QR Code Modal */} {showQRModal && (
setShowQRModal(false)} />
setShowQRModal(false)} onFilesChanged={(files) => { setMobileUploadedFiles(files) }} /> {/* Select button for mobile files */} {mobileUploadedFiles.length > 0 && (

Datei auswählen:

{mobileUploadedFiles.map((file) => ( ))}
)}
)}
) }