Files
breakpilot-lehrer/admin-lehrer/app/(admin)/ai/rag/_components/RegulationsTab.tsx
Benjamin Admin 9ba420fa91
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
Fix: Remove broken getKlausurApiUrl and clean up empty lines
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>
2026-04-24 16:02:04 +02:00

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>
)
}