Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
127 lines
5.7 KiB
TypeScript
127 lines
5.7 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useCallback } from 'react'
|
|
import { API_BASE } from './types'
|
|
|
|
export function UploadTab({ onUploadComplete }: { onUploadComplete: () => void }) {
|
|
const [isDragging, setIsDragging] = useState(false)
|
|
const [files, setFiles] = useState<File[]>([])
|
|
const [uploading, setUploading] = useState(false)
|
|
|
|
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) {
|
|
setFiles(prev => [...prev, ...Array.from(e.target.files!)])
|
|
}
|
|
}, [])
|
|
|
|
const removeFile = useCallback((index: number) => {
|
|
setFiles(prev => prev.filter((_, i) => i !== index))
|
|
}, [])
|
|
|
|
const handleUpload = async () => {
|
|
if (files.length === 0) return
|
|
setUploading(true)
|
|
try {
|
|
for (const file of files) {
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
formData.append('collection', 'bp_nibis_eh')
|
|
await fetch(`${API_BASE}/api/v1/admin/rag/upload`, { method: 'POST', body: formData })
|
|
}
|
|
setFiles([])
|
|
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>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-2">Ziel-Sammlung</label>
|
|
<select className="w-full md:w-64 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500">
|
|
<option value="bp_nibis_eh">Niedersachsen - Klausurkorrektur</option>
|
|
<option value="bp_ni_zeugnis" disabled>Niedersachsen - Zeugnisgenerator (demnächst)</option>
|
|
</select>
|
|
</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>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 & Indexieren
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|