[split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
246
website/app/admin/rag/components/CollectionCard.tsx
Normal file
246
website/app/admin/rag/components/CollectionCard.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { Collection } from '../types'
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
||||
|
||||
const statusColors = {
|
||||
ready: 'bg-green-100 text-green-800',
|
||||
indexing: 'bg-yellow-100 text-yellow-800',
|
||||
empty: 'bg-slate-100 text-slate-800',
|
||||
}
|
||||
|
||||
const statusLabels = {
|
||||
ready: 'Bereit',
|
||||
indexing: 'Indexierung...',
|
||||
empty: 'Leer',
|
||||
}
|
||||
|
||||
const useCaseLabels: Record<string, string> = {
|
||||
klausur: 'Klausurkorrektur',
|
||||
zeugnis: 'Zeugniserstellung',
|
||||
material: 'Unterrichtsmaterial',
|
||||
curriculum: 'Lehrplan',
|
||||
other: 'Sonstiges',
|
||||
unknown: 'Unbekannt',
|
||||
}
|
||||
|
||||
export function CollectionCard({ collection }: { collection: Collection }) {
|
||||
const [ingesting, setIngesting] = useState(false)
|
||||
const [reindexing, setReindexing] = useState(false)
|
||||
const [ingestMessage, setIngestMessage] = useState<string | null>(null)
|
||||
const [showReindexConfirm, setShowReindexConfirm] = useState(false)
|
||||
const [chunkingStrategy, setChunkingStrategy] = useState<'semantic' | 'recursive'>('semantic')
|
||||
|
||||
const handleIngest = async () => {
|
||||
setIngesting(true)
|
||||
setIngestMessage(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/v1/admin/rag/collections/${collection.name}/ingest?incremental=true`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setIngestMessage(data.message || 'Indexierung gestartet')
|
||||
} else {
|
||||
const error = await res.json()
|
||||
setIngestMessage(error.detail || 'Fehler beim Starten')
|
||||
}
|
||||
} catch (err) {
|
||||
setIngestMessage('Netzwerkfehler')
|
||||
} finally {
|
||||
setIngesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReindex = async () => {
|
||||
setShowReindexConfirm(false)
|
||||
setReindexing(true)
|
||||
setIngestMessage('Starte Re-Indexierung...')
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${API_BASE}/api/v1/admin/rag/collections/${collection.name}/reindex?chunking_strategy=${chunkingStrategy}`,
|
||||
{ method: 'POST' }
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json()
|
||||
setIngestMessage(error.detail || 'Fehler beim Starten')
|
||||
setReindexing(false)
|
||||
return
|
||||
}
|
||||
|
||||
const pollProgress = async () => {
|
||||
try {
|
||||
const progressRes = await fetch(`${API_BASE}/api/v1/admin/rag/reindex/progress`)
|
||||
if (progressRes.ok) {
|
||||
const progress = await progressRes.json()
|
||||
|
||||
if (progress.phase === 'deleting') {
|
||||
setIngestMessage('Loesche alte Chunks...')
|
||||
} else if (progress.phase === 'indexing') {
|
||||
const pct = progress.total_docs > 0
|
||||
? Math.round((progress.current_doc / progress.total_docs) * 100)
|
||||
: 0
|
||||
setIngestMessage(
|
||||
`Indexiere: ${progress.current_doc}/${progress.total_docs} (${pct}%) - ${progress.current_filename}`
|
||||
)
|
||||
} else if (progress.phase === 'complete') {
|
||||
setIngestMessage(
|
||||
`Fertig: ${progress.documents_processed} Dokumente, ` +
|
||||
`${progress.chunks_created} neue Chunks (${progress.old_chunks_deleted} alte geloescht)`
|
||||
)
|
||||
setReindexing(false)
|
||||
return
|
||||
} else if (progress.phase === 'failed') {
|
||||
setIngestMessage(`Fehler: ${progress.error || 'Unbekannter Fehler'}`)
|
||||
setReindexing(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (progress.running) {
|
||||
setTimeout(pollProgress, 1000)
|
||||
} else {
|
||||
setReindexing(false)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setTimeout(pollProgress, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(pollProgress, 500)
|
||||
} catch (err) {
|
||||
setIngestMessage('Netzwerkfehler bei Re-Indexierung')
|
||||
setReindexing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-6 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-900">{collection.displayName}</h3>
|
||||
<p className="text-sm text-slate-500 font-mono">{collection.name}</p>
|
||||
{collection.description && (
|
||||
<p className="text-sm text-slate-600 mt-1">{collection.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${statusColors[collection.status]}`}>
|
||||
{statusLabels[collection.status]}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 text-xs rounded">
|
||||
{useCaseLabels[collection.useCase] || collection.useCase}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Chunks</p>
|
||||
<p className="text-lg font-semibold text-slate-900">{collection.chunkCount.toLocaleString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Jahre</p>
|
||||
<p className="text-lg font-semibold text-slate-900">
|
||||
{collection.years?.length > 0
|
||||
? `${Math.min(...collection.years)}-${Math.max(...collection.years)}`
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Faecher</p>
|
||||
<p className="text-lg font-semibold text-slate-900">
|
||||
{collection.subjects?.length > 0 ? collection.subjects.length : '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-wider">Bundesland</p>
|
||||
<p className="text-lg font-semibold text-slate-900">{collection.bundesland}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{collection.subjects.length > 0 && (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{collection.subjects.slice(0, 8).map((subject) => (
|
||||
<span key={subject} className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded-md">{subject}</span>
|
||||
))}
|
||||
{collection.subjects.length > 8 && (
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-500 text-xs rounded-md">+{collection.subjects.length - 8} weitere</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ingestion Buttons */}
|
||||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={handleIngest}
|
||||
disabled={ingesting || reindexing}
|
||||
className="px-4 py-2 text-sm font-medium text-primary-700 bg-primary-50 border border-primary-200 rounded-lg hover:bg-primary-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{ingesting ? (
|
||||
<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div>Wird gestartet...</>
|
||||
) : (
|
||||
<><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>Neue indexieren</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowReindexConfirm(true)}
|
||||
disabled={ingesting || reindexing || collection.chunkCount === 0}
|
||||
className="px-4 py-2 text-sm font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-lg hover:bg-amber-100 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
title="Alle Dokumente mit neuem Chunking-Algorithmus neu indexieren"
|
||||
>
|
||||
{reindexing ? (
|
||||
<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-amber-600"></div>Re-Indexierung...</>
|
||||
) : (
|
||||
<><svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>Neu-Chunking</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{ingestMessage && <p className="mt-2 text-sm text-slate-600">{ingestMessage}</p>}
|
||||
</div>
|
||||
|
||||
{/* Re-Index Confirmation Modal */}
|
||||
{showReindexConfirm && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl p-6 max-w-md w-full mx-4 shadow-xl">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Collection neu indexieren?</h3>
|
||||
<p className="text-slate-600 mb-4">
|
||||
Dies loescht alle {collection.chunkCount.toLocaleString()} bestehenden Chunks
|
||||
und erstellt sie mit dem gewaehlten Chunking-Algorithmus neu.
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Chunking-Strategie</label>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="chunkingStrategy" value="semantic" checked={chunkingStrategy === 'semantic'} onChange={() => setChunkingStrategy('semantic')} className="text-primary-600" />
|
||||
<span className="text-sm"><strong>Semantisch</strong><span className="text-slate-500 ml-1">(empfohlen)</span></span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="chunkingStrategy" value="recursive" checked={chunkingStrategy === 'recursive'} onChange={() => setChunkingStrategy('recursive')} className="text-primary-600" />
|
||||
<span className="text-sm">Rekursiv (legacy)</span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">Semantisches Chunking respektiert Satzgrenzen und verbessert die Suchqualitaet.</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={() => setShowReindexConfirm(false)} className="px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200">Abbrechen</button>
|
||||
<button onClick={handleReindex} className="px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-lg hover:bg-amber-700">Neu indexieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user