'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 (
)
}
// =============================================================================
// 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 && (
)}
{/* 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 ? (
{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
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"
>
Automatisch (empfohlen)
OpenCV Telea (schnell)
OpenCV NS (glatter)
Die automatische Methode wählt die beste Option basierend auf dem Bildinhalt.
{/* Action Button */}
{isPreviewing ? (
<>
Analysiere...
>
) : (
<>
Vorschau
>
)}
>
)}
)}
{/* 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
Maske anzeigen
{/* Preview Images */}
Original
{previewUrl && (
)}
{maskUrl && (
Maske
)}
{/* Actions */}
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"
>
Zurück
Bereinigen starten
)}
{/* 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 && (
)}
{/* Cleaned */}
Bereinigt
{cleanedUrl ? (
) : (
Kein Bild
)}
{/* Actions */}
Neues Bild
{cleanedUrl && (
Download
)}
{pipelineResult.layout_reconstructed && pipelineResult.fabric_json && (
Im Editor öffnen
)}
)}
{/* 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) => (
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"
>
{file.type.startsWith('image/') ? '🖼️' : '📄'}
{file.name}
{formatFileSize(file.size)}
Verwenden →
))}
)}
)}
)
}