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>
167 lines
9.2 KiB
TypeScript
167 lines
9.2 KiB
TypeScript
'use client'
|
|
|
|
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'
|
|
|
|
interface CollectionsTabProps {
|
|
collections: Collection[]
|
|
loading: boolean
|
|
onRefresh: () => void
|
|
}
|
|
|
|
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)
|
|
const [formData, setFormData] = useState<CreateCollectionData>({
|
|
name: 'bp_',
|
|
display_name: '',
|
|
bundesland: 'NI',
|
|
use_case: '',
|
|
description: '',
|
|
})
|
|
|
|
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: '' })
|
|
onRefresh()
|
|
} else {
|
|
const error = await res.json()
|
|
setCreateError(error.detail || 'Fehler beim Erstellen')
|
|
}
|
|
} catch (err) {
|
|
setCreateError('Netzwerkfehler')
|
|
} finally {
|
|
setCreating(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-slate-900">RAG Sammlungen</h2>
|
|
<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>
|
|
Neue Sammlung
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{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>
|
|
)}
|
|
|
|
{!loading && (
|
|
<div className="grid gap-4">
|
|
{collections.length === 0 ? (
|
|
<div className="bg-slate-50 rounded-lg p-8 text-center">
|
|
<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="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
</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>
|
|
</div>
|
|
) : (
|
|
collections.map((col) => <CollectionCard key={col.name} collection={col} />)
|
|
)}
|
|
{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>
|
|
<span className="text-sm font-medium text-slate-600">Neue Sammlung erstellen</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Create Collection Modal */}
|
|
{showCreateModal && (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<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 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>}
|
|
<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" />
|
|
</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>)}
|
|
</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="">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" />
|
|
<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="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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { CollectionsTab, CollectionCard }
|