This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/studio-v2/components/DocumentUpload.tsx
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

364 lines
14 KiB
TypeScript

'use client'
import { useState, useRef, useCallback } from 'react'
import { useTheme } from '@/lib/ThemeContext'
interface UploadedDocument {
id: string
name: string
originalName: string
size: number
type: string
uploadedAt: Date
status: 'uploading' | 'processing' | 'complete' | 'error'
progress: number
url?: string
error?: string
}
interface DocumentUploadProps {
onUploadComplete?: (documents: UploadedDocument[]) => void
className?: string
}
// Formatiere Dateigroesse
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]
}
export function DocumentUpload({ onUploadComplete, className = '' }: DocumentUploadProps) {
const { isDark } = useTheme()
const [documents, setDocuments] = useState<UploadedDocument[]>([])
const [isDragging, setIsDragging] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [editName, setEditName] = useState('')
const fileInputRef = useRef<HTMLInputElement>(null)
// Echter Upload mit lokalem Blob URL fuer Vorschau
const uploadFile = useCallback((file: File): Promise<UploadedDocument> => {
return new Promise(async (resolve, reject) => {
const doc: UploadedDocument = {
id: `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name,
originalName: file.name,
size: file.size,
type: file.type,
uploadedAt: new Date(),
status: 'uploading',
progress: 0
}
// Dokument sofort zur Liste hinzufuegen
setDocuments(prev => [...prev, doc])
try {
// Fortschritt auf 30% setzen (Datei wird gelesen)
setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, progress: 30 } : d))
// Blob URL fuer lokale Vorschau erstellen
const blobUrl = URL.createObjectURL(file)
// Fortschritt auf 60% setzen
setDocuments(prev => prev.map(d => d.id === doc.id ? { ...d, progress: 60 } : d))
// Kurze Verzoegerung fuer visuelles Feedback
await new Promise(r => setTimeout(r, 300))
// Fortschritt auf 100% setzen
const completedDoc = {
...doc,
status: 'complete' as const,
progress: 100,
url: blobUrl
}
setDocuments(prev => prev.map(d => d.id === doc.id ? completedDoc : d))
resolve(completedDoc)
} catch (error) {
console.error('Upload error:', error)
setDocuments(prev => prev.map(d =>
d.id === doc.id ? { ...d, status: 'error' as const, error: 'Upload fehlgeschlagen' } : d
))
reject(error)
}
})
}, [])
// Dateien verarbeiten
const handleFiles = useCallback(async (files: FileList | null) => {
if (!files || files.length === 0) return
const fileArray = Array.from(files)
const validFiles = fileArray.filter(f =>
f.type === 'application/pdf' ||
f.type.startsWith('image/') ||
f.name.endsWith('.pdf') ||
f.name.endsWith('.jpg') ||
f.name.endsWith('.jpeg') ||
f.name.endsWith('.png')
)
if (validFiles.length === 0) {
alert('Bitte nur PDF- oder Bilddateien hochladen.')
return
}
const uploadedDocs = await Promise.all(validFiles.map(f => uploadFile(f)))
onUploadComplete?.(uploadedDocs)
}, [uploadFile, onUploadComplete])
// Drag & Drop Handler
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
}, [])
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
handleFiles(e.dataTransfer.files)
}, [handleFiles])
// Dokument loeschen
const handleDelete = useCallback((id: string) => {
setDocuments(prev => prev.filter(d => d.id !== id))
}, [])
// Dokument umbenennen starten
const handleStartRename = useCallback((doc: UploadedDocument) => {
setEditingId(doc.id)
setEditName(doc.name.replace(/\.[^/.]+$/, '')) // Name ohne Extension
}, [])
// Umbenennen speichern
const handleSaveRename = useCallback((id: string) => {
if (editName.trim()) {
setDocuments(prev => prev.map(d => {
if (d.id === id) {
const ext = d.originalName.split('.').pop()
return { ...d, name: `${editName.trim()}.${ext}` }
}
return d
}))
}
setEditingId(null)
setEditName('')
}, [editName])
// Datei-Icon basierend auf Typ
const getFileIcon = (type: string) => {
if (type === 'application/pdf') return '📄'
if (type.startsWith('image/')) return '🖼️'
return '📎'
}
return (
<div className={`space-y-6 ${className}`}>
{/* Upload-Bereich */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`relative border-2 border-dashed rounded-3xl p-8 text-center cursor-pointer transition-all ${
isDragging
? isDark
? 'border-blue-400 bg-blue-500/20'
: 'border-blue-500 bg-blue-50'
: isDark
? 'border-white/20 bg-white/5 hover:bg-white/10 hover:border-white/30'
: 'border-slate-300 bg-slate-50 hover:bg-slate-100 hover:border-slate-400'
}`}
>
<input
ref={fileInputRef}
type="file"
multiple
accept=".pdf,.jpg,.jpeg,.png,image/*,application/pdf"
onChange={(e) => handleFiles(e.target.files)}
className="hidden"
/>
<div className="flex flex-col items-center gap-4">
<div className={`w-16 h-16 rounded-2xl flex items-center justify-center ${
isDark ? 'bg-white/10' : 'bg-slate-200'
}`}>
<svg className={`w-8 h-8 ${isDark ? 'text-white/60' : 'text-slate-500'}`} 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>
</div>
<div>
<p className={`text-lg font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
{isDragging ? 'Dateien hier ablegen' : 'Dokumente hochladen'}
</p>
<p className={`text-sm mt-1 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
Ziehen Sie Dateien hierher oder klicken Sie zum Auswaehlen
</p>
<p className={`text-xs mt-2 ${isDark ? 'text-white/30' : 'text-slate-400'}`}>
PDF, JPG, PNG - max. 50 MB pro Datei
</p>
</div>
</div>
</div>
{/* Hochgeladene Dokumente */}
{documents.length > 0 && (
<div className={`rounded-2xl border overflow-hidden ${
isDark ? 'bg-white/5 border-white/10' : 'bg-white border-slate-200'
}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-white/10' : 'border-slate-200'}`}>
<h3 className={`font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
Hochgeladene Dokumente ({documents.length})
</h3>
</div>
<div className="divide-y divide-slate-200 dark:divide-white/10">
{documents.map((doc) => (
<div key={doc.id} className={`p-4 flex items-center gap-4 ${
isDark ? 'hover:bg-white/5' : 'hover:bg-slate-50'
}`}>
{/* Icon */}
<div className={`w-12 h-12 rounded-xl flex items-center justify-center text-2xl ${
doc.status === 'complete'
? isDark ? 'bg-green-500/20' : 'bg-green-100'
: doc.status === 'error'
? isDark ? 'bg-red-500/20' : 'bg-red-100'
: isDark ? 'bg-blue-500/20' : 'bg-blue-100'
}`}>
{getFileIcon(doc.type)}
</div>
{/* Info */}
<div className="flex-1 min-w-0">
{editingId === doc.id ? (
<div className="flex items-center gap-2">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveRename(doc.id)}
onBlur={() => handleSaveRename(doc.id)}
autoFocus
className={`flex-1 px-2 py-1 rounded border text-sm ${
isDark
? 'bg-white/10 border-white/20 text-white'
: 'bg-white border-slate-300 text-slate-900'
}`}
/>
<span className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
.{doc.originalName.split('.').pop()}
</span>
</div>
) : (
<p className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{doc.name}
</p>
)}
<div className="flex items-center gap-3 mt-1">
<span className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{formatFileSize(doc.size)}
</span>
{doc.status === 'complete' && (
<span className={`text-xs px-2 py-0.5 rounded-full ${
isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-100 text-green-700'
}`}>
Hochgeladen
</span>
)}
{doc.status === 'uploading' && (
<span className={`text-xs ${isDark ? 'text-blue-300' : 'text-blue-600'}`}>
{Math.round(doc.progress)}%
</span>
)}
{doc.status === 'error' && (
<span className={`text-xs px-2 py-0.5 rounded-full ${
isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-100 text-red-700'
}`}>
Fehler
</span>
)}
</div>
{/* Progress Bar */}
{doc.status === 'uploading' && (
<div className={`mt-2 h-1.5 rounded-full overflow-hidden ${
isDark ? 'bg-white/10' : 'bg-slate-200'
}`}>
<div
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full transition-all duration-300"
style={{ width: `${doc.progress}%` }}
/>
</div>
)}
</div>
{/* Aktionen */}
{doc.status === 'complete' && (
<div className="flex items-center gap-2">
{/* Umbenennen */}
<button
onClick={() => handleStartRename(doc)}
className={`p-2 rounded-lg transition-colors ${
isDark
? 'hover:bg-white/10 text-white/60 hover:text-white'
: 'hover:bg-slate-100 text-slate-400 hover:text-slate-700'
}`}
title="Umbenennen"
>
<svg className="w-4 h-4" 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>
</button>
{/* Oeffnen/Vorschau */}
{doc.url && (
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
className={`p-2 rounded-lg transition-colors ${
isDark
? 'hover:bg-white/10 text-white/60 hover:text-white'
: 'hover:bg-slate-100 text-slate-400 hover:text-slate-700'
}`}
title="Oeffnen"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
)}
{/* Loeschen */}
<button
onClick={() => handleDelete(doc.id)}
className={`p-2 rounded-lg transition-colors ${
isDark
? 'hover:bg-red-500/20 text-white/60 hover:text-red-300'
: 'hover:bg-red-100 text-slate-400 hover:text-red-600'
}`}
title="Loeschen"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
)
}