'use client' import { useState, useCallback, useEffect } from 'react' interface UploadedFile { id: string name: string size: number progress: number status: 'pending' | 'uploading' | 'complete' | 'error' error?: string } function 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] } const CHUNK_SIZE = 5 * 1024 * 1024 // 5 MB chunks export default function UploadPage() { const [files, setFiles] = useState([]) const [isDragging, setIsDragging] = useState(false) const [serverUrl, setServerUrl] = useState('') const [uploadDestination, setUploadDestination] = useState<'klausur' | 'rag'>('klausur') useEffect(() => { // Detect server URL from current location if (typeof window !== 'undefined') { const hostname = window.location.hostname // If localhost or local IP, use klausur-service port if (hostname === 'localhost' || hostname.startsWith('192.168.')) { setServerUrl(`http://${hostname}:8086`) } else { setServerUrl('/api/klausur') } } }, []) const uploadChunked = async (file: File, fileId: string) => { const totalChunks = Math.ceil(file.size / CHUNK_SIZE) let uploadedChunks = 0 // Create upload session const sessionResponse = await fetch(`${serverUrl}/api/v1/upload/init`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: file.name, filesize: file.size, chunks: totalChunks, destination: uploadDestination }) }) if (!sessionResponse.ok) { throw new Error('Konnte Upload-Session nicht starten') } const { upload_id } = await sessionResponse.json() // Upload chunks for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * CHUNK_SIZE const end = Math.min(start + CHUNK_SIZE, file.size) const chunk = file.slice(start, end) const formData = new FormData() formData.append('chunk', chunk) formData.append('upload_id', upload_id) formData.append('chunk_index', chunkIndex.toString()) const chunkResponse = await fetch(`${serverUrl}/api/v1/upload/chunk`, { method: 'POST', body: formData }) if (!chunkResponse.ok) { throw new Error(`Fehler beim Hochladen von Teil ${chunkIndex + 1}`) } uploadedChunks++ const progress = Math.round((uploadedChunks / totalChunks) * 100) setFiles(prev => prev.map(f => f.id === fileId ? { ...f, progress } : f )) } // Finalize upload const finalizeFormData = new FormData() finalizeFormData.append('upload_id', upload_id) const finalizeResponse = await fetch(`${serverUrl}/api/v1/upload/finalize`, { method: 'POST', body: finalizeFormData }) if (!finalizeResponse.ok) { throw new Error('Fehler beim Abschliessen des Uploads') } return await finalizeResponse.json() } const uploadFile = async (file: File) => { const fileId = Math.random().toString(36).substr(2, 9) setFiles(prev => [...prev, { id: fileId, name: file.name, size: file.size, progress: 0, status: 'pending' }]) try { setFiles(prev => prev.map(f => f.id === fileId ? { ...f, status: 'uploading' } : f )) // For large files (>10MB), use chunked upload if (file.size > 10 * 1024 * 1024) { await uploadChunked(file, fileId) } else { // Simple upload for smaller files const formData = new FormData() formData.append('file', file) formData.append('destination', uploadDestination) const response = await fetch(`${serverUrl}/api/v1/upload/simple`, { method: 'POST', body: formData }) if (!response.ok) { throw new Error('Upload fehlgeschlagen') } setFiles(prev => prev.map(f => f.id === fileId ? { ...f, progress: 100 } : f )) } setFiles(prev => prev.map(f => f.id === fileId ? { ...f, status: 'complete', progress: 100 } : f )) } catch (error) { setFiles(prev => prev.map(f => f.id === fileId ? { ...f, status: 'error', error: error instanceof Error ? error.message : 'Unbekannter Fehler' } : f )) } } const handleFiles = useCallback((fileList: FileList | File[]) => { const newFiles = Array.from(fileList).filter(f => f.type === 'application/pdf') newFiles.forEach(file => uploadFile(file)) }, [serverUrl, uploadDestination]) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) handleFiles(e.dataTransfer.files) }, [handleFiles]) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(true) }, []) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault() setIsDragging(false) }, []) const handleFileSelect = useCallback((e: React.ChangeEvent) => { if (e.target.files) { handleFiles(e.target.files) } }, [handleFiles]) const removeFile = (fileId: string) => { setFiles(prev => prev.filter(f => f.id !== fileId)) } const completedCount = files.filter(f => f.status === 'complete').length const totalSize = files.reduce((sum, f) => sum + f.size, 0) return (
{/* Header */}

BreakPilot Upload

DSGVO-konform
{/* Destination Selector */}
{/* Drop Zone */}

PDF-Dateien hochladen

Tippen zum Auswaehlen oder hierher ziehen

Grosse Dateien bis 200 MB werden automatisch in Teilen hochgeladen
{/* Stats */} {files.length > 0 && (
{completedCount} von {files.length} fertig {formatFileSize(totalSize)} gesamt
)} {/* File List */}
{files.map(file => (

{file.name}

{formatFileSize(file.size)}

{/* Progress Bar */} {(file.status === 'uploading' || file.status === 'pending') && (

{file.progress}% hochgeladen

)} {/* Status */} {file.status === 'complete' && (
Erfolgreich hochgeladen
)} {file.status === 'error' && (
{file.error || 'Fehler beim Hochladen'}
)}
))}
{/* Info Box */}

Hinweise:

  • Die Dateien werden lokal im WLAN uebertragen
  • Keine Daten werden ins Internet gesendet
  • Unterstuetzte Formate: PDF
{/* Server Info */}
Server: {serverUrl || 'Wird ermittelt...'}
) }