Fix: Remove broken getKlausurApiUrl and clean up empty lines
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s

sed replacement left orphaned hostname references in story page
and empty lines in getApiBase functions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-24 16:02:04 +02:00
parent b07f802c24
commit 9ba420fa91
150 changed files with 30231 additions and 32053 deletions

View File

@@ -0,0 +1,195 @@
'use client'
import React from 'react'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface DataTabProps {
hook: UseRAGPageReturn
}
export function DataTab({ hook }: DataTabProps) {
const {
customDocuments,
uploadFile,
setUploadFile,
uploadTitle,
setUploadTitle,
uploadCode,
setUploadCode,
uploading,
handleUpload,
linkUrl,
setLinkUrl,
linkTitle,
setLinkTitle,
linkCode,
setLinkCode,
addingLink,
handleAddLink,
handleDeleteDocument,
fetchCustomDocuments,
} = hook
return (
<div className="space-y-6">
{/* Upload Document */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Dokument hochladen (PDF)</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">PDF-Datei</label>
<input
type="file"
accept=".pdf"
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
className="w-full px-3 py-2 border rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Titel</label>
<input
type="text"
value={uploadTitle}
onChange={(e) => setUploadTitle(e.target.value)}
placeholder="z.B. Firmen-Datenschutzrichtlinie"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Code (eindeutig)</label>
<input
type="text"
value={uploadCode}
onChange={(e) => setUploadCode(e.target.value.toUpperCase())}
placeholder="z.B. CUSTOM-DSR-01"
className="w-full px-3 py-2 border rounded-lg font-mono"
/>
</div>
</div>
<button
onClick={handleUpload}
disabled={uploading || !uploadFile || !uploadTitle || !uploadCode}
className="mt-4 px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{uploading ? 'Wird hochgeladen...' : 'Hochladen & Indexieren'}
</button>
</div>
{/* Add Link */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Link hinzufuegen (Webseite/PDF)</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">URL</label>
<input
type="url"
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
placeholder="https://example.com/document.pdf"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Titel</label>
<input
type="text"
value={linkTitle}
onChange={(e) => setLinkTitle(e.target.value)}
placeholder="z.B. BSI IT-Grundschutz"
className="w-full px-3 py-2 border rounded-lg"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Code (eindeutig)</label>
<input
type="text"
value={linkCode}
onChange={(e) => setLinkCode(e.target.value.toUpperCase())}
placeholder="z.B. BSI-GRUNDSCHUTZ"
className="w-full px-3 py-2 border rounded-lg font-mono"
/>
</div>
</div>
<button
onClick={handleAddLink}
disabled={addingLink || !linkUrl || !linkTitle || !linkCode}
className="mt-4 px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{addingLink ? 'Wird hinzugefuegt...' : 'Link hinzufuegen & Indexieren'}
</button>
</div>
{/* Custom Documents List */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">Eigene Dokumente ({customDocuments.length})</h3>
<button
onClick={fetchCustomDocuments}
className="text-sm text-teal-600 hover:text-teal-700"
>
Aktualisieren
</button>
</div>
{customDocuments.length === 0 ? (
<div className="p-8 text-center text-slate-500">
Noch keine eigenen Dokumente hinzugefuegt.
</div>
) : (
<div className="divide-y">
{customDocuments.map((doc) => (
<div key={doc.id} className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center text-lg">
{doc.url ? '🔗' : '📄'}
</span>
<div>
<p className="font-medium text-slate-900">{doc.title}</p>
<p className="text-sm text-slate-500">
<span className="font-mono text-teal-600">{doc.code}</span>
{' • '}
{doc.filename || doc.url}
</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`px-2 py-1 rounded text-xs font-medium ${
doc.status === 'indexed' ? 'bg-green-100 text-green-700' :
doc.status === 'error' ? 'bg-red-100 text-red-700' :
doc.status === 'processing' || doc.status === 'fetching' ? 'bg-blue-100 text-blue-700' :
'bg-slate-100 text-slate-700'
}`}>
{doc.status === 'indexed' ? `${doc.chunk_count} Chunks` :
doc.status === 'error' ? 'Fehler' :
doc.status === 'processing' ? 'Verarbeitung...' :
doc.status === 'fetching' ? 'Abruf...' :
doc.status}
</span>
<button
onClick={() => handleDeleteDocument(doc.id)}
className="text-red-500 hover:text-red-700 text-sm"
>
Loeschen
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Info Box */}
<div className="bg-teal-50 border border-teal-200 rounded-xl p-6">
<h4 className="font-semibold text-teal-800 flex items-center gap-2">
<span></span>
Hinweis zur Verwendung
</h4>
<p className="text-sm text-teal-700 mt-2">
Laden Sie eigene Dokumente (z.B. interne Datenschutzrichtlinien, Vertraege) oder
externe Links hoch. Diese werden automatisch in Chunks aufgeteilt und indexiert.
Nach dem Hinzufuegen koennen Sie im <strong>Pipeline</strong>-Tab die vollstaendige
Compliance-Analyse starten.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,69 @@
'use client'
import React from 'react'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface IngestionTabProps {
hook: UseRAGPageReturn
}
export function IngestionTab({ hook }: IngestionTabProps) {
const { ingestionRunning, ingestionLog, triggerIngestion } = hook
return (
<div className="space-y-6">
{/* Ingestion Control */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Legal Corpus Re-Ingestion</h3>
<p className="text-slate-600 mb-4">
Startet die Neuindexierung aller 19 Regulierungen. Die Dokumente werden von EUR-Lex,
gesetze-im-internet.de und BSI heruntergeladen, in semantische Chunks aufgeteilt und
mit BGE-M3 Embeddings in Qdrant indexiert.
</p>
<div className="flex items-center gap-4">
<button
onClick={triggerIngestion}
disabled={ingestionRunning}
className="px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{ingestionRunning ? 'Laeuft...' : 'Re-Ingestion starten'}
</button>
{ingestionRunning && (
<span className="flex items-center gap-2 text-teal-600">
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
</svg>
Ingestion laeuft...
</span>
)}
</div>
</div>
{/* Ingestion Log */}
{ingestionLog.length > 0 && (
<div className="bg-slate-900 rounded-xl p-4">
<h4 className="text-slate-400 text-sm mb-2">Log</h4>
<div className="font-mono text-sm text-green-400 space-y-1 max-h-64 overflow-y-auto">
{ingestionLog.map((line, i) => (
<div key={i}>{line}</div>
))}
</div>
</div>
)}
{/* Info Box */}
<div className="bg-teal-50 border border-teal-200 rounded-xl p-6">
<h4 className="font-semibold text-teal-800 flex items-center gap-2">
<span>💡</span>
Hinweis zur Datenquelle
</h4>
<p className="text-sm text-teal-700 mt-2">
Alle indexierten Dokumente sind amtliche Werke (§5 UrhG) und damit urheberrechtsfrei.
Sie werden nur fuer RAG/Retrieval verwendet, nicht fuer Modell-Training.
Die Daten werden lokal auf dem Mac Mini verarbeitet und nicht an externe Dienste gesendet.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,373 @@
'use client'
import React from 'react'
import {
REGULATIONS,
DOC_TYPES,
INDUSTRIES_LIST,
INDUSTRIES,
INDUSTRY_REGULATION_MAP,
TYPE_COLORS,
THEMATIC_GROUPS,
KEY_INTERSECTIONS,
RAG_DOCUMENTS,
isInRag,
} from '../rag-data'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
import {
FutureOutlookSection,
RagCoverageSection,
FutureRegulationsSection,
LegalBasisSection,
} from './MapTabSections'
interface MapTabProps {
hook: UseRAGPageReturn
}
export function MapTab({ hook }: MapTabProps) {
const {
expandedRegulation,
setExpandedRegulation,
expandedDocTypes,
setExpandedDocTypes,
expandedMatrixDoc,
setExpandedMatrixDoc,
setActiveTab,
} = hook
return (
<div className="space-y-6">
{/* Industry Filter */}
<IndustryFilter
expandedRegulation={expandedRegulation}
setExpandedRegulation={setExpandedRegulation}
/>
{/* Thematic Groups */}
<ThematicGroupsSection setActiveTab={setActiveTab} setExpandedRegulation={setExpandedRegulation} />
{/* Key Intersections */}
<KeyIntersectionsSection />
{/* Regulation Matrix */}
<RegulationMatrix
expandedDocTypes={expandedDocTypes}
setExpandedDocTypes={setExpandedDocTypes}
expandedMatrixDoc={expandedMatrixDoc}
setExpandedMatrixDoc={setExpandedMatrixDoc}
/>
{/* Future Outlook Section */}
<FutureOutlookSection />
{/* RAG Coverage Overview */}
<RagCoverageSection />
{/* Potential Future Regulations */}
<FutureRegulationsSection />
{/* Legal Basis Info */}
<LegalBasisSection />
</div>
)
}
// --- Sub-components ---
function IndustryFilter({
expandedRegulation,
setExpandedRegulation,
}: {
expandedRegulation: string | null
setExpandedRegulation: (v: string | null) => void
}) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Regulierungen nach Branche</h3>
<p className="text-sm text-slate-500 mb-4">
Waehlen Sie Ihre Branche, um relevante Regulierungen zu sehen.
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{INDUSTRIES.map((industry) => {
const regs = INDUSTRY_REGULATION_MAP[industry.id] || []
return (
<button
key={industry.id}
onClick={() => setExpandedRegulation(industry.id === expandedRegulation ? null : industry.id)}
className={`p-4 rounded-lg border text-left transition-all ${
expandedRegulation === industry.id
? 'border-teal-500 bg-teal-50 ring-2 ring-teal-200'
: 'border-slate-200 hover:border-slate-300 hover:bg-slate-50'
}`}
>
<div className="text-2xl mb-2">{industry.icon}</div>
<div className="font-medium text-slate-900 text-sm">{industry.name}</div>
<div className="text-xs text-slate-500 mt-1">{regs.length} Regulierungen</div>
</button>
)
})}
</div>
{/* Selected Industry Details */}
{expandedRegulation && INDUSTRIES.find(i => i.id === expandedRegulation) && (
<div className="mt-6 p-4 bg-slate-50 rounded-lg">
{(() => {
const industry = INDUSTRIES.find(i => i.id === expandedRegulation)!
const regCodes = INDUSTRY_REGULATION_MAP[industry.id] || []
const regs = REGULATIONS.filter(r => regCodes.includes(r.code))
return (
<>
<div className="flex items-center gap-3 mb-4">
<span className="text-3xl">{industry.icon}</span>
<div>
<h4 className="font-semibold text-slate-900">{industry.name}</h4>
<p className="text-sm text-slate-500">{industry.description}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{regs.map((reg) => {
const regInRag = isInRag(reg.code)
return (
<div
key={reg.code}
className={`bg-white p-3 rounded-lg border ${regInRag ? 'border-green-200' : 'border-slate-200'}`}
>
<div className="flex items-center gap-2 mb-1">
<span className={`px-2 py-0.5 text-xs rounded ${TYPE_COLORS[reg.type]}`}>
{reg.code}
</span>
{regInRag ? (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-green-100 text-green-600 rounded">RAG</span>
) : (
<span className="px-1.5 py-0.5 text-[10px] font-bold bg-red-50 text-red-400 rounded"></span>
)}
</div>
<div className="font-medium text-sm text-slate-900">{reg.name}</div>
<div className="text-xs text-slate-500 mt-1 line-clamp-2">{reg.description}</div>
</div>
)
})}
</div>
</>
)
})()}
</div>
)}
</div>
)
}
function ThematicGroupsSection({
setActiveTab,
setExpandedRegulation,
}: {
setActiveTab: (v: any) => void
setExpandedRegulation: (v: string | null) => void
}) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Thematische Cluster</h3>
<p className="text-sm text-slate-500 mb-4">
Regulierungen gruppiert nach Themenbereichen - zeigt Ueberschneidungen.
</p>
<div className="space-y-4">
{THEMATIC_GROUPS.map((group) => (
<div key={group.id} className="border border-slate-200 rounded-lg overflow-hidden">
<div className={`${group.color} px-4 py-2 text-white font-medium flex items-center justify-between`}>
<span>{group.name}</span>
<span className="text-sm opacity-80">{group.regulations.length} Regulierungen</span>
</div>
<div className="p-4">
<p className="text-sm text-slate-600 mb-3">{group.description}</p>
<div className="flex flex-wrap gap-2">
{group.regulations.map((code) => {
const reg = REGULATIONS.find(r => r.code === code)
const codeInRag = isInRag(code)
return (
<span
key={code}
className={`px-3 py-1.5 rounded-full text-sm font-medium cursor-pointer ${
codeInRag
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
}`}
onClick={() => {
setActiveTab('regulations')
setExpandedRegulation(code)
}}
title={`${reg?.fullName || code}${codeInRag ? ' (im RAG)' : ' (nicht im RAG)'}`}
>
{codeInRag ? '✓ ' : '✗ '}{code}
</span>
)
})}
</div>
</div>
</div>
))}
</div>
</div>
)
}
function KeyIntersectionsSection() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Wichtige Schnittstellen</h3>
<p className="text-sm text-slate-500 mb-4">
Bereiche, in denen sich mehrere Regulierungen ueberschneiden und zusammenwirken.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{KEY_INTERSECTIONS.map((intersection, idx) => (
<div key={idx} className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-lg p-4 border border-slate-200">
<div className="flex flex-wrap gap-1 mb-2">
{intersection.regulations.map((code) => (
<span
key={code}
className={`px-2 py-0.5 text-xs font-medium rounded ${
isInRag(code)
? 'bg-green-100 text-green-700'
: 'bg-red-50 text-red-500'
}`}
>
{isInRag(code) ? '✓ ' : '✗ '}{code}
</span>
))}
</div>
<div className="font-medium text-slate-900 text-sm mb-1">{intersection.topic}</div>
<div className="text-xs text-slate-500">{intersection.description}</div>
</div>
))}
</div>
</div>
)
}
function RegulationMatrix({
expandedDocTypes,
setExpandedDocTypes,
expandedMatrixDoc,
setExpandedMatrixDoc,
}: {
expandedDocTypes: string[]
setExpandedDocTypes: (fn: (prev: string[]) => string[]) => void
expandedMatrixDoc: string | null
setExpandedMatrixDoc: (v: string | null) => void
}) {
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50">
<h3 className="font-semibold text-slate-900">Branchen-Regulierungs-Matrix</h3>
<p className="text-sm text-slate-500">{RAG_DOCUMENTS.length} Dokumente in {DOC_TYPES.length} Kategorien</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead className="bg-slate-50 border-b sticky top-0 z-10">
<tr>
<th className="px-2 py-2 text-left font-medium text-slate-500 sticky left-0 bg-slate-50 min-w-[200px]">Regulierung</th>
{INDUSTRIES_LIST.filter((i: any) => i.id !== 'all').map((industry: any) => (
<th key={industry.id} className="px-2 py-2 text-center font-medium text-slate-500 min-w-[60px]">
<div className="flex flex-col items-center">
<span className="text-lg">{industry.icon}</span>
<span className="text-[10px] leading-tight">{industry.name.split('/')[0]}</span>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{DOC_TYPES.map((docType: any) => {
const docsInType = RAG_DOCUMENTS.filter((d: any) => d.doc_type === docType.id)
if (docsInType.length === 0) return null
const isExpanded = expandedDocTypes.includes(docType.id)
return (
<React.Fragment key={docType.id}>
<tr
className="bg-slate-100 border-t-2 border-slate-300 cursor-pointer hover:bg-slate-200"
onClick={() => {
setExpandedDocTypes(prev =>
prev.includes(docType.id)
? prev.filter((id: string) => id !== docType.id)
: [...prev, docType.id]
)
}}
>
<td colSpan={INDUSTRIES_LIST.length} className="px-3 py-2 font-bold text-slate-700">
<span className="mr-2">{isExpanded ? '\u25BC' : '\u25B6'}</span>
{docType.icon} {docType.label} ({docsInType.length})
</td>
</tr>
{isExpanded && docsInType.map((doc: any) => (
<React.Fragment key={doc.code}>
<tr
className={`hover:bg-slate-50 border-b border-slate-100 cursor-pointer ${expandedMatrixDoc === doc.code ? 'bg-teal-50' : ''}`}
onClick={() => setExpandedMatrixDoc(expandedMatrixDoc === doc.code ? null : doc.code)}
>
<td className="px-2 py-1.5 font-medium sticky left-0 bg-white">
<span className="flex items-center gap-1">
{isInRag(doc.code) ? (
<span className="text-green-500 text-[10px]"></span>
) : (
<span className="text-red-300 text-[10px]"></span>
)}
<span className="text-teal-600 truncate max-w-[180px]" title={doc.full_name || doc.name}>
{doc.name}
</span>
{(doc.applicability_note || doc.description) && (
<span className="text-slate-400 text-[10px] ml-1">{expandedMatrixDoc === doc.code ? '▼' : 'ⓘ'}</span>
)}
</span>
</td>
{INDUSTRIES_LIST.filter((i: any) => i.id !== 'all').map((industry: any) => {
const applies = doc.industries.includes(industry.id) || doc.industries.includes('all')
return (
<td key={industry.id} className="px-2 py-1.5 text-center">
{applies ? (
<span className="inline-flex items-center justify-center w-5 h-5 bg-teal-100 text-teal-600 rounded-full"></span>
) : (
<span className="inline-flex items-center justify-center w-5 h-5 text-slate-300"></span>
)}
</td>
)
})}
</tr>
{expandedMatrixDoc === doc.code && (doc.applicability_note || doc.description) && (
<tr className="bg-teal-50 border-b border-teal-200">
<td colSpan={INDUSTRIES_LIST.length} className="px-4 py-3">
<div className="text-xs space-y-1.5">
{doc.full_name && (
<p className="font-semibold text-slate-700">{doc.full_name}</p>
)}
{doc.applicability_note && (
<p className="text-teal-700 bg-teal-100 px-2 py-1 rounded inline-block">
<span className="font-medium">Branchenrelevanz:</span> {doc.applicability_note}
</p>
)}
{doc.description && (
<p className="text-slate-600">{doc.description}</p>
)}
{doc.effective_date && (
<p className="text-slate-400">In Kraft: {doc.effective_date}</p>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
))}
</React.Fragment>
)
})}
</tbody>
</table>
</div>
</div>
)
}
// FutureOutlookSection, RagCoverageSection, FutureRegulationsSection,
// LegalBasisSection are imported from ./MapTabSections.tsx

View File

@@ -0,0 +1,199 @@
'use client'
import React from 'react'
import { REGULATIONS_IN_RAG } from '../rag-constants'
import {
RAG_DOCUMENTS,
FUTURE_OUTLOOK,
ADDITIONAL_REGULATIONS,
LEGAL_BASIS_INFO,
isInRag,
} from '../rag-data'
export function FutureOutlookSection() {
return (
<div className="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-xl border border-indigo-200 p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">🔮</span>
<div>
<h3 className="font-semibold text-slate-900">Zukunftsaussicht</h3>
<p className="text-sm text-slate-500">Geplante Aenderungen und neue Regulierungen</p>
</div>
</div>
<div className="space-y-4">
{FUTURE_OUTLOOK.map((item) => (
<div key={item.id} className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<div className="px-4 py-3 flex items-center justify-between bg-slate-50 border-b">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 text-xs font-medium rounded ${
item.status === 'proposed' ? 'bg-yellow-100 text-yellow-700' :
item.status === 'agreed' ? 'bg-green-100 text-green-700' :
item.status === 'withdrawn' ? 'bg-red-100 text-red-700' :
'bg-blue-100 text-blue-700'
}`}>
{item.statusLabel}
</span>
<h4 className="font-semibold text-slate-900">{item.name}</h4>
</div>
<span className="text-sm text-slate-500">Erwartet: {item.expectedDate}</span>
</div>
<div className="p-4">
<p className="text-sm text-slate-600 mb-3">{item.description}</p>
<div className="mb-3">
<p className="text-xs font-medium text-slate-500 uppercase mb-2">Wichtige Aenderungen:</p>
<ul className="text-sm text-slate-600 space-y-1">
{item.keyChanges.slice(0, 4).map((change, idx) => (
<li key={idx} className="flex items-start gap-2">
<span className="text-teal-500 mt-1"></span>
<span>{change}</span>
</li>
))}
{item.keyChanges.length > 4 && (
<li className="text-slate-400 text-xs">+ {item.keyChanges.length - 4} weitere...</li>
)}
</ul>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1">
{item.affectedRegulations.map((code) => (
<span key={code} className="px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded">
{code}
</span>
))}
</div>
<a
href={item.source}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-teal-600 hover:underline"
>
Quelle
</a>
</div>
</div>
</div>
))}
</div>
</div>
)
}
export function RagCoverageSection() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl"></span>
<div>
<h3 className="font-semibold text-slate-900">RAG-Abdeckung ({Object.keys(REGULATIONS_IN_RAG).length} von {RAG_DOCUMENTS.length} Regulierungen)</h3>
<p className="text-sm text-slate-500">Stand: Maerz 2026 Alle im RAG-System verfuegbaren Regulierungen (inkl. Verbraucherschutz Phase H)</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
{RAG_DOCUMENTS.filter((r: any) => isInRag(r.code)).map((reg: any) => (
<span key={reg.code} className="px-2.5 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full border border-green-200">
{reg.code}
</span>
))}
</div>
<div className="mt-4 pt-4 border-t border-slate-100">
<p className="text-xs font-medium text-slate-500 mb-2">Noch nicht im RAG:</p>
<div className="flex flex-wrap gap-2">
{RAG_DOCUMENTS.filter((r: any) => !isInRag(r.code)).map((reg: any) => (
<span key={reg.code} className="px-2.5 py-1 text-xs font-medium bg-red-50 text-red-400 rounded-full border border-red-100">
{reg.code}
</span>
))}
</div>
</div>
</div>
)
}
export function FutureRegulationsSection() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">🔮</span>
<div>
<h3 className="font-semibold text-slate-900">Zukuenftige Regulierungen</h3>
<p className="text-sm text-slate-500">Noch nicht verabschiedet oder zur Erweiterung vorgesehen</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{ADDITIONAL_REGULATIONS.map((reg) => (
<div key={reg.code} className={`rounded-lg border p-4 ${
reg.status === 'active' ? 'border-green-200 bg-green-50' : 'border-yellow-200 bg-yellow-50'
}`}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 text-xs font-bold rounded ${
reg.type === 'eu_regulation' ? 'bg-blue-100 text-blue-700' : 'bg-purple-100 text-purple-700'
}`}>
{reg.code}
</span>
<span className={`px-2 py-0.5 text-xs rounded ${
reg.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
}`}>
{reg.status === 'active' ? 'In Kraft' : 'Vorgeschlagen'}
</span>
</div>
<span className={`px-2 py-0.5 text-xs rounded ${
reg.priority === 'high' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-600'
}`}>
{reg.priority === 'high' ? 'Hohe Prioritaet' : 'Mittel'}
</span>
</div>
<h4 className="font-medium text-slate-900 text-sm mb-1">{reg.name}</h4>
<p className="text-xs text-slate-600 mb-2">{reg.description}</p>
<div className="flex items-center justify-between text-xs">
<span className="text-slate-500">Ab: {reg.effectiveDate}</span>
{reg.celex && (
<a
href={`https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:${reg.celex}`}
target="_blank"
rel="noopener noreferrer"
className="text-teal-600 hover:underline"
>
EUR-Lex
</a>
)}
</div>
</div>
))}
</div>
</div>
)
}
export function LegalBasisSection() {
return (
<div className="bg-emerald-50 rounded-xl border border-emerald-200 p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl"></span>
<div>
<h3 className="font-semibold text-slate-900">{LEGAL_BASIS_INFO.title}</h3>
<p className="text-sm text-emerald-700">{LEGAL_BASIS_INFO.summary}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{LEGAL_BASIS_INFO.details.map((detail, idx) => (
<div key={idx} className="bg-white rounded-lg border border-emerald-100 p-3">
<div className="flex items-center gap-2 mb-1">
<span className={`px-2 py-0.5 text-xs font-medium rounded ${
detail.status === 'Erlaubt' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
}`}>
{detail.status}
</span>
<span className="font-medium text-sm text-slate-900">{detail.aspect}</span>
</div>
<p className="text-xs text-slate-600">{detail.explanation}</p>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,113 @@
'use client'
import React from 'react'
import { REGULATIONS_IN_RAG } from '../rag-constants'
import {
REGULATIONS,
COLLECTION_TOTALS,
TYPE_LABELS,
TYPE_COLORS,
isInRag,
getKnownChunks,
} from '../rag-data'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface OverviewTabProps {
hook: UseRAGPageReturn
}
export function OverviewTab({ hook }: OverviewTabProps) {
const {
dsfaLoading,
dsfaStatus,
dsfaSources,
setRegulationCategory,
setActiveTab,
} = hook
return (
<div className="space-y-6">
{/* RAG Categories Overview */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">RAG-Kategorien</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={() => { setRegulationCategory('regulations'); setActiveTab('regulations') }}
className="p-4 rounded-lg border border-blue-200 bg-blue-50 hover:bg-blue-100 transition-colors text-left"
>
<p className="text-xs font-medium text-blue-600 uppercase">Gesetze & Regulierungen</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{COLLECTION_TOTALS.total_legal.toLocaleString()}</p>
<p className="text-xs text-slate-500 mt-1">{Object.keys(REGULATIONS_IN_RAG).length}/{REGULATIONS.length} im RAG</p>
</button>
<button
onClick={() => { setRegulationCategory('dsfa'); setActiveTab('regulations') }}
className="p-4 rounded-lg border border-purple-200 bg-purple-50 hover:bg-purple-100 transition-colors text-left"
>
<p className="text-xs font-medium text-purple-600 uppercase">DSFA Corpus</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{dsfaLoading ? '-' : (dsfaStatus?.total_chunks || 0).toLocaleString()}</p>
<p className="text-xs text-slate-500 mt-1">{dsfaSources.length || '~70'} Quellen (WP248, DSK, Gesetze)</p>
</button>
<div className="p-4 rounded-lg border border-emerald-200 bg-emerald-50 text-left">
<p className="text-xs font-medium text-emerald-600 uppercase">NiBiS EH</p>
<p className="text-2xl font-bold text-slate-900 mt-1">7.996</p>
<p className="text-xs text-slate-500 mt-1">Chunks &middot; Bildungs-Erwartungshorizonte</p>
</div>
<div className="p-4 rounded-lg border border-orange-200 bg-orange-50 text-left">
<p className="text-xs font-medium text-orange-600 uppercase">Legal Templates</p>
<p className="text-2xl font-bold text-slate-900 mt-1">7.689</p>
<p className="text-xs text-slate-500 mt-1">Chunks &middot; Dokumentvorlagen (VVT, TOM, DSFA)</p>
</div>
</div>
</div>
{/* Quick Stats per Type */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{Object.entries(TYPE_LABELS).map(([type, label]) => {
const regs = REGULATIONS.filter((r) => r.type === type)
const inRagCount = regs.filter((r) => isInRag(r.code)).length
const totalChunks = regs.reduce((sum, r) => sum + getKnownChunks(r.code), 0)
return (
<div key={type} className="bg-white rounded-xl p-4 border border-slate-200">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-0.5 text-xs rounded ${TYPE_COLORS[type]}`}>{label}</span>
<span className="text-slate-500 text-sm">{inRagCount}/{regs.length} im RAG</span>
</div>
<p className="text-xl font-bold text-slate-900">{totalChunks.toLocaleString()} Chunks</p>
</div>
)
})}
</div>
{/* Top Regulations */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50">
<h3 className="font-semibold text-slate-900">Top Regulierungen (nach Chunks)</h3>
</div>
<div className="divide-y">
{[...REGULATIONS].sort((a, b) => getKnownChunks(b.code) - getKnownChunks(a.code))
.slice(0, 10)
.map((reg) => {
const chunks = getKnownChunks(reg.code)
return (
<div key={reg.code} className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
{isInRag(reg.code) ? (
<span className="text-green-500 text-sm"></span>
) : (
<span className="text-red-400 text-sm"></span>
)}
<span className={`px-2 py-0.5 text-xs rounded ${TYPE_COLORS[reg.type]}`}>
{TYPE_LABELS[reg.type]}
</span>
<span className="font-medium text-slate-900">{reg.name}</span>
<span className="text-slate-500 text-sm">({reg.code})</span>
</div>
<span className={`font-bold ${chunks > 0 ? 'text-teal-600' : 'text-slate-300'}`}>{chunks > 0 ? chunks.toLocaleString() + ' Chunks' : '—'}</span>
</div>
)
})}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,410 @@
'use client'
import React from 'react'
import type { PipelineCheckpoint } from '../types'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface PipelineTabProps {
hook: UseRAGPageReturn
}
export function PipelineTab({ hook }: PipelineTabProps) {
const {
pipelineState,
pipelineLoading,
pipelineStarting,
autoRefresh,
setAutoRefresh,
elapsedTime,
fetchPipeline,
handleStartPipeline,
collectionStatus,
} = hook
return (
<div className="space-y-6">
{/* Pipeline Header */}
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4">
<h3 className="text-lg font-semibold text-slate-900">Compliance Pipeline Status</h3>
{pipelineState?.status === 'running' && elapsedTime && (
<div className="flex items-center gap-2 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-full">
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
<span className="text-sm font-medium text-blue-700">Laufzeit: {elapsedTime}</span>
</div>
)}
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-sm text-slate-600 cursor-pointer">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="w-4 h-4 text-teal-600 rounded border-slate-300 focus:ring-teal-500"
/>
Auto-Refresh
</label>
{(!pipelineState || pipelineState.status !== 'running') && (
<button
onClick={() => handleStartPipeline(false)}
disabled={pipelineStarting}
className="flex items-center gap-2 px-4 py-2 text-sm bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{pipelineStarting ? (
<SpinnerIcon />
) : (
<PlayIcon />
)}
Pipeline starten
</button>
)}
<button
onClick={fetchPipeline}
disabled={pipelineLoading}
className="flex items-center gap-2 px-4 py-2 text-sm bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{pipelineLoading ? <SpinnerIcon /> : <RefreshIcon />}
Aktualisieren
</button>
</div>
</div>
{/* No Data */}
{(!pipelineState || pipelineState.status === 'no_data') && !pipelineLoading && (
<NoDataCard pipelineStarting={pipelineStarting} handleStartPipeline={handleStartPipeline} />
)}
{/* Pipeline Status */}
{pipelineState && pipelineState.status !== 'no_data' && (
<>
{/* Status Card */}
<PipelineStatusCard pipelineState={pipelineState} />
{/* Current Progress */}
{pipelineState.status === 'running' && pipelineState.current_phase && (
<CurrentProgressCard pipelineState={pipelineState} collectionStatus={collectionStatus} />
)}
{/* Validation Summary */}
{pipelineState.validation_summary && (
<ValidationSummary summary={pipelineState.validation_summary} />
)}
{/* Checkpoints */}
<CheckpointsList checkpoints={pipelineState.checkpoints} />
{/* Summary */}
{Object.keys(pipelineState.summary || {}).length > 0 && (
<PipelineSummary summary={pipelineState.summary} />
)}
</>
)}
</div>
)
}
// --- Icons ---
function SpinnerIcon() {
return (
<svg className="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
</svg>
)
}
function PlayIcon() {
return (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)
}
function RefreshIcon() {
return (
<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>
)
}
// --- Sub-components ---
function NoDataCard({
pipelineStarting,
handleStartPipeline,
}: {
pipelineStarting: boolean
handleStartPipeline: (skip: boolean) => void
}) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-slate-100 flex items-center justify-center">
<svg className="w-8 h-8 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>
<h4 className="text-lg font-semibold text-slate-900 mb-2">Keine Pipeline-Daten</h4>
<p className="text-slate-600 mb-4">
Es wurde noch keine Pipeline ausgefuehrt. Starten Sie die Compliance-Pipeline um Checkpoint-Daten zu sehen.
</p>
<button
onClick={() => handleStartPipeline(false)}
disabled={pipelineStarting}
className="inline-flex items-center gap-2 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{pipelineStarting ? (
<>
<svg className="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
</svg>
Startet...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Pipeline jetzt starten
</>
)}
</button>
</div>
)
}
function PipelineStatusCard({ pipelineState }: { pipelineState: any }) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
pipelineState.status === 'completed' ? 'bg-green-100' :
pipelineState.status === 'running' ? 'bg-blue-100' :
pipelineState.status === 'failed' ? 'bg-red-100' : 'bg-slate-100'
}`}>
{pipelineState.status === 'completed' && (
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
)}
{pipelineState.status === 'running' && (
<svg className="w-6 h-6 text-blue-600 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.7.689 3 7.938l3-2.647z" />
</svg>
)}
{pipelineState.status === 'failed' && (
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</div>
<div>
<h4 className="font-semibold text-slate-900">Pipeline {pipelineState.pipeline_id}</h4>
<p className="text-sm text-slate-500">
Gestartet: {pipelineState.started_at ? new Date(pipelineState.started_at).toLocaleString('de-DE') : '-'}
{pipelineState.completed_at && ` | Beendet: ${new Date(pipelineState.completed_at).toLocaleString('de-DE')}`}
</p>
</div>
</div>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${
pipelineState.status === 'completed' ? 'bg-green-100 text-green-700' :
pipelineState.status === 'running' ? 'bg-blue-100 text-blue-700' :
pipelineState.status === 'failed' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700'
}`}>
{pipelineState.status === 'completed' ? 'Abgeschlossen' :
pipelineState.status === 'running' ? 'Laeuft' :
pipelineState.status === 'failed' ? 'Fehlgeschlagen' : pipelineState.status}
</span>
</div>
</div>
)
}
function CurrentProgressCard({ pipelineState, collectionStatus }: { pipelineState: any; collectionStatus: any }) {
return (
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200 p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="font-semibold text-blue-900 flex items-center gap-2">
<svg className="w-5 h-5 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Aktuelle Verarbeitung
</h4>
<span className="text-sm text-blue-600">Phase: {pipelineState.current_phase}</span>
</div>
{/* Phase Progress Indicator */}
<div className="flex items-center gap-2 mb-4">
{['ingestion', 'extraction', 'controls', 'measures'].map((phase, idx) => (
<div key={phase} className="flex-1 flex items-center">
<div className={`flex-1 h-2 rounded-full ${
pipelineState.current_phase === phase ? 'bg-blue-500 animate-pulse' :
pipelineState.checkpoints?.some((c: PipelineCheckpoint) => c.phase === phase && c.status === 'completed') ? 'bg-green-500' :
'bg-slate-200'
}`} />
{idx < 3 && <div className="w-2" />}
</div>
))}
</div>
<div className="flex justify-between text-xs text-slate-500 mb-4">
<span>Ingestion</span>
<span>Extraktion</span>
<span>Controls</span>
<span>Massnahmen</span>
</div>
{/* Current checkpoint details */}
{pipelineState.checkpoints?.filter((c: PipelineCheckpoint) => c.status === 'running').map((checkpoint: PipelineCheckpoint, idx: number) => (
<div key={idx} className="bg-white/60 rounded-lg p-4 mt-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-pulse" />
<span className="font-medium text-slate-900">{checkpoint.name}</span>
</div>
{checkpoint.metrics && Object.keys(checkpoint.metrics).length > 0 && (
<div className="flex gap-2">
{Object.entries(checkpoint.metrics).slice(0, 3).map(([key, value]) => (
<span key={key} className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
{key.replace(/_/g, ' ')}: {typeof value === 'number' ? value.toLocaleString() : String(value)}
</span>
))}
</div>
)}
</div>
</div>
))}
{/* Live chunk count */}
<div className="mt-4 flex items-center justify-between text-sm">
<span className="text-slate-600">Chunks in Qdrant:</span>
<span className="font-bold text-blue-700">{collectionStatus?.totalPoints?.toLocaleString() || '-'}</span>
</div>
</div>
)
}
function ValidationSummary({ summary }: { summary: { passed: number; warning: number; failed: number; total: number } }) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-green-200 p-4">
<p className="text-sm text-slate-500">Bestanden</p>
<p className="text-2xl font-bold text-green-600">{summary.passed}</p>
</div>
<div className="bg-white rounded-xl border border-yellow-200 p-4">
<p className="text-sm text-slate-500">Warnungen</p>
<p className="text-2xl font-bold text-yellow-600">{summary.warning}</p>
</div>
<div className="bg-white rounded-xl border border-red-200 p-4">
<p className="text-sm text-slate-500">Fehlgeschlagen</p>
<p className="text-2xl font-bold text-red-600">{summary.failed}</p>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<p className="text-sm text-slate-500">Gesamt</p>
<p className="text-2xl font-bold text-slate-700">{summary.total}</p>
</div>
</div>
)
}
function CheckpointsList({ checkpoints }: { checkpoints?: PipelineCheckpoint[] }) {
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50">
<h3 className="font-semibold text-slate-900">Checkpoints ({checkpoints?.length || 0})</h3>
</div>
<div className="divide-y">
{checkpoints?.map((checkpoint, idx) => (
<div key={idx} className="p-4">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<span className={`w-3 h-3 rounded-full ${
checkpoint.phase === 'ingestion' ? 'bg-blue-500' :
checkpoint.phase === 'extraction' ? 'bg-purple-500' :
checkpoint.phase === 'controls' ? 'bg-green-500' : 'bg-orange-500'
}`} />
<span className="font-medium text-slate-900">{checkpoint.name}</span>
<span className="text-sm text-slate-500">
({checkpoint.phase}) |
{checkpoint.duration_seconds ? ` ${checkpoint.duration_seconds.toFixed(1)}s` : ' -'}
</span>
</div>
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
checkpoint.status === 'completed' ? 'bg-green-100 text-green-700' :
checkpoint.status === 'running' ? 'bg-blue-100 text-blue-700' :
checkpoint.status === 'failed' ? 'bg-red-100 text-red-700' : 'bg-slate-100 text-slate-700'
}`}>
{checkpoint.status}
</span>
</div>
{/* Metrics */}
{Object.keys(checkpoint.metrics || {}).length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{Object.entries(checkpoint.metrics).map(([key, value]) => (
<span key={key} className="px-2 py-1 bg-slate-100 rounded text-xs text-slate-600">
{key.replace(/_/g, ' ')}: <strong>{typeof value === 'number' ? value.toLocaleString() : String(value)}</strong>
</span>
))}
</div>
)}
{/* Validations */}
{checkpoint.validations?.length > 0 && (
<div className="mt-3 space-y-1">
{checkpoint.validations.map((v, vIdx) => (
<div key={vIdx} className="flex items-center gap-2 text-sm">
<span className={`w-4 h-4 flex items-center justify-center ${
v.status === 'passed' ? 'text-green-500' :
v.status === 'warning' ? 'text-yellow-500' : 'text-red-500'
}`}>
{v.status === 'passed' ? '✓' : v.status === 'warning' ? '⚠' : '✗'}
</span>
<span className="text-slate-700">{v.name}:</span>
<span className="text-slate-500">{v.message}</span>
</div>
))}
</div>
)}
{/* Error */}
{checkpoint.error && (
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{checkpoint.error}
</div>
)}
</div>
))}
{(!checkpoints || checkpoints.length === 0) && (
<div className="p-4 text-center text-slate-500">
Noch keine Checkpoints vorhanden.
</div>
)}
</div>
</div>
)
}
function PipelineSummary({ summary }: { summary: Record<string, any> }) {
return (
<div className="bg-white rounded-xl border border-slate-200 p-4">
<h4 className="font-semibold text-slate-900 mb-3">Zusammenfassung</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{Object.entries(summary).map(([key, value]) => (
<div key={key}>
<p className="text-sm text-slate-500">{key.replace(/_/g, ' ')}</p>
<p className="font-bold text-slate-900">
{typeof value === 'number' ? value.toLocaleString() : String(value)}
</p>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,451 @@
'use client'
import React from 'react'
import {
REGULATIONS,
TYPE_COLORS,
TYPE_LABELS,
isInRag,
getKnownChunks,
} from '../rag-data'
import {
REGULATION_SOURCES,
REGULATION_LICENSES,
LICENSE_LABELS,
} from '../rag-sources'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface RegulationsTabProps {
hook: UseRAGPageReturn
}
export function RegulationsTab({ hook }: RegulationsTabProps) {
const {
regulationCategory,
setRegulationCategory,
expandedRegulation,
setExpandedRegulation,
fetchStatus,
dsfaSources,
dsfaLoading,
expandedDsfaSource,
setExpandedDsfaSource,
fetchDsfaStatus,
setActiveTab,
} = hook
return (
<div className="space-y-4">
{/* Category Filter */}
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => setRegulationCategory('regulations')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
regulationCategory === 'regulations'
? 'bg-blue-100 text-blue-700 ring-2 ring-blue-300'
: 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'
}`}
>
Gesetze & Regulierungen ({REGULATIONS.length})
</button>
<button
onClick={() => setRegulationCategory('dsfa')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
regulationCategory === 'dsfa'
? 'bg-purple-100 text-purple-700 ring-2 ring-purple-300'
: 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'
}`}
>
DSFA Quellen ({dsfaSources.length || '~70'})
</button>
<button
onClick={() => setRegulationCategory('nibis')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
regulationCategory === 'nibis'
? 'bg-emerald-100 text-emerald-700 ring-2 ring-emerald-300'
: 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'
}`}
>
NiBiS Dokumente
</button>
<button
onClick={() => setRegulationCategory('templates')}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors ${
regulationCategory === 'templates'
? 'bg-orange-100 text-orange-700 ring-2 ring-orange-300'
: 'bg-white text-slate-600 border border-slate-200 hover:bg-slate-50'
}`}
>
Templates & Vorlagen
</button>
</div>
{/* Regulations Table */}
{regulationCategory === 'regulations' && (
<RegulationsTable
expandedRegulation={expandedRegulation}
setExpandedRegulation={setExpandedRegulation}
fetchStatus={fetchStatus}
setActiveTab={setActiveTab}
/>
)}
{/* DSFA Sources */}
{regulationCategory === 'dsfa' && (
<DsfaSourcesList
dsfaSources={dsfaSources}
dsfaLoading={dsfaLoading}
expandedDsfaSource={expandedDsfaSource}
setExpandedDsfaSource={setExpandedDsfaSource}
fetchDsfaStatus={fetchDsfaStatus}
/>
)}
{/* NiBiS Dokumente (info only) */}
{regulationCategory === 'nibis' && <NibisInfo />}
{/* Templates (info only) */}
{regulationCategory === 'templates' && <TemplatesInfo />}
</div>
)
}
// --- Sub-components ---
function RegulationsTable({
expandedRegulation,
setExpandedRegulation,
fetchStatus,
setActiveTab,
}: {
expandedRegulation: string | null
setExpandedRegulation: (v: string | null) => void
fetchStatus: () => void
setActiveTab: (v: any) => void
}) {
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">
Alle {REGULATIONS.length} Regulierungen
<span className="ml-2 text-sm font-normal text-slate-500">
({REGULATIONS.filter(r => isInRag(r.code)).length} im RAG,{' '}
{REGULATIONS.filter(r => !isInRag(r.code)).length} ausstehend)
</span>
</h3>
<button onClick={fetchStatus} className="text-sm text-teal-600 hover:text-teal-700">
Aktualisieren
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b">
<tr>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase w-12">RAG</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Code</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Typ</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Name</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase">Chunks</th>
<th className="px-4 py-3 text-right text-xs font-medium text-slate-500 uppercase">Erwartet</th>
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="divide-y">
{REGULATIONS.map((reg) => {
const chunks = getKnownChunks(reg.code)
const inRag = isInRag(reg.code)
const statusColor = inRag ? 'text-green-500' : 'text-red-500'
const statusIcon = inRag ? '✓' : '❌'
const isExpanded = expandedRegulation === reg.code
return (
<React.Fragment key={reg.code}>
<tr
onClick={() => setExpandedRegulation(isExpanded ? null : reg.code)}
className="hover:bg-slate-50 cursor-pointer transition-colors"
>
<td className="px-4 py-3 text-center">
{isInRag(reg.code) ? (
<span className="inline-flex items-center justify-center w-6 h-6 bg-green-100 text-green-600 rounded-full text-xs font-bold" title="Im RAG vorhanden"></span>
) : (
<span className="inline-flex items-center justify-center w-6 h-6 bg-red-50 text-red-400 rounded-full text-xs font-bold" title="Nicht im RAG"></span>
)}
</td>
<td className="px-4 py-3 font-mono font-medium text-teal-600">
<span className="inline-flex items-center gap-2">
<span className={`transform transition-transform ${isExpanded ? 'rotate-90' : ''}`}></span>
{reg.code}
</span>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 text-xs rounded ${TYPE_COLORS[reg.type]}`}>
{TYPE_LABELS[reg.type]}
</span>
</td>
<td className="px-4 py-3 text-slate-900">{reg.name}</td>
<td className="px-4 py-3 text-right font-bold">
<span className={chunks > 0 && chunks < 10 && reg.expected >= 10 ? 'text-amber-600' : ''}>
{chunks.toLocaleString()}
{chunks > 0 && chunks < 10 && reg.expected >= 10 && (
<span className="ml-1 inline-block w-4 h-4 text-[10px] leading-4 text-center bg-amber-100 text-amber-700 rounded-full" title="Verdaechtig niedrig — Ingestion pruefen"></span>
)}
</span>
</td>
<td className="px-4 py-3 text-right text-slate-500">{reg.expected}</td>
<td className={`px-4 py-3 text-center ${statusColor}`}>{statusIcon}</td>
</tr>
{isExpanded && (
<tr key={`${reg.code}-detail`} className="bg-slate-50">
<td colSpan={7} className="px-4 py-4">
<div className="bg-white rounded-lg border border-slate-200 p-4 space-y-3">
<div>
<h4 className="font-semibold text-slate-900 mb-1">{reg.fullName}</h4>
<p className="text-sm text-slate-600">{reg.description}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-2 border-t border-slate-100">
<div>
<p className="text-xs font-medium text-slate-500 uppercase mb-1">Relevant fuer</p>
<div className="flex flex-wrap gap-1">
{reg.relevantFor.map((item, idx) => (
<span key={idx} className="px-2 py-0.5 text-xs bg-slate-100 text-slate-600 rounded">
{item}
</span>
))}
</div>
</div>
<div>
<p className="text-xs font-medium text-slate-500 uppercase mb-1">Kernthemen</p>
<div className="flex flex-wrap gap-1">
{reg.keyTopics.map((topic, idx) => (
<span key={idx} className="px-2 py-0.5 text-xs bg-teal-50 text-teal-700 rounded">
{topic}
</span>
))}
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-100 text-xs text-slate-500">
<div className="flex items-center gap-4">
<span>In Kraft seit: {reg.effectiveDate}</span>
{REGULATION_LICENSES[reg.code] && (
<span className="flex items-center gap-1">
<span className="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-medium">
{LICENSE_LABELS[REGULATION_LICENSES[reg.code].license] || REGULATION_LICENSES[reg.code].license}
</span>
<span className="text-slate-400">{REGULATION_LICENSES[reg.code].licenseNote}</span>
</span>
)}
</div>
<div className="flex items-center gap-3">
{REGULATION_SOURCES[reg.code] && (
<a
href={REGULATION_SOURCES[reg.code]}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-blue-600 hover:text-blue-700 font-medium"
>
Originalquelle
</a>
)}
<button
onClick={(e) => {
e.stopPropagation()
setActiveTab('chunks')
}}
className="text-teal-600 hover:text-teal-700 font-medium"
>
In Chunks suchen
</button>
</div>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
)
})}
</tbody>
</table>
</div>
</div>
)
}
function DsfaSourcesList({
dsfaSources,
dsfaLoading,
expandedDsfaSource,
setExpandedDsfaSource,
fetchDsfaStatus,
}: {
dsfaSources: any[]
dsfaLoading: boolean
expandedDsfaSource: string | null
setExpandedDsfaSource: (v: string | null) => void
fetchDsfaStatus: () => void
}) {
const typeColors: Record<string, string> = {
regulation: 'bg-blue-100 text-blue-700',
legislation: 'bg-indigo-100 text-indigo-700',
guideline: 'bg-teal-100 text-teal-700',
checklist: 'bg-yellow-100 text-yellow-700',
standard: 'bg-green-100 text-green-700',
methodology: 'bg-purple-100 text-purple-700',
specification: 'bg-orange-100 text-orange-700',
catalog: 'bg-pink-100 text-pink-700',
guidance: 'bg-cyan-100 text-cyan-700',
}
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50 flex items-center justify-between">
<div>
<h3 className="font-semibold text-slate-900">DSFA Quellen ({dsfaSources.length || '~70'})</h3>
<p className="text-xs text-slate-500">WP248, DSK Kurzpapiere, Muss-Listen, nationale Datenschutzgesetze</p>
</div>
<button onClick={fetchDsfaStatus} className="text-sm text-teal-600 hover:text-teal-700">
Aktualisieren
</button>
</div>
{dsfaLoading ? (
<div className="p-8 text-center text-slate-500">Lade DSFA-Quellen...</div>
) : dsfaSources.length === 0 ? (
<div className="p-8 text-center text-slate-500">
<p className="mb-2">Keine DSFA-Quellen vom Backend geladen.</p>
<p className="text-xs">Endpunkt: <code className="bg-slate-100 px-1 rounded">/api/dsfa-corpus?action=sources</code></p>
</div>
) : (
<div className="divide-y">
{dsfaSources.map((source) => {
const isExpanded = expandedDsfaSource === source.source_code
return (
<React.Fragment key={source.source_code}>
<div
onClick={() => setExpandedDsfaSource(isExpanded ? null : source.source_code)}
className="px-4 py-3 hover:bg-slate-50 cursor-pointer transition-colors flex items-center justify-between"
>
<div className="flex items-center gap-3">
<span className={`transform transition-transform text-xs ${isExpanded ? 'rotate-90' : ''}`}></span>
<span className="font-mono text-sm text-purple-600 font-medium">{source.source_code}</span>
<span className={`px-2 py-0.5 text-xs rounded ${typeColors[source.document_type] || 'bg-slate-100 text-slate-600'}`}>
{source.document_type}
</span>
<span className="text-sm text-slate-900">{source.name}</span>
</div>
<div className="flex items-center gap-3">
<span className="px-1.5 py-0.5 text-[10px] font-medium bg-slate-100 text-slate-500 rounded uppercase">
{source.language}
</span>
{source.chunk_count != null && (
<span className="text-sm font-bold text-purple-600">{source.chunk_count} Chunks</span>
)}
</div>
</div>
{isExpanded && (
<div className="px-4 pb-4 bg-slate-50">
<div className="bg-white rounded-lg border border-slate-200 p-4 space-y-3">
<div>
<h4 className="font-semibold text-slate-900 mb-1">{source.full_name || source.name}</h4>
{source.organization && (
<p className="text-sm text-slate-600">Organisation: {source.organization}</p>
)}
</div>
<div className="flex items-center gap-4 pt-2 border-t border-slate-100 text-xs text-slate-500">
<span className="flex items-center gap-1">
<span className="px-1.5 py-0.5 bg-slate-100 text-slate-600 rounded text-[10px] font-medium">
{LICENSE_LABELS[source.license_code] || source.license_code}
</span>
<span className="text-slate-400">{source.attribution_text}</span>
</span>
</div>
{source.source_url && (
<div className="text-xs">
<a
href={source.source_url}
target="_blank"
rel="noopener noreferrer"
className="text-teal-600 hover:underline"
onClick={(e) => e.stopPropagation()}
>
Quelle: {source.source_url}
</a>
</div>
)}
</div>
</div>
)}
</React.Fragment>
)
})}
</div>
)}
</div>
)
}
function NibisInfo() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-emerald-100 flex items-center justify-center text-xl">📚</div>
<div>
<h3 className="font-semibold text-slate-900">NiBiS Erwartungshorizonte</h3>
<p className="text-sm text-slate-500">Collection: <code className="bg-slate-100 px-1 rounded">bp_nibis_eh</code></p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-emerald-50 rounded-lg p-4 border border-emerald-200">
<p className="text-sm text-emerald-600 font-medium">Chunks</p>
<p className="text-2xl font-bold text-slate-900">7.996</p>
</div>
<div className="bg-emerald-50 rounded-lg p-4 border border-emerald-200">
<p className="text-sm text-emerald-600 font-medium">Vector Size</p>
<p className="text-2xl font-bold text-slate-900">1024</p>
</div>
<div className="bg-emerald-50 rounded-lg p-4 border border-emerald-200">
<p className="text-sm text-emerald-600 font-medium">Typ</p>
<p className="text-2xl font-bold text-slate-900">BGE-M3</p>
</div>
</div>
<p className="text-sm text-slate-600">
Bildungsinhalte aus dem Niedersaechsischen Bildungsserver (NiBiS). Enthaelt Erwartungshorizonte fuer
verschiedene Faecher und Schulformen. Wird ueber die Klausur-Korrektur fuer EH-Matching genutzt.
Diese Daten sind nicht direkt compliance-relevant.
</p>
</div>
)
}
function TemplatesInfo() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center text-xl">📋</div>
<div>
<h3 className="font-semibold text-slate-900">Legal Templates & Vorlagen</h3>
<p className="text-sm text-slate-500">Collection: <code className="bg-slate-100 px-1 rounded">bp_legal_templates</code></p>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mb-4">
<div className="bg-orange-50 rounded-lg p-4 border border-orange-200">
<p className="text-sm text-orange-600 font-medium">Chunks</p>
<p className="text-2xl font-bold text-slate-900">7.689</p>
</div>
<div className="bg-orange-50 rounded-lg p-4 border border-orange-200">
<p className="text-sm text-orange-600 font-medium">Vector Size</p>
<p className="text-2xl font-bold text-slate-900">1024</p>
</div>
<div className="bg-orange-50 rounded-lg p-4 border border-orange-200">
<p className="text-sm text-orange-600 font-medium">Typ</p>
<p className="text-2xl font-bold text-slate-900">BGE-M3</p>
</div>
</div>
<p className="text-sm text-slate-600">
Vorlagen fuer VVT (Verzeichnis von Verarbeitungstaetigkeiten), TOM (Technisch-Organisatorische Massnahmen),
DSFA-Berichte und weitere Compliance-Dokumente. Werden vom AI Compliance SDK fuer die Dokumentgenerierung genutzt.
</p>
</div>
)
}

View File

@@ -0,0 +1,97 @@
'use client'
import React from 'react'
import type { UseRAGPageReturn } from '../_hooks/useRAGPage'
interface SearchTabProps {
hook: UseRAGPageReturn
}
export function SearchTab({ hook }: SearchTabProps) {
const {
searchQuery,
setSearchQuery,
searchResults,
searching,
selectedRegulations,
setSelectedRegulations,
handleSearch,
} = hook
return (
<div className="space-y-6">
{/* Search Box */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Semantische Suche</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Suchanfrage</label>
<textarea
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="z.B. 'Welche Anforderungen gibt es fuer KI-Systeme mit hohem Risiko?'"
rows={3}
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-teal-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Filter (optional)</label>
<div className="flex flex-wrap gap-2">
{['GDPR', 'AIACT', 'CRA', 'NIS2', 'BSI-TR-03161-1'].map((code) => (
<button
key={code}
onClick={() => {
setSelectedRegulations((prev: string[]) =>
prev.includes(code) ? prev.filter((c: string) => c !== code) : [...prev, code]
)
}}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedRegulations.includes(code)
? 'bg-teal-100 border-teal-300 text-teal-700'
: 'bg-white border-slate-200 text-slate-600 hover:border-slate-300'
}`}
>
{code}
</button>
))}
</div>
</div>
<button
onClick={handleSearch}
disabled={searching || !searchQuery.trim()}
className="px-6 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 disabled:opacity-50"
>
{searching ? 'Suche...' : 'Suchen'}
</button>
</div>
</div>
{/* Search Results */}
{searchResults.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-4 py-3 border-b bg-slate-50">
<h3 className="font-semibold text-slate-900">{searchResults.length} Ergebnisse</h3>
</div>
<div className="divide-y">
{searchResults.map((result, i) => (
<div key={i} className="p-4">
<div className="flex items-center gap-2 mb-2">
<span className="px-2 py-0.5 text-xs rounded bg-teal-100 text-teal-700">
{result.regulation_code}
</span>
{result.article && (
<span className="text-sm text-slate-500">Art. {result.article}</span>
)}
<span className="ml-auto text-sm text-slate-400">
Score: {(result.score * 100).toFixed(1)}%
</span>
</div>
<p className="text-slate-700 text-sm">{result.text}</p>
</div>
))}
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,441 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
import { API_PROXY, DSFA_API_PROXY } from '../rag-data'
import type {
TabId,
RegulationCategory,
CollectionStatus,
SearchResult,
DsfaSource,
DsfaCorpusStatus,
CustomDocument,
PipelineState,
PipelineCheckpoint,
} from '../types'
export function useRAGPage() {
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [collectionStatus, setCollectionStatus] = useState<CollectionStatus | null>(null)
const [loading, setLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
const [searching, setSearching] = useState(false)
const [selectedRegulations, setSelectedRegulations] = useState<string[]>([])
const [ingestionRunning, setIngestionRunning] = useState(false)
const [ingestionLog, setIngestionLog] = useState<string[]>([])
const [pipelineState, setPipelineState] = useState<PipelineState | null>(null)
const [pipelineLoading, setPipelineLoading] = useState(false)
const [pipelineStarting, setPipelineStarting] = useState(false)
const [expandedRegulation, setExpandedRegulation] = useState<string | null>(null)
const [autoRefresh, setAutoRefresh] = useState(true)
const [elapsedTime, setElapsedTime] = useState<string>('')
const [expandedDocTypes, setExpandedDocTypes] = useState<string[]>(['eu_regulation', 'eu_directive'])
const [expandedMatrixDoc, setExpandedMatrixDoc] = useState<string | null>(null)
// DSFA corpus state
const [dsfaSources, setDsfaSources] = useState<DsfaSource[]>([])
const [dsfaStatus, setDsfaStatus] = useState<DsfaCorpusStatus | null>(null)
const [dsfaLoading, setDsfaLoading] = useState(false)
const [regulationCategory, setRegulationCategory] = useState<RegulationCategory>('regulations')
const [expandedDsfaSource, setExpandedDsfaSource] = useState<string | null>(null)
// Data tab state
const [customDocuments, setCustomDocuments] = useState<CustomDocument[]>([])
const [uploadFile, setUploadFile] = useState<File | null>(null)
const [uploadTitle, setUploadTitle] = useState('')
const [uploadCode, setUploadCode] = useState('')
const [uploading, setUploading] = useState(false)
const [linkUrl, setLinkUrl] = useState('')
const [linkTitle, setLinkTitle] = useState('')
const [linkCode, setLinkCode] = useState('')
const [addingLink, setAddingLink] = useState(false)
const fetchStatus = useCallback(async () => {
setLoading(true)
try {
const res = await fetch(`${API_PROXY}?action=status`)
if (res.ok) {
const data = await res.json()
setCollectionStatus(data)
}
} catch (error) {
console.error('Failed to fetch status:', error)
} finally {
setLoading(false)
}
}, [])
const fetchPipeline = useCallback(async () => {
setPipelineLoading(true)
try {
const res = await fetch(`${API_PROXY}?action=pipeline-checkpoints`)
if (res.ok) {
const data = await res.json()
setPipelineState(data)
}
} catch (error) {
console.error('Failed to fetch pipeline:', error)
} finally {
setPipelineLoading(false)
}
}, [])
const fetchDsfaStatus = useCallback(async () => {
setDsfaLoading(true)
try {
const [statusRes, sourcesRes] = await Promise.all([
fetch(`${DSFA_API_PROXY}?action=status`),
fetch(`${DSFA_API_PROXY}?action=sources`),
])
if (statusRes.ok) {
const data = await statusRes.json()
setDsfaStatus(data)
}
if (sourcesRes.ok) {
const data = await sourcesRes.json()
setDsfaSources(data.sources || data || [])
}
} catch (error) {
console.error('Failed to fetch DSFA status:', error)
} finally {
setDsfaLoading(false)
}
}, [])
const fetchCustomDocuments = useCallback(async () => {
try {
const res = await fetch(`${API_PROXY}?action=custom-documents`)
if (res.ok) {
const data = await res.json()
setCustomDocuments(data.documents || [])
}
} catch (error) {
console.error('Failed to fetch custom documents:', error)
}
}, [])
const handleUpload = async () => {
if (!uploadFile || !uploadTitle || !uploadCode) return
setUploading(true)
try {
const formData = new FormData()
formData.append('file', uploadFile)
formData.append('title', uploadTitle)
formData.append('code', uploadCode)
formData.append('document_type', 'custom')
const res = await fetch(`${API_PROXY}?action=upload`, {
method: 'POST',
body: formData,
})
if (res.ok) {
setUploadFile(null)
setUploadTitle('')
setUploadCode('')
fetchCustomDocuments()
fetchStatus()
}
} catch (error) {
console.error('Upload failed:', error)
} finally {
setUploading(false)
}
}
const handleAddLink = async () => {
if (!linkUrl || !linkTitle || !linkCode) return
setAddingLink(true)
try {
const res = await fetch(`${API_PROXY}?action=add-link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: linkUrl,
title: linkTitle,
code: linkCode,
document_type: 'custom',
}),
})
if (res.ok) {
setLinkUrl('')
setLinkTitle('')
setLinkCode('')
fetchCustomDocuments()
}
} catch (error) {
console.error('Add link failed:', error)
} finally {
setAddingLink(false)
}
}
const handleDeleteDocument = async (docId: string) => {
try {
const res = await fetch(`${API_PROXY}?action=delete-document&docId=${docId}`, {
method: 'DELETE',
})
if (res.ok) {
fetchCustomDocuments()
fetchStatus()
}
} catch (error) {
console.error('Delete failed:', error)
}
}
const handleStartPipeline = async (skipIngestion: boolean = false) => {
setPipelineStarting(true)
try {
const res = await fetch(`${API_PROXY}?action=start-pipeline`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
force_reindex: false,
skip_ingestion: skipIngestion,
}),
})
if (res.ok) {
setTimeout(() => {
fetchPipeline()
setPipelineStarting(false)
}, 2000)
} else {
setPipelineStarting(false)
}
} catch (error) {
console.error('Failed to start pipeline:', error)
setPipelineStarting(false)
}
}
const handleSearch = async () => {
if (!searchQuery.trim()) return
setSearching(true)
try {
const params = new URLSearchParams({
action: 'search',
query: searchQuery,
top_k: '5',
})
if (selectedRegulations.length > 0) {
params.append('regulations', selectedRegulations.join(','))
}
const res = await fetch(`${API_PROXY}?${params}`)
if (res.ok) {
const data = await res.json()
setSearchResults(data.results || [])
}
} catch (error) {
console.error('Search failed:', error)
} finally {
setSearching(false)
}
}
const triggerIngestion = async () => {
setIngestionRunning(true)
setIngestionLog(['Starte Re-Ingestion aller 19 Regulierungen...'])
try {
const res = await fetch(`${API_PROXY}?action=ingest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: true }),
})
if (res.ok) {
const data = await res.json()
setIngestionLog((prev) => [...prev, 'Ingestion gestartet. Job-ID: ' + (data.job_id || 'N/A')])
const checkStatus = setInterval(async () => {
try {
const statusRes = await fetch(`${API_PROXY}?action=ingestion-status`)
if (statusRes.ok) {
const statusData = await statusRes.json()
if (statusData.completed) {
clearInterval(checkStatus)
setIngestionRunning(false)
setIngestionLog((prev) => [...prev, 'Ingestion abgeschlossen!'])
fetchStatus()
} else if (statusData.current_regulation) {
setIngestionLog((prev) => [
...prev,
`Verarbeite: ${statusData.current_regulation} (${statusData.processed}/${statusData.total})`,
])
}
}
} catch {
// Ignore polling errors
}
}, 5000)
} else {
setIngestionLog((prev) => [...prev, 'Fehler: ' + res.statusText])
setIngestionRunning(false)
}
} catch (error) {
setIngestionLog((prev) => [...prev, 'Fehler: ' + String(error)])
setIngestionRunning(false)
}
}
const getRegulationChunks = (code: string): number => {
return collectionStatus?.regulations?.[code] || 0
}
const getTotalChunks = (): number => {
return collectionStatus?.totalPoints || 0
}
// Initial data fetch
useEffect(() => {
fetchStatus()
fetchDsfaStatus()
}, [fetchStatus, fetchDsfaStatus])
// Fetch pipeline when tab changes
useEffect(() => {
if (activeTab === 'pipeline') {
fetchPipeline()
}
}, [activeTab, fetchPipeline])
// Fetch custom documents when data tab is active
useEffect(() => {
if (activeTab === 'data') {
fetchCustomDocuments()
}
}, [activeTab, fetchCustomDocuments])
// Auto-refresh pipeline status when running
useEffect(() => {
if (activeTab !== 'pipeline' || !autoRefresh) return
const isRunning = pipelineState?.status === 'running'
if (isRunning) {
const interval = setInterval(() => {
fetchPipeline()
fetchStatus()
}, 5000)
return () => clearInterval(interval)
}
}, [activeTab, autoRefresh, pipelineState?.status, fetchPipeline, fetchStatus])
// Update elapsed time
useEffect(() => {
if (!pipelineState?.started_at || pipelineState?.status !== 'running') {
setElapsedTime('')
return
}
const updateElapsed = () => {
const start = new Date(pipelineState.started_at!).getTime()
const now = Date.now()
const diff = Math.floor((now - start) / 1000)
const hours = Math.floor(diff / 3600)
const minutes = Math.floor((diff % 3600) / 60)
const seconds = diff % 60
if (hours > 0) {
setElapsedTime(`${hours}h ${minutes}m ${seconds}s`)
} else if (minutes > 0) {
setElapsedTime(`${minutes}m ${seconds}s`)
} else {
setElapsedTime(`${seconds}s`)
}
}
updateElapsed()
const interval = setInterval(updateElapsed, 1000)
return () => clearInterval(interval)
}, [pipelineState?.started_at, pipelineState?.status])
return {
// Tab state
activeTab,
setActiveTab,
// Collection status
collectionStatus,
loading,
fetchStatus,
// Search
searchQuery,
setSearchQuery,
searchResults,
searching,
selectedRegulations,
setSelectedRegulations,
handleSearch,
// Ingestion
ingestionRunning,
ingestionLog,
triggerIngestion,
// Pipeline
pipelineState,
pipelineLoading,
pipelineStarting,
autoRefresh,
setAutoRefresh,
elapsedTime,
fetchPipeline,
handleStartPipeline,
// Regulation expansion
expandedRegulation,
setExpandedRegulation,
expandedDocTypes,
setExpandedDocTypes,
expandedMatrixDoc,
setExpandedMatrixDoc,
// DSFA
dsfaSources,
dsfaStatus,
dsfaLoading,
regulationCategory,
setRegulationCategory,
expandedDsfaSource,
setExpandedDsfaSource,
fetchDsfaStatus,
// Data tab
customDocuments,
uploadFile,
setUploadFile,
uploadTitle,
setUploadTitle,
uploadCode,
setUploadCode,
uploading,
handleUpload,
linkUrl,
setLinkUrl,
linkTitle,
setLinkTitle,
linkCode,
setLinkCode,
addingLink,
handleAddLink,
handleDeleteDocument,
fetchCustomDocuments,
// Helpers
getRegulationChunks,
getTotalChunks,
}
}
export type UseRAGPageReturn = ReturnType<typeof useRAGPage>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,352 @@
/**
* RAG & Legal Corpus Management - Static Data
*
* Core data constants: regulations, industries, thematic groups, etc.
* Source URLs and licenses are in rag-sources.ts.
*/
import { REGULATIONS_IN_RAG } from './rag-constants'
import ragData from './rag-documents.json'
import type {
Regulation,
Industry,
ThematicGroup,
KeyIntersection,
FutureOutlookItem,
AdditionalRegulation,
LegalBasisInfo,
TabDef,
} from './types'
// Re-export source URLs, licenses and license labels from rag-sources.ts
export {
REGULATION_SOURCES,
REGULATION_LICENSES,
LICENSE_LABELS,
} from './rag-sources'
// API uses local proxy route to klausur-service
export const API_PROXY = '/api/legal-corpus'
export const DSFA_API_PROXY = '/api/dsfa-corpus'
// Import documents and metadata from JSON
export const RAG_DOCUMENTS = ragData.documents
export const DOC_TYPES = ragData.doc_types
export const INDUSTRIES_LIST = ragData.industries
// Derive REGULATIONS from JSON (backwards compatible for regulations tab)
export const REGULATIONS: Regulation[] = RAG_DOCUMENTS.filter((d: any) => d.description).map((d: any) => ({
code: d.code,
name: d.name,
fullName: d.full_name || d.name,
type: d.doc_type,
expected: 0,
description: d.description || '',
relevantFor: [] as string[],
keyTopics: [] as string[],
effectiveDate: d.effective_date || ''
}))
// Helper: Check if regulation is in RAG
export const isInRag = (code: string): boolean => code in REGULATIONS_IN_RAG
// Helper: Get known chunk count for a regulation
export const getKnownChunks = (code: string): number => REGULATIONS_IN_RAG[code]?.chunks || 0
// Known collection totals (updated: 2026-03-12)
export const COLLECTION_TOTALS = {
bp_compliance_gesetze: 63567,
bp_compliance_ce: 18183,
bp_legal_templates: 7689,
bp_compliance_datenschutz: 17459,
bp_dsfa_corpus: 8666,
bp_compliance_recht: 1425,
bp_nibis_eh: 7996,
total_legal: 81750,
total_all: 124985,
}
export const TYPE_COLORS: Record<string, string> = {
eu_regulation: 'bg-blue-100 text-blue-700',
eu_directive: 'bg-purple-100 text-purple-700',
de_law: 'bg-yellow-100 text-yellow-700',
at_law: 'bg-red-100 text-red-700',
ch_law: 'bg-rose-100 text-rose-700',
bsi_standard: 'bg-green-100 text-green-700',
national_law: 'bg-orange-100 text-orange-700',
eu_guideline: 'bg-teal-100 text-teal-700',
}
export const TYPE_LABELS: Record<string, string> = {
eu_regulation: 'EU-VO',
eu_directive: 'EU-RL',
de_law: 'DE-Gesetz',
at_law: 'AT-Gesetz',
ch_law: 'CH-Gesetz',
bsi_standard: 'BSI',
national_law: 'Nat. Gesetz',
eu_guideline: 'EDPB-GL',
}
// Industries for backward compatibility
export const INDUSTRIES: Industry[] = INDUSTRIES_LIST.map((ind: any) => ({
id: ind.id,
name: ind.name,
icon: ind.icon,
description: ''
}))
// Derive industry map from document data
export const INDUSTRY_REGULATION_MAP: Record<string, string[]> = {}
for (const ind of INDUSTRIES_LIST) {
INDUSTRY_REGULATION_MAP[ind.id] = RAG_DOCUMENTS
.filter((d: any) => d.industries.includes(ind.id) || d.industries.includes('all'))
.map((d: any) => d.code)
}
// Thematic groupings showing overlaps
export const THEMATIC_GROUPS: ThematicGroup[] = [
{
id: 'datenschutz',
name: 'Datenschutz & Privacy',
color: 'bg-blue-500',
regulations: ['GDPR', 'EPRIVACY', 'TDDDG', 'SCC', 'DPF'],
description: 'Schutz personenbezogener Daten, Einwilligung, Betroffenenrechte'
},
{
id: 'cybersecurity',
name: 'Cybersicherheit',
color: 'bg-red-500',
regulations: ['NIS2', 'EUCSA', 'CRA', 'BSI-TR-03161-1', 'BSI-TR-03161-2', 'BSI-TR-03161-3', 'DORA'],
description: 'IT-Sicherheit, Risikomanagement, Incident Response'
},
{
id: 'ai',
name: 'Kuenstliche Intelligenz',
color: 'bg-purple-500',
regulations: ['AIACT', 'PLD', 'GPSR'],
description: 'KI-Regulierung, Hochrisiko-Systeme, Haftung'
},
{
id: 'digital-markets',
name: 'Digitale Maerkte & Plattformen',
color: 'bg-green-500',
regulations: ['DSA', 'DGA', 'DATAACT', 'DSM'],
description: 'Plattformregulierung, Datenzugang, Urheberrecht'
},
{
id: 'product-safety',
name: 'Produktsicherheit & Haftung',
color: 'bg-orange-500',
regulations: ['CRA', 'PLD', 'GPSR', 'EAA', 'MACHINERY_REG', 'BLUE_GUIDE'],
description: 'Sicherheitsanforderungen, CE-Kennzeichnung, Maschinenverordnung, Barrierefreiheit'
},
{
id: 'finance',
name: 'Finanzmarktregulierung',
color: 'bg-emerald-500',
regulations: ['DORA', 'PSD2', 'AMLR', 'MiCA'],
description: 'Zahlungsdienste, Krypto-Assets, Geldwaeschebekaempfung, digitale Resilienz'
},
{
id: 'health',
name: 'Gesundheitsdaten',
color: 'bg-pink-500',
regulations: ['EHDS', 'BSI-TR-03161-1', 'BSI-TR-03161-2', 'BSI-TR-03161-3'],
description: 'Gesundheitsdatenraum, DiGA-Sicherheit, Patientenrechte'
},
{
id: 'verbraucherschutz',
name: 'Verbraucherschutz & E-Commerce',
color: 'bg-amber-500',
regulations: ['DE_PANGV', 'DE_VSBG', 'DE_PRODHAFTG', 'DE_UWG', 'DE_BFSG',
'WARENKAUF_RL', 'KLAUSEL_RL', 'UNLAUTERE_PRAKTIKEN_RL', 'PREISANGABEN_RL',
'OMNIBUS_RL', 'E_COMMERCE_RL', 'VERBRAUCHERRECHTE_RL', 'DIGITALE_INHALTE_RL'],
description: 'Widerrufsrecht, Preisangaben, Fernabsatz, AGB-Recht, Barrierefreiheit'
},
]
// Key overlaps and intersections
export const KEY_INTERSECTIONS: KeyIntersection[] = [
{
regulations: ['GDPR', 'AIACT'],
topic: 'KI und personenbezogene Daten',
description: 'Automatisierte Entscheidungen, Profiling, Erklaerbarkeit'
},
{
regulations: ['NIS2', 'CRA'],
topic: 'Cybersicherheit von Produkten',
description: 'Sicherheitsanforderungen ueber den gesamten Lebenszyklus'
},
{
regulations: ['AIACT', 'PLD'],
topic: 'KI-Haftung',
description: 'Wer haftet, wenn KI Schaeden verursacht?'
},
{
regulations: ['DSA', 'GDPR'],
topic: 'Plattform-Transparenz',
description: 'Inhaltsmoderation und Datenschutz'
},
{
regulations: ['DATAACT', 'GDPR'],
topic: 'Datenzugang vs. Datenschutz',
description: 'Balance zwischen Datenteilung und Privacy'
},
{
regulations: ['CRA', 'GPSR'],
topic: 'Digitale Produktsicherheit',
description: 'Hardware mit Software-Komponenten'
},
]
// Future outlook - proposed and discussed regulations
export const FUTURE_OUTLOOK: FutureOutlookItem[] = [
{
id: 'digital-omnibus',
name: 'EU Digital Omnibus',
status: 'proposed',
statusLabel: 'Vorgeschlagen Nov 2025',
expectedDate: '2026/2027',
description: 'Umfassendes Vereinfachungspaket fuer AI Act, DSGVO und Cybersicherheit. Ziel: 5 Mrd. EUR Einsparung bei Verwaltungskosten.',
keyChanges: [
'AI Act: Verschiebung Hochrisiko-Pflichten um bis zu 16 Monate (bis Dez 2027)',
'AI Act: Vereinfachte Dokumentation fuer KMU und Small Midcaps',
'AI Act: EU-weite regulatorische Sandbox fuer KI-Tests',
'DSGVO: Cookie-Banner-Reform - Berechtigtes Interesse statt nur Einwilligung',
'DSGVO: Automatische Privacy-Signale via Browser statt Pop-ups',
'Cybersecurity: Single Entry Point fuer Meldepflichten'
],
affectedRegulations: ['AIACT', 'GDPR', 'NIS2', 'CRA', 'EUCSA'],
source: 'https://digital-strategy.ec.europa.eu/en/library/digital-omnibus-ai-regulation-proposal'
},
{
id: 'sustainability-omnibus',
name: 'EU Nachhaltigkeits-Omnibus',
status: 'agreed',
statusLabel: 'Einigung Dez 2025',
expectedDate: 'Q1 2026',
description: 'Drastische Reduzierung der Nachhaltigkeits-Berichtspflichten. Anwendungsbereich wird stark eingeschraenkt.',
keyChanges: [
'CSRD: Nur noch Unternehmen >1.000 MA und >450 Mio EUR Umsatz berichtspflichtig',
'CSRD: Betroffene Unternehmen sinken von 50.000 auf ca. 5.000 in der EU',
'CSRD: Verschiebung Welle 2+3 um 2 Jahre (auf Geschaeftsjahr 2027)',
'CSDDD: Nur noch Unternehmen >5.000 MA und >1,5 Mrd EUR Umsatz',
'CSDDD: Sorgfaltspflichten nur noch fuer Tier-1-Lieferanten',
'CSDDD: Pruefung nur noch alle 5 Jahre statt jaehrlich'
],
affectedRegulations: ['CSRD', 'CSDDD', 'EU-Taxonomie'],
source: 'https://kpmg-law.de/erste-omnibus-verordnung-soll-die-pflichten-der-csddd-csrd-und-eu-taxonomie-lockern/'
},
{
id: 'eprivacy-withdrawal',
name: 'ePrivacy-Verordnung',
status: 'withdrawn',
statusLabel: 'Zurueckgezogen Feb 2025',
expectedDate: 'Unbekannt',
description: 'Nach 9 Jahren Verhandlung hat die EU-Kommission den Vorschlag zurueckgezogen. Die ePrivacy-Richtlinie bleibt in Kraft, Cookie-Reform kommt via DSGVO/Digital Omnibus.',
keyChanges: [
'Urspruenglicher Vorschlag: Einheitliche EU-Cookie-Regeln',
'Urspruenglicher Vorschlag: Strikte Tracking-Einwilligung',
'Status: ePrivacy-Richtlinie + TDDDG bleiben gueltig',
'Zukunft: Cookie-Reform wird Teil der DSGVO-Aenderungen'
],
affectedRegulations: ['EPRIVACY', 'TDDDG', 'GDPR'],
source: 'https://netzpolitik.org/2025/cookie-banner-und-online-tracking-eu-kommission-beerdigt-plaene-fuer-eprivacy-verordnung/'
},
{
id: 'ai-liability',
name: 'KI-Haftungsrichtlinie',
status: 'pending',
statusLabel: 'In Verhandlung',
expectedDate: '2026',
description: 'Ergaenzt den AI Act um zivilrechtliche Haftungsregeln. Erleichtert Geschaedigten die Beweisfuehrung bei KI-Schaeden.',
keyChanges: [
'Beweislasterleichterung bei KI-verursachten Schaeden',
'Offenlegungspflichten fuer KI-Anbieter im Schadensfall',
'Verknuepfung mit Produkthaftungsrichtlinie'
],
affectedRegulations: ['AIACT', 'PLD'],
source: 'https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:52022PC0496'
},
]
// Potential future regulations (not yet integrated)
export const ADDITIONAL_REGULATIONS: AdditionalRegulation[] = [
{
code: 'PSD3',
name: 'Payment Services Directive 3',
fullName: 'Richtlinie zur dritten Zahlungsdiensterichtlinie (Entwurf)',
type: 'eu_directive',
status: 'proposed',
effectiveDate: 'Voraussichtlich 2026',
description: 'Modernisierung der Zahlungsdienste-Regulierung. Staerkerer Verbraucherschutz, Open Banking 2.0, Betrugsbekaempfung. Ersetzt dann PSD2.',
relevantFor: ['Banken', 'Zahlungsdienstleister', 'Fintechs', 'E-Commerce'],
celex: '52023PC0366',
priority: 'medium'
},
{
code: 'AMLD6',
name: 'AML-Richtlinie 6',
fullName: 'Richtlinie (EU) 2024/1640 - 6. Geldwaescherichtlinie',
type: 'eu_directive',
status: 'active',
effectiveDate: '10. Juli 2027 (Umsetzung)',
description: 'Ergaenzt die AML-Verordnung. Nationale Umsetzungsvorschriften, strafrechtliche Sanktionen, AMLA-Behoerde.',
relevantFor: ['Banken', 'Krypto-Anbieter', 'Immobilienmakler', 'Gluecksspielanbieter'],
celex: '32024L1640',
priority: 'medium'
},
{
code: 'FIDA',
name: 'Financial Data Access',
fullName: 'Verordnung zum Zugang zu Finanzdaten (Entwurf)',
type: 'eu_regulation',
status: 'proposed',
effectiveDate: 'Voraussichtlich 2027',
description: 'Open Finance Framework - erweitert PSD2-Open-Banking auf Versicherungen, Investitionen, Kredite.',
relevantFor: ['Banken', 'Versicherungen', 'Fintechs', 'Datenaggregatoren'],
celex: '52023PC0360',
priority: 'medium'
},
]
// Legal basis for using EUR-Lex content
export const LEGAL_BASIS_INFO: LegalBasisInfo = {
title: 'Rechtliche Grundlage fuer RAG-Nutzung',
summary: 'EU-Rechtstexte auf EUR-Lex sind oeffentliche amtliche Dokumente und duerfen frei verwendet werden.',
details: [
{
aspect: 'EUR-Lex Dokumente',
status: 'Erlaubt',
explanation: 'Offizielle EU-Gesetzestexte, Richtlinien und Verordnungen sind gemeinfrei (Public Domain) und duerfen frei reproduziert und kommerziell genutzt werden.'
},
{
aspect: 'Text-und-Data-Mining (TDM)',
status: 'Erlaubt',
explanation: 'Art. 4 der DSM-Richtlinie (2019/790) erlaubt TDM fuer kommerzielle Zwecke, sofern kein Opt-out des Rechteinhabers vorliegt. Fuer amtliche Texte gilt kein Opt-out.'
},
{
aspect: 'AI Act Anforderungen',
status: 'Beachten',
explanation: 'Art. 53 AI Act verlangt von GPAI-Anbietern die Einhaltung des Urheberrechts. Fuer oeffentliche Rechtstexte unproblematisch.'
},
{
aspect: 'BSI-Richtlinien',
status: 'Erlaubt',
explanation: 'BSI-Publikationen sind oeffentlich zugaenglich und duerfen fuer Compliance-Zwecke verwendet werden.'
},
]
}
// Tab definitions
export const TABS: TabDef[] = [
{ id: 'overview', name: 'Uebersicht', icon: '📊' },
{ id: 'regulations', name: 'Regulierungen', icon: '📜' },
{ id: 'map', name: 'Landkarte', icon: '🗺️' },
{ id: 'search', name: 'Suche', icon: '🔍' },
{ id: 'chunks', name: 'Chunk-Browser', icon: '🧩' },
{ id: 'data', name: 'Daten', icon: '📁' },
{ id: 'ingestion', name: 'Ingestion', icon: '⚙️' },
{ id: 'pipeline', name: 'Pipeline', icon: '🔄' },
]

View File

@@ -0,0 +1,221 @@
/**
* RAG - Regulation Source URLs and License Information
*
* Extracted from rag-data.ts to stay under 500 LOC per file.
*/
// Source URLs for original documents (click to view original)
export const REGULATION_SOURCES: Record<string, string> = {
// EU Verordnungen/Richtlinien (EUR-Lex)
GDPR: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32016R0679',
EPRIVACY: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32002L0058',
SCC: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32021D0914',
DPF: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023D1795',
AIACT: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024R1689',
CRA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024R2847',
NIS2: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022L2555',
EUCSA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32019R0881',
DATAACT: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R2854',
DGA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022R0868',
DSA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022R2065',
EAA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32019L0882',
DSM: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32019L0790',
PLD: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024L2853',
GPSR: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R0988',
DORA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022R2554',
PSD2: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32015L2366',
AMLR: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32024R1624',
MiCA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R1114',
EHDS: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32025R0327',
SCC_FULL_TEXT: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32021D0914',
E_COMMERCE_RL: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32000L0031',
VERBRAUCHERRECHTE_RL: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32011L0083',
DIGITALE_INHALTE_RL: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32019L0770',
DMA: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32022R1925',
MACHINERY_REG: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R1230',
BLUE_GUIDE: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:52022XC0629(04)',
EU_IFRS: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R1803',
// EDPB Guidelines
EDPB_GUIDELINES_2_2019: 'https://www.edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-22019-processing-personal-data-under-article-61b_en',
EDPB_GUIDELINES_3_2019: 'https://www.edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-32019-processing-personal-data-through-video_en',
EDPB_GUIDELINES_5_2020: 'https://www.edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-052020-consent-under-regulation-2016679_en',
EDPB_GUIDELINES_7_2020: 'https://www.edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-072020-concepts-controller-and-processor-gdpr_en',
EDPB_GUIDELINES_1_2022: 'https://www.edpb.europa.eu/our-work-tools/our-documents/guidelines/guidelines-042022-calculation-administrative-fines-under-gdpr_en',
// BSI Technische Richtlinien
'BSI-TR-03161-1': 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-1.html',
'BSI-TR-03161-2': 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-2.html',
'BSI-TR-03161-3': 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-3.html',
// Nationale Datenschutzgesetze
AT_DSG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001597',
BDSG_FULL: 'https://www.gesetze-im-internet.de/bdsg_2018/',
CH_DSG: 'https://www.fedlex.admin.ch/eli/cc/2022/491/de',
LI_DSG: 'https://www.gesetze.li/konso/2018.272',
BE_DPA_LAW: 'https://www.autoriteprotectiondonnees.be/citoyen/la-loi-du-30-juillet-2018',
NL_UAVG: 'https://wetten.overheid.nl/BWBR0040940/',
FR_CNIL_GUIDE: 'https://www.cnil.fr/fr/rgpd-par-ou-commencer',
ES_LOPDGDD: 'https://www.boe.es/buscar/act.php?id=BOE-A-2018-16673',
IT_CODICE_PRIVACY: 'https://www.garanteprivacy.it/home/docweb/-/docweb-display/docweb/9042678',
IE_DPA_2018: 'https://www.irishstatutebook.ie/eli/2018/act/7/enacted/en/html',
UK_DPA_2018: 'https://www.legislation.gov.uk/ukpga/2018/12/contents',
UK_GDPR: 'https://www.legislation.gov.uk/eur/2016/679/contents',
NO_PERSONOPPLYSNINGSLOVEN: 'https://lovdata.no/dokument/NL/lov/2018-06-15-38',
SE_DATASKYDDSLAG: 'https://www.riksdagen.se/sv/dokument-och-lagar/dokument/svensk-forfattningssamling/lag-2018218-med-kompletterande-bestammelser_sfs-2018-218/',
FI_TIETOSUOJALAKI: 'https://www.finlex.fi/fi/laki/ajantasa/2018/20181050',
PL_UODO: 'https://isap.sejm.gov.pl/isap.nsf/DocDetails.xsp?id=WDU20180001000',
CZ_ZOU: 'https://www.zakonyprolidi.cz/cs/2019-110',
HU_INFOTV: 'https://net.jogtar.hu/jogszabaly?docid=a1100112.tv',
LU_DPA_LAW: 'https://legilux.public.lu/eli/etat/leg/loi/2018/08/01/a686/jo',
DK_DATABESKYTTELSESLOVEN: 'https://www.retsinformation.dk/eli/lta/2018/502',
// Deutschland — Weitere Gesetze
TDDDG: 'https://www.gesetze-im-internet.de/tdddg/',
DE_DDG: 'https://www.gesetze-im-internet.de/ddg/',
DE_BGB_AGB: 'https://www.gesetze-im-internet.de/bgb/__305.html',
DE_EGBGB: 'https://www.gesetze-im-internet.de/bgbeg/art_246.html',
DE_UWG: 'https://www.gesetze-im-internet.de/uwg_2004/',
DE_HGB_RET: 'https://www.gesetze-im-internet.de/hgb/__257.html',
DE_AO_RET: 'https://www.gesetze-im-internet.de/ao_1977/__147.html',
DE_TKG: 'https://www.gesetze-im-internet.de/tkg_2021/',
DE_PANGV: 'https://www.gesetze-im-internet.de/pangv_2022/',
DE_DLINFOV: 'https://www.gesetze-im-internet.de/dlinfov/',
DE_BETRVG: 'https://www.gesetze-im-internet.de/betrvg/__87.html',
DE_GESCHGEHG: 'https://www.gesetze-im-internet.de/geschgehg/',
DE_BSIG: 'https://www.gesetze-im-internet.de/bsig_2009/',
DE_USTG_RET: 'https://www.gesetze-im-internet.de/ustg_1980/__14b.html',
// Oesterreich — Weitere Gesetze
AT_ECG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20001703',
AT_TKG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20007898',
AT_KSCHG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10002462',
AT_FAGG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20008783',
AT_UGB_RET: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001702',
AT_BAO_RET: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10003940',
AT_MEDIENG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10000719',
AT_ABGB_AGB: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001622',
AT_UWG: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10002665',
// Schweiz
CH_DSV: 'https://www.fedlex.admin.ch/eli/cc/2022/568/de',
CH_OR_AGB: 'https://www.fedlex.admin.ch/eli/cc/27/317_321_377/de',
CH_UWG: 'https://www.fedlex.admin.ch/eli/cc/1988/223_223_223/de',
CH_FMG: 'https://www.fedlex.admin.ch/eli/cc/1997/2187_2187_2187/de',
CH_GEBUV: 'https://www.fedlex.admin.ch/eli/cc/2002/249/de',
CH_ZERTES: 'https://www.fedlex.admin.ch/eli/cc/2016/752/de',
CH_ZGB_PERS: 'https://www.fedlex.admin.ch/eli/cc/24/233_245_233/de',
// Industrie-Compliance
ENISA_SECURE_BY_DESIGN: 'https://www.enisa.europa.eu/publications/secure-development-best-practices',
ENISA_SUPPLY_CHAIN: 'https://www.enisa.europa.eu/publications/threat-landscape-for-supply-chain-attacks',
NIST_SSDF: 'https://csrc.nist.gov/pubs/sp/800/218/final',
NIST_CSF_2: 'https://www.nist.gov/cyberframework',
OECD_AI_PRINCIPLES: 'https://legalinstruments.oecd.org/en/instruments/OECD-LEGAL-0449',
// IFRS / EFRAG
EU_IFRS_DE: 'https://eur-lex.europa.eu/legal-content/DE/TXT/?uri=CELEX:32023R1803',
EU_IFRS_EN: 'https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32023R1803',
EFRAG_ENDORSEMENT: 'https://www.efrag.org/activities/endorsement-status-report',
// Full-text Datenschutzgesetz AT
AT_DSG_FULL: 'https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=10001597',
}
// License info for each regulation
export const REGULATION_LICENSES: Record<string, { license: string; licenseNote: string }> = {
GDPR: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk der EU — frei verwendbar' },
EPRIVACY: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
TDDDG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
SCC: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Durchfuehrungsbeschluss — amtliches Werk' },
DPF: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Angemessenheitsbeschluss — amtliches Werk' },
AIACT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
CRA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
NIS2: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
EUCSA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
DATAACT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
DGA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
DSA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
EAA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
DSM: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
PLD: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
GPSR: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
'BSI-TR-03161-1': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' },
'BSI-TR-03161-2': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' },
'BSI-TR-03161-3': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' },
DORA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
PSD2: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
AMLR: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
MiCA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
EHDS: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
AT_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
BDSG_FULL: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
CH_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
LI_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Liechtenstein — frei verwendbar' },
BE_DPA_LAW: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Belgien — frei verwendbar' },
NL_UAVG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Niederlande — frei verwendbar' },
FR_CNIL_GUIDE: { license: 'PUBLIC_DOMAIN', licenseNote: 'CNIL — oeffentliches Dokument' },
ES_LOPDGDD: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Spanien (BOE) — frei verwendbar' },
IT_CODICE_PRIVACY: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Italien — frei verwendbar' },
IE_DPA_2018: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — Ireland' },
UK_DPA_2018: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — UK' },
UK_GDPR: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — UK' },
NO_PERSONOPPLYSNINGSLOVEN: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Norwegen — frei verwendbar' },
SE_DATASKYDDSLAG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweden — frei verwendbar' },
FI_TIETOSUOJALAKI: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Finnland — frei verwendbar' },
PL_UODO: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Polen — frei verwendbar' },
CZ_ZOU: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Tschechien — frei verwendbar' },
HU_INFOTV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Ungarn — frei verwendbar' },
SCC_FULL_TEXT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Durchfuehrungsbeschluss — amtliches Werk' },
EDPB_GUIDELINES_2_2019: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' },
EDPB_GUIDELINES_3_2019: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' },
EDPB_GUIDELINES_5_2020: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' },
EDPB_GUIDELINES_7_2020: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' },
MACHINERY_REG: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
BLUE_GUIDE: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Leitfaden — amtliches Werk der Kommission' },
ENISA_SECURE_BY_DESIGN: { license: 'CC-BY-4.0', licenseNote: 'ENISA Publication — CC BY 4.0' },
ENISA_SUPPLY_CHAIN: { license: 'CC-BY-4.0', licenseNote: 'ENISA Publication — CC BY 4.0' },
NIST_SSDF: { license: 'PUBLIC_DOMAIN', licenseNote: 'US Government Work — Public Domain' },
NIST_CSF_2: { license: 'PUBLIC_DOMAIN', licenseNote: 'US Government Work — Public Domain' },
OECD_AI_PRINCIPLES: { license: 'PUBLIC_DOMAIN', licenseNote: 'OECD Legal Instrument — Reuse Notice' },
EU_IFRS_DE: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
EU_IFRS_EN: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
EFRAG_ENDORSEMENT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EFRAG — oeffentliches Dokument' },
DE_DDG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_BGB_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_EGBGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_HGB_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_AO_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_TKG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_PANGV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsche Verordnung — amtliches Werk (§5 UrhG)' },
DE_DLINFOV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsche Verordnung — amtliches Werk (§5 UrhG)' },
DE_BETRVG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_GESCHGEHG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_BSIG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
DE_USTG_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' },
AT_ECG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_TKG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_KSCHG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_FAGG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_UGB_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_BAO_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_MEDIENG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_ABGB_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
AT_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' },
CH_DSV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_OR_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_FMG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_GEBUV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_ZERTES: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
CH_ZGB_PERS: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' },
LU_DPA_LAW: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Luxemburg — frei verwendbar' },
DK_DATABESKYTTELSESLOVEN: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Daenemark — frei verwendbar' },
EDPB_GUIDELINES_1_2022: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' },
E_COMMERCE_RL: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
VERBRAUCHERRECHTE_RL: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
DIGITALE_INHALTE_RL: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' },
DMA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' },
}
// License display labels
export const LICENSE_LABELS: Record<string, string> = {
PUBLIC_DOMAIN: 'Public Domain',
'DL-DE-BY-2.0': 'DL-DE-BY 2.0',
'CC-BY-4.0': 'CC BY 4.0',
'EDPB-LICENSE': 'EDPB License',
'OGL-3.0': 'OGL v3.0',
PROPRIETARY: 'Proprietaer',
}

View File

@@ -0,0 +1,183 @@
/**
* RAG & Legal Corpus Management - Type Definitions
*/
export interface RegulationStatus {
code: string
name: string
fullName: string
type: string
chunkCount: number
expectedRequirements: number
sourceUrl: string
status: 'ready' | 'empty' | 'error'
}
export interface CollectionStatus {
collection: string
totalPoints: number
vectorSize: number
status: string
regulations: Record<string, number>
}
export interface SearchResult {
text: string
regulation_code: string
regulation_name: string
article: string | null
paragraph: string | null
source_url: string
score: number
}
export interface DsfaSource {
source_code: string
name: string
full_name?: string
organization?: string
source_url?: string
license_code: string
attribution_text: string
document_type: string
language: string
chunk_count?: number
}
export interface DsfaCorpusStatus {
qdrant_collection: string
total_sources: number
total_documents: number
total_chunks: number
qdrant_points_count: number
qdrant_status: string
}
export type RegulationCategory = 'regulations' | 'dsfa' | 'nibis' | 'templates'
export type TabId = 'overview' | 'regulations' | 'map' | 'search' | 'chunks' | 'data' | 'ingestion' | 'pipeline'
export interface CustomDocument {
id: string
code: string
title: string
filename?: string
url?: string
document_type: string
uploaded_at: string
status: 'uploaded' | 'queued' | 'fetching' | 'processing' | 'indexed' | 'error'
chunk_count: number
error?: string
}
export interface Validation {
name: string
status: 'passed' | 'warning' | 'failed' | 'not_run'
expected: any
actual: any
message: string
}
export interface PipelineCheckpoint {
phase: string
name: string
status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
started_at: string | null
completed_at: string | null
duration_seconds: number | null
metrics: Record<string, any>
validations: Validation[]
error: string | null
}
export interface PipelineState {
status: string
pipeline_id: string | null
started_at: string | null
completed_at: string | null
current_phase: string | null
checkpoints: PipelineCheckpoint[]
summary: Record<string, any>
validation_summary?: {
passed: number
warning: number
failed: number
total: number
}
}
export interface Regulation {
code: string
name: string
fullName: string
type: string
expected: number
description: string
relevantFor: string[]
keyTopics: string[]
effectiveDate: string
}
export interface Industry {
id: string
name: string
icon: string
description: string
}
export interface ThematicGroup {
id: string
name: string
color: string
regulations: string[]
description: string
}
export interface KeyIntersection {
regulations: string[]
topic: string
description: string
}
export interface FutureOutlookItem {
id: string
name: string
status: string
statusLabel: string
expectedDate: string
description: string
keyChanges: string[]
affectedRegulations: string[]
source: string
}
export interface AdditionalRegulation {
code: string
name: string
fullName: string
type: string
status: string
effectiveDate: string
description: string
relevantFor: string[]
celex: string
priority: string
}
export interface LegalBasisDetail {
aspect: string
status: string
explanation: string
}
export interface LegalBasisInfo {
title: string
summary: string
details: LegalBasisDetail[]
}
export interface TabDef {
id: TabId
name: string
icon: string
}