'use client' /** * DSFA Document Manager * * Manages DSFA-related sources and documents for the RAG pipeline. * Features: * - View all registered DSFA sources with license info * - Upload new documents * - Trigger re-indexing * - View corpus statistics */ import { useState, useEffect } from 'react' import Link from 'next/link' import { ArrowLeft, RefreshCw, Upload, FileText, Database, Scale, ExternalLink, ChevronDown, ChevronUp, Search, Filter, CheckCircle, Clock, AlertCircle, BookOpen } from 'lucide-react' import { DSFASource, DSFACorpusStats, DSFASourceStats, DSFALicenseCode, DSFA_LICENSE_LABELS, DSFA_DOCUMENT_TYPE_LABELS } from '@/lib/sdk/types' // ============================================================================ // TYPES // ============================================================================ interface APIError { message: string status?: number } // ============================================================================ // API FUNCTIONS // ============================================================================ const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086' async function fetchSources(): Promise { try { const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources`) if (!response.ok) throw new Error('Failed to fetch sources') return await response.json() } catch { // Return mock data for demo return MOCK_SOURCES } } async function fetchStats(): Promise { try { const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/stats`) if (!response.ok) throw new Error('Failed to fetch stats') return await response.json() } catch { return MOCK_STATS } } async function initializeCorpus(): Promise<{ sources_registered: number }> { const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/init`, { method: 'POST', }) if (!response.ok) throw new Error('Failed to initialize corpus') return await response.json() } async function triggerIngestion(sourceCode: string): Promise { const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources/${sourceCode}/ingest`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }) if (!response.ok) throw new Error('Failed to trigger ingestion') } // ============================================================================ // MOCK DATA // ============================================================================ const MOCK_SOURCES: DSFASource[] = [ { id: '1', sourceCode: 'WP248', name: 'WP248 rev.01 - Leitlinien zur DSFA', fullName: 'Leitlinien zur Datenschutz-Folgenabschaetzung', organization: 'Artikel-29-Datenschutzgruppe / EDPB', sourceUrl: 'https://ec.europa.eu/newsroom/article29/items/611236/en', licenseCode: 'EDPB-LICENSE', licenseName: 'EDPB Document License', attributionRequired: true, attributionText: 'Quelle: WP248 rev.01, Artikel-29-Datenschutzgruppe (2017)', documentType: 'guideline', language: 'de', }, { id: '2', sourceCode: 'DSK_KP5', name: 'Kurzpapier Nr. 5 - DSFA nach Art. 35 DS-GVO', organization: 'Datenschutzkonferenz (DSK)', sourceUrl: 'https://www.datenschutzkonferenz-online.de/media/kp/dsk_kpnr_5.pdf', licenseCode: 'DL-DE-BY-2.0', licenseName: 'Datenlizenz DE – Namensnennung 2.0', licenseUrl: 'https://www.govdata.de/dl-de/by-2-0', attributionRequired: true, attributionText: 'Quelle: DSK Kurzpapier Nr. 5 (Stand: 2018)', documentType: 'guideline', language: 'de', }, { id: '3', sourceCode: 'BFDI_MUSS_PUBLIC', name: 'BfDI DSFA-Liste (oeffentlicher Bereich)', organization: 'BfDI', sourceUrl: 'https://www.bfdi.bund.de', licenseCode: 'DL-DE-ZERO-2.0', licenseName: 'Datenlizenz DE – Zero 2.0', attributionRequired: false, attributionText: 'Quelle: BfDI, Liste gem. Art. 35 Abs. 4 DSGVO', documentType: 'checklist', language: 'de', }, { id: '4', sourceCode: 'NI_MUSS_PRIVATE', name: 'LfD NI DSFA-Liste (nicht-oeffentlich)', organization: 'LfD Niedersachsen', sourceUrl: 'https://www.lfd.niedersachsen.de/download/131098', licenseCode: 'DL-DE-BY-2.0', licenseName: 'Datenlizenz DE – Namensnennung 2.0', attributionRequired: true, attributionText: 'Quelle: LfD Niedersachsen, DSFA-Muss-Liste', documentType: 'checklist', language: 'de', }, ] const MOCK_STATS: DSFACorpusStats = { sources: [ { sourceId: '1', sourceCode: 'WP248', name: 'WP248 rev.01', organization: 'EDPB', licenseCode: 'EDPB-LICENSE', documentType: 'guideline', documentCount: 1, chunkCount: 50, lastIndexedAt: '2026-02-09T10:00:00Z', }, { sourceId: '2', sourceCode: 'DSK_KP5', name: 'DSK Kurzpapier Nr. 5', organization: 'DSK', licenseCode: 'DL-DE-BY-2.0', documentType: 'guideline', documentCount: 1, chunkCount: 35, lastIndexedAt: '2026-02-09T10:00:00Z', }, ], totalSources: 45, totalDocuments: 45, totalChunks: 850, qdrantCollection: 'bp_dsfa_corpus', qdrantPointsCount: 850, qdrantStatus: 'green', } // ============================================================================ // COMPONENTS // ============================================================================ function LicenseBadge({ licenseCode }: { licenseCode: DSFALicenseCode }) { const colorMap: Record = { 'DL-DE-BY-2.0': 'bg-blue-100 text-blue-700 border-blue-200', 'DL-DE-ZERO-2.0': 'bg-gray-100 text-gray-700 border-gray-200', 'CC-BY-4.0': 'bg-green-100 text-green-700 border-green-200', 'EDPB-LICENSE': 'bg-purple-100 text-purple-700 border-purple-200', 'PUBLIC_DOMAIN': 'bg-gray-100 text-gray-600 border-gray-200', 'PROPRIETARY': 'bg-amber-100 text-amber-700 border-amber-200', } return ( {DSFA_LICENSE_LABELS[licenseCode] || licenseCode} ) } function DocumentTypeBadge({ type }: { type?: string }) { if (!type) return null const colorMap: Record = { guideline: 'bg-indigo-100 text-indigo-700', checklist: 'bg-emerald-100 text-emerald-700', regulation: 'bg-red-100 text-red-700', template: 'bg-orange-100 text-orange-700', } return ( {DSFA_DOCUMENT_TYPE_LABELS[type as keyof typeof DSFA_DOCUMENT_TYPE_LABELS] || type} ) } function StatusIndicator({ status }: { status: string }) { const statusConfig: Record = { green: { color: 'text-green-500', icon: , label: 'Aktiv' }, yellow: { color: 'text-yellow-500', icon: , label: 'Ausstehend' }, red: { color: 'text-red-500', icon: , label: 'Fehler' }, } const config = statusConfig[status] || statusConfig.yellow return ( {config.icon} {config.label} ) } function SourceCard({ source, stats, onIngest, isIngesting }: { source: DSFASource stats?: DSFASourceStats onIngest: () => void isIngesting: boolean }) { const [isExpanded, setIsExpanded] = useState(false) return (
{source.sourceCode}

{source.name}

{source.organization && (

{source.organization}

)}
{stats && ( <> {stats.documentCount} Dok. {stats.chunkCount} Chunks )}
{source.attributionRequired && (
Attribution: {source.attributionText}
)}
{isExpanded && (
{source.sourceUrl && ( <>
Quelle:
Link
)} {source.licenseUrl && ( <>
Lizenz-URL:
{source.licenseName}
)}
Sprache:
{source.language}
{stats?.lastIndexedAt && ( <>
Zuletzt indexiert:
{new Date(stats.lastIndexedAt).toLocaleString('de-DE')}
)}
)}
) } function StatsOverview({ stats }: { stats: DSFACorpusStats }) { return (

Corpus-Statistik

{stats.totalSources}

Quellen

{stats.totalDocuments}

Dokumente

{stats.totalChunks.toLocaleString()}

Chunks

{stats.qdrantPointsCount.toLocaleString()}

Vektoren

Collection:{' '} {stats.qdrantCollection}

) } // ============================================================================ // MAIN PAGE // ============================================================================ export default function DSFADocumentManagerPage() { const [sources, setSources] = useState([]) const [stats, setStats] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [filterType, setFilterType] = useState('all') const [ingestingSource, setIngestingSource] = useState(null) const [isInitializing, setIsInitializing] = useState(false) useEffect(() => { async function loadData() { setIsLoading(true) try { const [sourcesData, statsData] = await Promise.all([ fetchSources(), fetchStats(), ]) setSources(sourcesData) setStats(statsData) setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load data') setSources(MOCK_SOURCES) setStats(MOCK_STATS) } finally { setIsLoading(false) } } loadData() }, []) const handleInitialize = async () => { setIsInitializing(true) try { await initializeCorpus() // Reload data const [sourcesData, statsData] = await Promise.all([ fetchSources(), fetchStats(), ]) setSources(sourcesData) setStats(statsData) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to initialize') } finally { setIsInitializing(false) } } const handleIngest = async (sourceCode: string) => { setIngestingSource(sourceCode) try { await triggerIngestion(sourceCode) // Reload stats const statsData = await fetchStats() setStats(statsData) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to ingest') } finally { setIngestingSource(null) } } // Filter sources const filteredSources = sources.filter(source => { const matchesSearch = searchQuery === '' || source.name.toLowerCase().includes(searchQuery.toLowerCase()) || source.sourceCode.toLowerCase().includes(searchQuery.toLowerCase()) || source.organization?.toLowerCase().includes(searchQuery.toLowerCase()) const matchesType = filterType === 'all' || source.documentType === filterType return matchesSearch && matchesType }) // Get stats by source code const getStatsForSource = (sourceCode: string): DSFASourceStats | undefined => { return stats?.sources.find(s => s.sourceCode === sourceCode) } return (
{/* Header */}
Zurueck zur RAG-Pipeline

DSFA-Quellen Manager

Verwalten Sie DSFA-Guidance Dokumente mit vollstaendiger Lizenzattribution

{/* Error Banner */} {error && (
{error}
)} {/* Stats Overview */} {stats && } {/* Search & Filter */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800" />
{/* Sources List */}

Registrierte Quellen ({filteredSources.length})

{isLoading ? (

Lade Quellen...

) : filteredSources.length === 0 ? (

{searchQuery || filterType !== 'all' ? 'Keine Quellen gefunden' : 'Noch keine Quellen registriert'}

{!searchQuery && filterType === 'all' && ( )}
) : (
{filteredSources.map(source => ( handleIngest(source.sourceCode)} isIngesting={ingestingSource === source.sourceCode} /> ))}
)}
{/* Info Box */}

Ueber die Lizenzattribution

Alle DSFA-Quellen werden mit vollstaendiger Lizenzinformation gespeichert. Bei der Nutzung der RAG-Suche werden automatisch die korrekten Attributionen angezeigt.

Namensnennung
Keine Attribution
CC Attribution
) }