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>
452 lines
20 KiB
TypeScript
452 lines
20 KiB
TypeScript
'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>
|
|
)
|
|
}
|