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>
This commit is contained in:
327
website/app/admin/rag/components/UploadTab.tsx
Normal file
327
website/app/admin/rag/components/UploadTab.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
import type { Collection, UploadResult } from '../types'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
||||
|
||||
interface UploadTabProps {
|
||||
collections: Collection[]
|
||||
onUploadComplete: () => void
|
||||
}
|
||||
|
||||
function UploadTab({
|
||||
collections,
|
||||
onUploadComplete
|
||||
}: UploadTabProps) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [uploadResults, setUploadResults] = useState<UploadResult[]>([])
|
||||
const [currentFile, setCurrentFile] = useState<string>('')
|
||||
const [selectedCollection, setSelectedCollection] = useState('bp_nibis_eh')
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
|
||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||
const validFiles = droppedFiles.filter(
|
||||
f => f.name.endsWith('.zip') || f.name.endsWith('.pdf')
|
||||
)
|
||||
setFiles(prev => [...prev, ...validFiles])
|
||||
}, [])
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const selectedFiles = Array.from(e.target.files)
|
||||
setFiles(prev => [...prev, ...selectedFiles])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const removeFile = useCallback((index: number) => {
|
||||
setFiles(prev => prev.filter((_, i) => i !== index))
|
||||
}, [])
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (files.length === 0) return
|
||||
|
||||
setUploading(true)
|
||||
setUploadResults([])
|
||||
const results: UploadResult[] = []
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
setCurrentFile(file.name)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('collection', selectedCollection)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/rag/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
results.push({
|
||||
filename: file.name,
|
||||
status: data.status,
|
||||
pdfs_extracted: data.pdfs_extracted,
|
||||
pdfs_skipped: data.pdfs_skipped,
|
||||
duplicates: data.duplicates || [],
|
||||
target_year: data.target_year,
|
||||
message: data.message,
|
||||
})
|
||||
} else {
|
||||
results.push({
|
||||
filename: file.name,
|
||||
status: 'error',
|
||||
pdfs_extracted: 0,
|
||||
pdfs_skipped: 0,
|
||||
duplicates: [],
|
||||
target_year: 0,
|
||||
message: 'Upload fehlgeschlagen',
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
results.push({
|
||||
filename: file.name,
|
||||
status: 'error',
|
||||
pdfs_extracted: 0,
|
||||
pdfs_skipped: 0,
|
||||
duplicates: [],
|
||||
target_year: 0,
|
||||
message: 'Netzwerkfehler',
|
||||
})
|
||||
}
|
||||
setUploadResults([...results])
|
||||
}
|
||||
|
||||
setFiles([])
|
||||
setCurrentFile('')
|
||||
onUploadComplete()
|
||||
} catch (err) {
|
||||
console.error('Upload failed:', err)
|
||||
} finally {
|
||||
setUploading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">Dokumente hochladen</h2>
|
||||
<p className="text-sm text-slate-500">
|
||||
ZIP-Archive oder einzelne PDFs hochladen. ZIPs werden automatisch entpackt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Collection Selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Ziel-Sammlung
|
||||
</label>
|
||||
<select
|
||||
value={selectedCollection}
|
||||
onChange={(e) => setSelectedCollection(e.target.value)}
|
||||
className="w-full md:w-96 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
||||
>
|
||||
{collections.length > 0 ? (
|
||||
collections.map((col) => (
|
||||
<option key={col.name} value={col.name}>
|
||||
{col.displayName} ({col.chunkCount.toLocaleString()} Chunks)
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="bp_nibis_eh">Niedersachsen - Klausurkorrektur</option>
|
||||
)}
|
||||
</select>
|
||||
{collections.length === 0 && (
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Keine Sammlungen vorhanden. Erstellen Sie zuerst eine Sammlung im Tab "Sammlungen".
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Drop Zone */}
|
||||
<div
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragging(true) }}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={handleDrop}
|
||||
className={`
|
||||
border-2 border-dashed rounded-lg p-12 text-center transition-colors
|
||||
${isDragging
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-slate-300 hover:border-slate-400'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg className="w-12 h-12 text-slate-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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>
|
||||
<p className="text-lg font-medium text-slate-700 mb-2">
|
||||
ZIP-Datei oder Ordner hierher ziehen
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
oder
|
||||
</p>
|
||||
<label className="cursor-pointer">
|
||||
<span className="px-4 py-2 bg-white border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50">
|
||||
Dateien auswählen
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".zip,.pdf"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
<p className="text-xs text-slate-400 mt-4">
|
||||
Unterstützt: .zip, .pdf
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* File Queue */}
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-slate-700">Upload-Queue ({files.length})</h3>
|
||||
{files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between bg-slate-50 rounded-lg p-3"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-900">{file.name}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="p-1 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<svg className="w-5 h-5" 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>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={uploading}
|
||||
className="w-full py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
{currentFile ? `Lade ${currentFile}...` : 'Wird hochgeladen...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
Hochladen
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Results */}
|
||||
{uploadResults.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-medium text-slate-700">Upload-Ergebnisse</h3>
|
||||
{uploadResults.map((result, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`rounded-lg p-4 ${
|
||||
result.status === 'success'
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: result.status === 'all_duplicates'
|
||||
? 'bg-yellow-50 border border-yellow-200'
|
||||
: result.status === 'partial_duplicates'
|
||||
? 'bg-blue-50 border border-blue-200'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{result.status === 'success' && (
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
{result.status === 'all_duplicates' && (
|
||||
<svg className="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
)}
|
||||
{result.status === 'partial_duplicates' && (
|
||||
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{result.status === 'error' && (
|
||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)}
|
||||
<span className="font-medium text-slate-900">{result.filename}</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-500">Jahr {result.target_year}</span>
|
||||
</div>
|
||||
<p className={`mt-1 text-sm ${
|
||||
result.status === 'success' ? 'text-green-700' :
|
||||
result.status === 'all_duplicates' ? 'text-yellow-700' :
|
||||
result.status === 'partial_duplicates' ? 'text-blue-700' :
|
||||
'text-red-700'
|
||||
}`}>
|
||||
{result.message}
|
||||
</p>
|
||||
{(result.pdfs_extracted ?? 0) > 0 && (
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
{result.pdfs_extracted} neue PDFs extrahiert
|
||||
</p>
|
||||
)}
|
||||
{(result.pdfs_skipped ?? 0) > 0 && (result.duplicates?.length ?? 0) > 0 && (
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs text-slate-500 cursor-pointer hover:text-slate-700">
|
||||
{result.pdfs_skipped} Duplikate anzeigen
|
||||
</summary>
|
||||
<ul className="mt-1 text-xs text-slate-500 pl-4 max-h-32 overflow-y-auto">
|
||||
{result.duplicates?.map((dup, i) => (
|
||||
<li key={i} className="truncate">{dup}</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setUploadResults([])}
|
||||
className="text-sm text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
Ergebnisse ausblenden
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Ingestion Tab
|
||||
// ============================================================================
|
||||
|
||||
|
||||
export { UploadTab }
|
||||
Reference in New Issue
Block a user