[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:
Benjamin Admin
2026-04-25 08:56:45 +02:00
parent b4613e26f3
commit 451365a312
115 changed files with 10694 additions and 13839 deletions

View File

@@ -2,6 +2,7 @@
import { useState } from 'react'
import type { Collection, CreateCollectionData } from '../types'
import { CollectionCard } from './CollectionCard'
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
@@ -11,11 +12,24 @@ interface CollectionsTabProps {
onRefresh: () => void
}
function CollectionsTab({
collections,
loading,
onRefresh
}: CollectionsTabProps) {
const useCaseOptions = [
{ value: 'klausur', label: 'Klausurkorrektur' },
{ value: 'zeugnis', label: 'Zeugniserstellung' },
{ value: 'material', label: 'Unterrichtsmaterial' },
{ value: 'curriculum', label: 'Lehrplan' },
{ value: 'other', label: 'Sonstiges' },
]
const bundeslandOptions = [
{ value: 'NI', label: 'Niedersachsen' },
{ value: 'NW', label: 'Nordrhein-Westfalen' },
{ value: 'BY', label: 'Bayern' },
{ value: 'BW', label: 'Baden-Wuerttemberg' },
{ value: 'HE', label: 'Hessen' },
{ value: 'DE', label: 'Bundesweit' },
]
function CollectionsTab({ collections, loading, onRefresh }: CollectionsTabProps) {
const [showCreateModal, setShowCreateModal] = useState(false)
const [creating, setCreating] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
@@ -30,23 +44,15 @@ function CollectionsTab({
const handleCreate = async () => {
setCreating(true)
setCreateError(null)
try {
const res = await fetch(`${API_BASE}/api/v1/admin/rag/collections`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (res.ok) {
setShowCreateModal(false)
setFormData({
name: 'bp_',
display_name: '',
bundesland: 'NI',
use_case: '',
description: '',
})
setFormData({ name: 'bp_', display_name: '', bundesland: 'NI', use_case: '', description: '' })
onRefresh()
} else {
const error = await res.json()
@@ -59,23 +65,6 @@ function CollectionsTab({
}
}
const useCaseOptions = [
{ value: 'klausur', label: 'Klausurkorrektur' },
{ value: 'zeugnis', label: 'Zeugniserstellung' },
{ value: 'material', label: 'Unterrichtsmaterial' },
{ value: 'curriculum', label: 'Lehrplan' },
{ value: 'other', label: 'Sonstiges' },
]
const bundeslandOptions = [
{ value: 'NI', label: 'Niedersachsen' },
{ value: 'NW', label: 'Nordrhein-Westfalen' },
{ value: 'BY', label: 'Bayern' },
{ value: 'BW', label: 'Baden-Württemberg' },
{ value: 'HE', label: 'Hessen' },
{ value: 'DE', label: 'Bundesweit' },
]
return (
<div className="space-y-6">
{/* Header */}
@@ -85,32 +74,20 @@ function CollectionsTab({
<p className="text-sm text-slate-500">Verwaltung der indexierten Dokumentensammlungen</p>
</div>
<div className="flex gap-3">
<button
onClick={onRefresh}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50"
>
Aktualisieren
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 flex items-center gap-2"
>
<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>
<button onClick={onRefresh} className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50">Aktualisieren</button>
<button onClick={() => setShowCreateModal(true)} className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 flex items-center gap-2">
<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 Sammlung
</button>
</div>
</div>
{/* Loading State */}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
</div>
)}
{/* Collections Grid */}
{!loading && (
<div className="grid gap-4">
{collections.length === 0 ? (
@@ -120,28 +97,14 @@ function CollectionsTab({
</svg>
<h3 className="text-lg font-medium text-slate-900 mb-2">Keine Sammlungen vorhanden</h3>
<p className="text-slate-500 mb-4">Erstellen Sie eine neue Sammlung, um Dokumente zu indexieren.</p>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700"
>
Erste Sammlung erstellen
</button>
<button onClick={() => setShowCreateModal(true)} className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700">Erste Sammlung erstellen</button>
</div>
) : (
collections.map((col) => (
<CollectionCard key={col.name} collection={col} />
))
collections.map((col) => <CollectionCard key={col.name} collection={col} />)
)}
{/* Add new collection card */}
{collections.length > 0 && (
<button
onClick={() => setShowCreateModal(true)}
className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer"
>
<svg className="w-8 h-8 text-slate-400 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
<button onClick={() => setShowCreateModal(true)} className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center hover:border-primary-400 hover:bg-primary-50 transition-colors cursor-pointer">
<svg className="w-8 h-8 text-slate-400 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>
<span className="text-sm font-medium text-slate-600">Neue Sammlung erstellen</span>
</button>
)}
@@ -154,115 +117,43 @@ function CollectionsTab({
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4">
<div className="p-6 border-b border-slate-200">
<h3 className="text-lg font-semibold text-slate-900">Neue RAG-Sammlung erstellen</h3>
<p className="text-sm text-slate-500 mt-1">
Erstellen Sie eine neue Sammlung für einen spezifischen Anwendungsfall
</p>
<p className="text-sm text-slate-500 mt-1">Erstellen Sie eine neue Sammlung fuer einen spezifischen Anwendungsfall</p>
</div>
<div className="p-6 space-y-4">
{createError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{createError}
</div>
)}
{createError && <div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{createError}</div>}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Anzeigename *
</label>
<input
type="text"
value={formData.display_name}
onChange={(e) => setFormData(prev => ({ ...prev, display_name: e.target.value }))}
placeholder="z.B. Niedersachsen - Zeugniserstellung"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
<label className="block text-sm font-medium text-slate-700 mb-1">Anzeigename *</label>
<input type="text" value={formData.display_name} onChange={(e) => setFormData(prev => ({ ...prev, display_name: e.target.value }))} placeholder="z.B. Niedersachsen - Zeugniserstellung" className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Bundesland
</label>
<select
value={formData.bundesland}
onChange={(e) => setFormData(prev => ({ ...prev, bundesland: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
{bundeslandOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
<label className="block text-sm font-medium text-slate-700 mb-1">Bundesland</label>
<select value={formData.bundesland} onChange={(e) => setFormData(prev => ({ ...prev, bundesland: e.target.value }))} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500">
{bundeslandOptions.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Anwendungsfall *
</label>
<select
value={formData.use_case}
onChange={(e) => setFormData(prev => ({ ...prev, use_case: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">Auswählen...</option>
{useCaseOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
<label className="block text-sm font-medium text-slate-700 mb-1">Anwendungsfall *</label>
<select value={formData.use_case} onChange={(e) => setFormData(prev => ({ ...prev, use_case: e.target.value }))} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500">
<option value="">Auswaehlen...</option>
{useCaseOptions.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Technischer Name *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="bp_ni_zeugnis"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 font-mono text-sm"
/>
<label className="block text-sm font-medium text-slate-700 mb-1">Technischer Name *</label>
<input type="text" value={formData.name} onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder="bp_ni_zeugnis" className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 font-mono text-sm" />
<p className="text-xs text-slate-500 mt-1">Muss mit "bp_" beginnen. Nur Kleinbuchstaben und Unterstriche.</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Beschreibung
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
placeholder="Wofür wird diese Sammlung verwendet?"
rows={3}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<textarea value={formData.description} onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} placeholder="Wofuer wird diese Sammlung verwendet?" rows={3} className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500" />
</div>
</div>
<div className="p-6 border-t border-slate-200 flex justify-end gap-3">
<button
onClick={() => {
setShowCreateModal(false)
setCreateError(null)
}}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50"
>
Abbrechen
</button>
<button
onClick={handleCreate}
disabled={creating || !formData.name || !formData.display_name || !formData.use_case}
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
{creating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
Wird erstellt...
</>
) : (
'Sammlung erstellen'
)}
<button onClick={() => { setShowCreateModal(false); setCreateError(null) }} className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50">Abbrechen</button>
<button onClick={handleCreate} disabled={creating || !formData.name || !formData.display_name || !formData.use_case} className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-lg hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2">
{creating ? (<><div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>Wird erstellt...</>) : 'Sammlung erstellen'}
</button>
</div>
</div>
@@ -272,322 +163,4 @@ function CollectionsTab({
)
}
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 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',
}
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 {
// Start the reindex
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
}
// Poll for progress
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('Lösche 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 gelöscht)`
)
setReindexing(false)
return // Stop polling
} else if (progress.phase === 'failed') {
setIngestMessage(`Fehler: ${progress.error || 'Unbekannter Fehler'}`)
setReindexing(false)
return // Stop polling
}
// Continue polling if still running
if (progress.running) {
setTimeout(pollProgress, 1000)
} else {
setReindexing(false)
}
}
} catch (err) {
// Ignore polling errors, will retry
setTimeout(pollProgress, 2000)
}
}
// Start polling after a short delay
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">Fächer</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 löscht alle {collection.chunkCount.toLocaleString()} bestehenden Chunks
und erstellt sie mit dem gewählten 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 Suchqualität.
</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>
)
}
// ============================================================================
// Upload Tab
// ============================================================================
export { CollectionsTab, CollectionCard }