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/DocumentSpace.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

406 lines
16 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import { useTheme } from '@/lib/ThemeContext'
interface Document {
id: string
name: string
type: string
size: number
uploadedAt: Date
category?: string
tags?: string[]
url?: string
}
interface DocumentSpaceProps {
documents: Document[]
onDelete?: (id: string) => void
onRename?: (id: string, newName: string) => void
onOpen?: (doc: Document) => void
className?: string
}
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]
}
const formatDate = (date: Date): string => {
return new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(date)
}
export function DocumentSpace({
documents,
onDelete,
onRename,
onOpen,
className = ''
}: DocumentSpaceProps) {
const { isDark } = useTheme()
const [searchQuery, setSearchQuery] = useState('')
const [filterType, setFilterType] = useState<string>('all')
const [sortBy, setSortBy] = useState<'name' | 'date' | 'size'>('date')
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('list')
const [editingId, setEditingId] = useState<string | null>(null)
const [editName, setEditName] = useState('')
const [previewDoc, setPreviewDoc] = useState<Document | null>(null)
// Filtertypen ermitteln
const fileTypes = useMemo(() => {
const types = new Set(documents.map(d => d.type.split('/')[1] || d.type))
return ['all', ...Array.from(types)]
}, [documents])
// Dokumente filtern und sortieren
const filteredDocuments = useMemo(() => {
let filtered = [...documents]
// Suchfilter
if (searchQuery) {
const query = searchQuery.toLowerCase()
filtered = filtered.filter(d =>
d.name.toLowerCase().includes(query) ||
d.tags?.some(t => t.toLowerCase().includes(query))
)
}
// Typfilter
if (filterType !== 'all') {
filtered = filtered.filter(d =>
d.type.includes(filterType)
)
}
// Sortieren
filtered.sort((a, b) => {
let cmp = 0
switch (sortBy) {
case 'name':
cmp = a.name.localeCompare(b.name)
break
case 'date':
cmp = new Date(a.uploadedAt).getTime() - new Date(b.uploadedAt).getTime()
break
case 'size':
cmp = a.size - b.size
break
}
return sortOrder === 'asc' ? cmp : -cmp
})
return filtered
}, [documents, searchQuery, filterType, sortBy, sortOrder])
const handleStartRename = (doc: Document) => {
setEditingId(doc.id)
setEditName(doc.name.replace(/\.[^/.]+$/, ''))
}
const handleSaveRename = (doc: Document) => {
if (editName.trim() && onRename) {
const ext = doc.name.split('.').pop()
onRename(doc.id, `${editName.trim()}.${ext}`)
}
setEditingId(null)
setEditName('')
}
const getFileIcon = (type: string) => {
if (type.includes('pdf')) return '📄'
if (type.includes('image')) return '🖼️'
if (type.includes('word') || type.includes('doc')) return '📝'
if (type.includes('sheet') || type.includes('excel')) return '📊'
return '📎'
}
if (documents.length === 0) {
return (
<div className={`${className} text-center py-12`}>
<div className={`w-16 h-16 mx-auto rounded-2xl flex items-center justify-center mb-4 ${
isDark ? 'bg-white/10' : 'bg-slate-100'
}`}>
<span className="text-3xl">📁</span>
</div>
<h3 className={`text-lg font-medium ${isDark ? 'text-white' : 'text-slate-900'}`}>
Noch keine Dokumente
</h3>
<p className={`text-sm mt-2 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
Laden Sie Ihr erstes Dokument hoch, um loszulegen.
</p>
</div>
)
}
return (
<div className={`space-y-4 ${className}`}>
{/* Toolbar */}
<div className="flex flex-wrap items-center gap-4">
{/* Suche */}
<div className="relative flex-1 min-w-[200px]">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Dokumente durchsuchen..."
className={`w-full pl-10 pr-4 py-2 rounded-xl border text-sm ${
isDark
? 'bg-white/5 border-white/10 text-white placeholder-white/40'
: 'bg-white border-slate-200 text-slate-900 placeholder-slate-400'
}`}
/>
<svg className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${
isDark ? 'text-white/40' : 'text-slate-400'
}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
{/* Filter */}
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className={`px-3 py-2 rounded-xl border text-sm ${
isDark
? 'bg-white/5 border-white/10 text-white'
: 'bg-white border-slate-200 text-slate-900'
}`}
>
{fileTypes.map(type => (
<option key={type} value={type}>
{type === 'all' ? 'Alle Typen' : type.toUpperCase()}
</option>
))}
</select>
{/* Sortierung */}
<select
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [by, order] = e.target.value.split('-')
setSortBy(by as 'name' | 'date' | 'size')
setSortOrder(order as 'asc' | 'desc')
}}
className={`px-3 py-2 rounded-xl border text-sm ${
isDark
? 'bg-white/5 border-white/10 text-white'
: 'bg-white border-slate-200 text-slate-900'
}`}
>
<option value="date-desc">Neueste zuerst</option>
<option value="date-asc">Aelteste zuerst</option>
<option value="name-asc">Name A-Z</option>
<option value="name-desc">Name Z-A</option>
<option value="size-desc">Groesste zuerst</option>
<option value="size-asc">Kleinste zuerst</option>
</select>
{/* Ansicht */}
<div className={`flex rounded-xl border overflow-hidden ${
isDark ? 'border-white/10' : 'border-slate-200'
}`}>
<button
onClick={() => setViewMode('list')}
className={`p-2 ${
viewMode === 'list'
? isDark ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-900'
: isDark ? 'text-white/60' : 'text-slate-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<button
onClick={() => setViewMode('grid')}
className={`p-2 ${
viewMode === 'grid'
? isDark ? 'bg-white/20 text-white' : 'bg-slate-200 text-slate-900'
: isDark ? 'text-white/60' : 'text-slate-500'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</button>
</div>
</div>
{/* Ergebnisse */}
<div className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{filteredDocuments.length} Dokument{filteredDocuments.length !== 1 ? 'e' : ''} gefunden
</div>
{/* Dokumentliste */}
{viewMode === 'list' ? (
<div className={`rounded-2xl border overflow-hidden ${
isDark ? 'bg-white/5 border-white/10' : 'bg-white border-slate-200'
}`}>
<div className="divide-y divide-slate-200 dark:divide-white/10">
{filteredDocuments.map((doc) => (
<div
key={doc.id}
className={`p-4 flex items-center gap-4 cursor-pointer ${
isDark ? 'hover:bg-white/5' : 'hover:bg-slate-50'
}`}
onClick={() => setPreviewDoc(doc)}
>
<div className={`w-10 h-10 rounded-xl flex items-center justify-center text-xl ${
isDark ? 'bg-white/10' : 'bg-slate-100'
}`}>
{getFileIcon(doc.type)}
</div>
<div className="flex-1 min-w-0">
{editingId === doc.id ? (
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveRename(doc)}
onBlur={() => handleSaveRename(doc)}
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'
}`}
/>
</div>
) : (
<p className={`font-medium truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{doc.name}
</p>
)}
<p className={`text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{formatFileSize(doc.size)} · {formatDate(new Date(doc.uploadedAt))}
</p>
</div>
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleStartRename(doc)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
title="Umbenennen"
>
<svg className={`w-4 h-4 ${isDark ? 'text-white/50' : 'text-slate-400'}`} 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>
<button
onClick={() => onDelete?.(doc.id)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-red-500/20' : 'hover:bg-red-100'}`}
title="Loeschen"
>
<svg className={`w-4 h-4 ${isDark ? 'text-white/50 hover:text-red-300' : 'text-slate-400 hover:text-red-600'}`} 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 className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{filteredDocuments.map((doc) => (
<div
key={doc.id}
className={`rounded-2xl border p-4 cursor-pointer transition-all hover:scale-105 ${
isDark
? 'bg-white/5 border-white/10 hover:bg-white/10'
: 'bg-white border-slate-200 hover:shadow-lg'
}`}
onClick={() => setPreviewDoc(doc)}
>
<div className={`w-full aspect-square rounded-xl flex items-center justify-center mb-3 ${
isDark ? 'bg-white/10' : 'bg-slate-100'
}`}>
<span className="text-4xl">{getFileIcon(doc.type)}</span>
</div>
<p className={`font-medium text-sm truncate ${isDark ? 'text-white' : 'text-slate-900'}`}>
{doc.name}
</p>
<p className={`text-xs mt-1 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
{formatFileSize(doc.size)}
</p>
</div>
))}
</div>
)}
{/* Vorschau-Modal */}
{previewDoc && previewDoc.url && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
onClick={() => setPreviewDoc(null)}
/>
<div className={`relative w-full max-w-4xl max-h-[90vh] rounded-3xl overflow-hidden ${
isDark ? 'bg-slate-900' : 'bg-white'
}`}>
{/* Header */}
<div className={`flex items-center justify-between p-4 border-b ${
isDark ? 'border-white/10' : 'border-slate-200'
}`}>
<div className="flex items-center gap-3">
<span className="text-2xl">{getFileIcon(previewDoc.type)}</span>
<div>
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
{previewDoc.name}
</h3>
<p className={`text-xs ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
{formatFileSize(previewDoc.size)} · {formatDate(new Date(previewDoc.uploadedAt))}
</p>
</div>
</div>
<button
onClick={() => setPreviewDoc(null)}
className={`p-2 rounded-lg ${isDark ? 'hover:bg-white/10' : 'hover:bg-slate-100'}`}
>
<svg className={`w-6 h-6 ${isDark ? 'text-white/60' : 'text-slate-400'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Vorschau-Inhalt */}
<div className={`p-4 overflow-auto max-h-[calc(90vh-120px)] ${
isDark ? 'bg-slate-800' : 'bg-slate-50'
}`}>
{previewDoc.type.includes('image') ? (
<img
src={previewDoc.url}
alt={previewDoc.name}
className="max-w-full h-auto mx-auto rounded-lg shadow-lg"
/>
) : previewDoc.type.includes('pdf') ? (
<iframe
src={previewDoc.url}
className="w-full h-[70vh] rounded-lg"
title={previewDoc.name}
/>
) : (
<div className={`text-center py-12 ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
<span className="text-6xl block mb-4">{getFileIcon(previewDoc.type)}</span>
<p>Vorschau fuer diesen Dateityp nicht verfuegbar</p>
</div>
)}
</div>
</div>
</div>
)}
</div>
)
}