This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/ai/rag/page.tsx
BreakPilot Dev 660295e218 fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 23:40:15 -08:00

2232 lines
104 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* RAG & Legal Corpus Management
*
* Verwaltet das Legal Corpus RAG System fuer Compliance.
* Zeigt Status aller 19 Regulierungen, Chunks und ermoeglicht Suche.
*/
import React, { useState, useEffect, useCallback } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
// API uses local proxy route to klausur-service
const API_PROXY = '/api/legal-corpus'
// Types
interface RegulationStatus {
code: string
name: string
fullName: string
type: string
chunkCount: number
expectedRequirements: number
sourceUrl: string
status: 'ready' | 'empty' | 'error'
}
interface CollectionStatus {
collection: string
totalPoints: number
vectorSize: number
status: string
regulations: Record<string, number>
}
interface SearchResult {
text: string
regulation_code: string
regulation_name: string
article: string | null
paragraph: string | null
source_url: string
score: number
}
// Tab definitions
type TabId = 'overview' | 'regulations' | 'map' | 'search' | 'data' | 'ingestion' | 'pipeline'
// Custom document type
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
}
// Pipeline types
interface Validation {
name: string
status: 'passed' | 'warning' | 'failed' | 'not_run'
expected: any
actual: any
message: string
}
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
}
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
}
}
// All regulations with descriptions
const REGULATIONS = [
{
code: 'GDPR',
name: 'DSGVO',
fullName: 'Datenschutz-Grundverordnung (GDPR)',
type: 'eu_regulation',
expected: 99,
description: 'Die DSGVO ist das zentrale Datenschutzgesetz der EU. Sie regelt die Verarbeitung personenbezogener Daten und gibt Betroffenen umfangreiche Rechte (Auskunft, Loeschung, Datenportabilitaet). Gilt fuer alle Unternehmen, die Daten von EU-Buergern verarbeiten.',
relevantFor: ['Alle Unternehmen mit EU-Kunden', 'Datenverarbeiter', 'Auftragsverarbeiter'],
keyTopics: ['Einwilligung', 'Betroffenenrechte', 'Datenschutz-Folgenabschaetzung', 'Auftragsverarbeitung', 'Bussgelder bis 4% Umsatz'],
effectiveDate: '25. Mai 2018'
},
{
code: 'EPRIVACY',
name: 'ePrivacy-Richtlinie',
fullName: 'Richtlinie 2002/58/EG (ePrivacy)',
type: 'eu_directive',
expected: 25,
description: 'Ergaenzt die DSGVO speziell fuer elektronische Kommunikation. Regelt Cookies, Tracking, Direktmarketing und Vertraulichkeit der Kommunikation. Wird durch nationale Gesetze wie das TDDDG umgesetzt.',
relevantFor: ['Website-Betreiber', 'App-Entwickler', 'Marketing-Abteilungen', 'Telekommunikationsanbieter'],
keyTopics: ['Cookie-Consent', 'Tracking', 'E-Mail-Marketing', 'Vertraulichkeit'],
effectiveDate: '31. Juli 2002'
},
{
code: 'TDDDG',
name: 'TDDDG',
fullName: 'Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz',
type: 'de_law',
expected: 30,
description: 'Deutsches Umsetzungsgesetz der ePrivacy-Richtlinie. Regelt den Datenschutz bei Telemedien und Telekommunikation. Enthaelt die strengen deutschen Cookie-Consent-Anforderungen.',
relevantFor: ['Deutsche Unternehmen', 'Website-Betreiber mit deutschen Nutzern', 'App-Anbieter'],
keyTopics: ['Cookie-Banner', 'Einwilligung fuer Endgeraetezugriff', 'Tracking-Consent', 'Bestandsdaten'],
effectiveDate: '1. Dezember 2021'
},
{
code: 'SCC',
name: 'Standardvertragsklauseln',
fullName: 'EU-Standardvertragsklauseln (2021/914/EU)',
type: 'eu_regulation',
expected: 18,
description: 'Vorgefertigte Vertragsklauseln fuer den internationalen Datentransfer. Erforderlich, wenn personenbezogene Daten in Drittlaender ohne Angemessenheitsbeschluss uebermittelt werden (z.B. USA vor DPF).',
relevantFor: ['Unternehmen mit Drittland-Transfers', 'Cloud-Nutzer', 'Internationale Konzerne'],
keyTopics: ['Drittlandtransfer', 'Controller-to-Controller', 'Controller-to-Processor', 'TIA (Transfer Impact Assessment)'],
effectiveDate: '27. Juni 2021'
},
{
code: 'DPF',
name: 'EU-US Data Privacy Framework',
fullName: 'EU-US Data Privacy Framework',
type: 'eu_regulation',
expected: 12,
description: 'Angemessenheitsbeschluss fuer Datentransfers in die USA. Nachfolger des gekippten Privacy Shield. Ermoeglicht Datenuebermittlungen an zertifizierte US-Unternehmen ohne zusaetzliche Garantien.',
relevantFor: ['Unternehmen mit US-Dienstleistern', 'Cloud-Nutzer (AWS, Google, Microsoft)', 'SaaS-Anwender'],
keyTopics: ['US-Datentransfer', 'Zertifizierung', 'Redress-Mechanismus', 'Privacy Shield Nachfolger'],
effectiveDate: '10. Juli 2023'
},
{
code: 'AIACT',
name: 'EU AI Act',
fullName: 'Verordnung (EU) 2024/1689 - KI-Verordnung',
type: 'eu_regulation',
expected: 85,
description: 'Weltweit erste umfassende KI-Regulierung. Klassifiziert KI-Systeme nach Risiko (verboten, hoch, begrenzt, minimal) und stellt entsprechende Anforderungen. Hochrisiko-KI benoetigt CE-Kennzeichnung.',
relevantFor: ['KI-Entwickler', 'KI-Anwender', 'Hersteller von KI-Produkten', 'Importeure'],
keyTopics: ['Risikoklassifizierung', 'Hochrisiko-KI', 'Verbotene KI', 'Transparenzpflichten', 'CE-Kennzeichnung'],
effectiveDate: '1. August 2024 (gestaffelt bis 2027)'
},
{
code: 'CRA',
name: 'Cyber Resilience Act',
fullName: 'Verordnung (EU) 2024/2847 - Cyber Resilience Act',
type: 'eu_regulation',
expected: 45,
description: 'Cybersicherheitsanforderungen fuer Produkte mit digitalen Elementen. Verpflichtet Hersteller zu Security-by-Design, Schwachstellenmanagement und Security-Updates ueber den Lebenszyklus.',
relevantFor: ['IoT-Hersteller', 'Software-Entwickler', 'Hardware-Hersteller', 'Importeure'],
keyTopics: ['Security by Design', 'Schwachstellenmanagement', 'SBOM', 'CE-Kennzeichnung', 'Security Updates'],
effectiveDate: '2024 (Uebergangsfristen bis 2027)'
},
{
code: 'NIS2',
name: 'NIS2-Richtlinie',
fullName: 'Richtlinie (EU) 2022/2555 - Network and Information Security',
type: 'eu_directive',
expected: 46,
description: 'Cybersicherheitsrichtlinie fuer kritische und wichtige Einrichtungen. Erweitert NIS1 erheblich auf mehr Sektoren. Fordert Risikomanagement, Incident Reporting und Lieferkettensicherheit.',
relevantFor: ['Kritische Infrastrukturen (KRITIS)', 'Gesundheitswesen', 'Energie', 'Transport', 'Digitale Dienste', 'Oeffentliche Verwaltung'],
keyTopics: ['Cybersicherheits-Risikomanagement', 'Incident Reporting (24h)', 'Lieferkettensicherheit', 'Geschaeftsfuehrer-Haftung'],
effectiveDate: '17. Oktober 2024 (nationale Umsetzung)'
},
{
code: 'EUCSA',
name: 'EU Cybersecurity Act',
fullName: 'Verordnung (EU) 2019/881 - EU Cybersecurity Act',
type: 'eu_regulation',
expected: 35,
description: 'Schafft den EU-Rahmen fuer Cybersicherheitszertifizierung und staerkt die ENISA. Ermoeglicht EU-weite Zertifizierungsschemata fuer IT-Produkte, -Dienste und -Prozesse.',
relevantFor: ['IT-Produkthersteller', 'Cloud-Anbieter', 'Zertifizierungsstellen', 'Oeffentliche Auftraggeber'],
keyTopics: ['Cybersicherheitszertifizierung', 'ENISA', 'Vertrauensstufen (basic/substantial/high)', 'EUCC Schema'],
effectiveDate: '27. Juni 2019'
},
{
code: 'DATAACT',
name: 'Data Act',
fullName: 'Verordnung (EU) 2023/2854 - Data Act',
type: 'eu_regulation',
expected: 42,
description: 'Regelt den Zugang zu und die Nutzung von Daten. Gibt Nutzern Rechte an Daten, die durch vernetzte Produkte erzeugt werden. Ermoeglicht Datenweitergabe und Cloud-Wechsel.',
relevantFor: ['IoT-Hersteller', 'Cloud-Anbieter', 'Dateninhaber', 'Datennutzer', 'Oeffentliche Stellen'],
keyTopics: ['Datenzugang', 'Datenportabilitaet', 'Cloud-Switching', 'B2B-Datenteilung', 'IoT-Daten'],
effectiveDate: '12. September 2025'
},
{
code: 'DGA',
name: 'Data Governance Act',
fullName: 'Verordnung (EU) 2022/868 - Data Governance Act',
type: 'eu_regulation',
expected: 35,
description: 'Schafft Rahmenbedingungen fuer Datenmaerkte und Datenaltruismus. Regelt Datenvermittlungsdienste und die Weiterverwendung geschuetzter oeffentlicher Daten.',
relevantFor: ['Datenvermittler', 'Oeffentliche Stellen', 'Forschungseinrichtungen', 'Datenaltruismus-Organisationen'],
keyTopics: ['Datenvermittlung', 'Datenaltruismus', 'Oeffentliche Daten', 'Datentreuhaender'],
effectiveDate: '24. September 2023'
},
{
code: 'DSA',
name: 'Digital Services Act',
fullName: 'Verordnung (EU) 2022/2065 - Digital Services Act',
type: 'eu_regulation',
expected: 93,
description: 'Reguliert digitale Dienste und Plattformen. Schafft Pflichten fuer Online-Vermittler, Hosting-Dienste und Plattformen. Sehr grosse Plattformen (VLOPs) haben erweiterte Pflichten.',
relevantFor: ['Online-Plattformen', 'Marktplaetze', 'Social Media', 'Hosting-Anbieter', 'Suchmaschinen'],
keyTopics: ['Notice-and-Action', 'Transparenz', 'Illegale Inhalte', 'VLOP-Pflichten', 'Algorithmen-Transparenz'],
effectiveDate: '17. Februar 2024'
},
{
code: 'EAA',
name: 'European Accessibility Act',
fullName: 'Richtlinie (EU) 2019/882 - Barrierefreiheitsanforderungen',
type: 'eu_directive',
expected: 25,
description: 'Barrierefreiheitsanforderungen fuer Produkte und Dienstleistungen. Betrifft Computer, Smartphones, Bankdienstleistungen, E-Commerce, E-Books und mehr.',
relevantFor: ['E-Commerce', 'Banken', 'Telekommunikation', 'Verkehrsdienste', 'Medienanbieter'],
keyTopics: ['Barrierefreiheit', 'WCAG', 'Assistive Technologien', 'Ausnahmen fuer KMU'],
effectiveDate: '28. Juni 2025'
},
{
code: 'DSM',
name: 'DSM-Urheberrechtsrichtlinie',
fullName: 'Richtlinie (EU) 2019/790 - Digital Single Market Copyright',
type: 'eu_directive',
expected: 22,
description: 'Modernisiert das EU-Urheberrecht fuer das digitale Zeitalter. Enthaelt kontroverse Artikel zu Uploadfiltern (Art. 17) und Leistungsschutzrecht fuer Presseverleger (Art. 15).',
relevantFor: ['Content-Plattformen', 'Nachrichtenaggregatoren', 'Bildungseinrichtungen', 'Kulturerbe-Einrichtungen'],
keyTopics: ['Upload-Filter (Art. 17)', 'Leistungsschutzrecht', 'Text-und-Data-Mining', 'Verguetungsansprueche'],
effectiveDate: '7. Juni 2021'
},
{
code: 'PLD',
name: 'Produkthaftungsrichtlinie',
fullName: 'Richtlinie 85/374/EWG (aktualisiert) - Produkthaftung',
type: 'eu_directive',
expected: 18,
description: 'Regelt die Haftung fuer fehlerhafte Produkte. Aktualisierte Version umfasst auch Software und KI. Hersteller haften verschuldensunabhaengig fuer Produktfehler.',
relevantFor: ['Produkthersteller', 'Software-Entwickler', 'KI-Anbieter', 'Importeure'],
keyTopics: ['Verschuldensunabhaengige Haftung', 'Software als Produkt', 'KI-Haftung', 'Beweislast'],
effectiveDate: 'Ueberarbeitung 2024'
},
{
code: 'GPSR',
name: 'General Product Safety',
fullName: 'Verordnung (EU) 2023/988 - Allgemeine Produktsicherheit',
type: 'eu_regulation',
expected: 30,
description: 'Ersetzt die alte Produktsicherheitsrichtlinie. Stellt sicher, dass nur sichere Verbraucherprodukte auf den EU-Markt gelangen. Gilt auch fuer Online-Marktplaetze.',
relevantFor: ['Produkthersteller', 'Importeure', 'Online-Marktplaetze', 'Haendler'],
keyTopics: ['Produktsicherheit', 'Marktplatzhaftung', 'Rueckrufe', 'Safety Gate'],
effectiveDate: '13. Dezember 2024'
},
{
code: 'BSI-TR-03161-1',
name: 'BSI-TR Teil 1',
fullName: 'BSI TR-03161 Teil 1 - Sicherheitsanforderungen an Digitale Gesundheitsanwendungen (DiGA) - Mobile Anwendungen',
type: 'bsi_standard',
expected: 45,
description: 'Deutsche Technische Richtlinie fuer die Sicherheit mobiler Gesundheits-Apps (DiGA). Definiert Pruefverfahren und Sicherheitsanforderungen fuer die DiGA-Zulassung.',
relevantFor: ['DiGA-Hersteller', 'Mobile-App-Entwickler im Gesundheitswesen', 'Pruefstellen'],
keyTopics: ['Mobile App Security', 'Authentifizierung', 'Datenverschluesselung', 'Secure Coding'],
effectiveDate: 'Version 1.0: 2020'
},
{
code: 'BSI-TR-03161-2',
name: 'BSI-TR Teil 2',
fullName: 'BSI TR-03161 Teil 2 - Sicherheitsanforderungen an Digitale Gesundheitsanwendungen (DiGA) - Web-Anwendungen',
type: 'bsi_standard',
expected: 40,
description: 'Technische Richtlinie fuer die Sicherheit von Web-Anwendungen im Gesundheitswesen. Ergaenzt Teil 1 um spezifische Anforderungen fuer Web-Frontends.',
relevantFor: ['DiGA-Hersteller mit Web-Apps', 'Web-Entwickler im Gesundheitswesen', 'Pruefstellen'],
keyTopics: ['Web Application Security', 'OWASP Top 10', 'Session Management', 'TLS'],
effectiveDate: 'Version 1.0: 2020'
},
{
code: 'BSI-TR-03161-3',
name: 'BSI-TR Teil 3',
fullName: 'BSI TR-03161 Teil 3 - Sicherheitsanforderungen an Digitale Gesundheitsanwendungen (DiGA) - Hintergrundsysteme',
type: 'bsi_standard',
expected: 35,
description: 'Technische Richtlinie fuer Backend-Systeme von Gesundheitsanwendungen. Deckt Server, APIs, Datenbanken und Cloud-Infrastruktur ab.',
relevantFor: ['DiGA-Backend-Entwickler', 'Cloud-Architekten im Gesundheitswesen', 'DevOps-Teams'],
keyTopics: ['Backend Security', 'API Security', 'Datenbanksicherheit', 'Cloud Security', 'Logging'],
effectiveDate: 'Version 1.0: 2020'
},
// Financial Sector Regulations
{
code: 'DORA',
name: 'DORA',
fullName: 'Verordnung (EU) 2022/2554 - Digital Operational Resilience Act',
type: 'eu_regulation',
expected: 64,
description: 'Digitale operationale Resilienz fuer den Finanzsektor. Verpflichtet Finanzunternehmen zu umfassendem IKT-Risikomanagement, Vorfallmeldung, Resilienz-Tests und Drittanbieter-Management.',
relevantFor: ['Banken', 'Versicherungen', 'Wertpapierfirmen', 'Zahlungsdienstleister', 'Krypto-Anbieter', 'IKT-Drittanbieter'],
keyTopics: ['IKT-Risikomanagement', 'Incident Reporting', 'Resilience Testing', 'Third-Party Risk', 'Threat-Led Penetration Testing'],
effectiveDate: '17. Januar 2025'
},
{
code: 'PSD2',
name: 'PSD2',
fullName: 'Richtlinie (EU) 2015/2366 - Zahlungsdiensterichtlinie',
type: 'eu_directive',
expected: 117,
description: 'Reguliert Zahlungsdienste im EU-Binnenmarkt. Fuehrt Open Banking ein, verpflichtet zu starker Kundenauthentifizierung (SCA) und ermoeglicht Drittanbieterzugang zu Bankkonten.',
relevantFor: ['Banken', 'Zahlungsdienstleister', 'FinTechs', 'E-Commerce-Anbieter', 'Kontoinformationsdienste'],
keyTopics: ['Strong Customer Authentication (SCA)', 'Open Banking APIs', 'PSD2-Schnittstellen', 'Kontoinformationsdienste', 'Zahlungsauslösedienste'],
effectiveDate: '13. Januar 2018'
},
{
code: 'AMLR',
name: 'AML-Verordnung',
fullName: 'Verordnung (EU) 2024/1624 - Anti-Money Laundering Regulation',
type: 'eu_regulation',
expected: 89,
description: 'EU-weite Verordnung zur Bekaempfung von Geldwaesche und Terrorismusfinanzierung. Ersetzt die bisherigen Richtlinien durch direkt anwendbare Vorschriften. Schafft einheitliche Sorgfaltspflichten.',
relevantFor: ['Banken', 'Finanzdienstleister', 'Krypto-Anbieter', 'Immobilienmakler', 'Wirtschaftspruefer', 'Notare'],
keyTopics: ['Sorgfaltspflichten (KYC)', 'Verdachtsmeldungen', 'Wirtschaftlich Berechtigte', 'Risikobewertung', 'AMLA (neue EU-Behoerde)'],
effectiveDate: '2027 (gestaffelt)'
},
{
code: 'MiCA',
name: 'MiCA',
fullName: 'Verordnung (EU) 2023/1114 - Markets in Crypto-Assets',
type: 'eu_regulation',
expected: 149,
description: 'Umfassende Regulierung fuer Kryptowerte, Stablecoins und Crypto-Asset-Dienstleister. Schafft EU-weiten Rechtsrahmen fuer Krypto-Maerkte mit Zulassungspflichten und Verbraucherschutz.',
relevantFor: ['Krypto-Boersen', 'Wallet-Anbieter', 'Stablecoin-Emittenten', 'Token-Herausgeber', 'Krypto-Verwahrer', 'FinTechs'],
keyTopics: ['Krypto-Zulassung', 'Stablecoin-Regulierung', 'Whitepaper-Pflicht', 'Marktmissbrauch', 'Verwahrung'],
effectiveDate: '30. Dezember 2024'
},
{
code: 'EHDS',
name: 'EHDS',
fullName: 'Verordnung (EU) 2025/327 - Europaeischer Gesundheitsdatenraum',
type: 'eu_regulation',
expected: 95,
description: 'Schafft den Europaeischen Raum fuer Gesundheitsdaten. Ermoeglicht Primaernutzung (Patientenrechte) und Sekundaernutzung (Forschung, KI-Training) von Gesundheitsdaten unter strengen Auflagen.',
relevantFor: ['Krankenhaeuser', 'Aerzte', 'Gesundheits-Apps', 'Pharma', 'Forschungseinrichtungen', 'Versicherungen'],
keyTopics: ['Patientenakte (MyHealth@EU)', 'Sekundaernutzung', 'Datenzugangsorgane', 'Gesundheitsdatenstandards', 'Forschungszugang'],
effectiveDate: '2025 (gestaffelt bis 2029)'
},
]
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',
bsi_standard: 'bg-green-100 text-green-700',
}
const TYPE_LABELS: Record<string, string> = {
eu_regulation: 'EU-VO',
eu_directive: 'EU-RL',
de_law: 'DE-Gesetz',
bsi_standard: 'BSI',
}
// Industry/Sector definitions for the regulation map
const INDUSTRIES = [
{ id: 'all', name: 'Alle Unternehmen', icon: '🏢', description: 'Grundlegende Anforderungen fuer alle' },
{ id: 'health', name: 'Gesundheitswesen', icon: '🏥', description: 'Krankenhaeuser, DiGA, Medizintechnik' },
{ id: 'finance', name: 'Finanzsektor', icon: '🏦', description: 'Banken, Versicherungen, Zahlungsdienstleister' },
{ id: 'ecommerce', name: 'E-Commerce', icon: '🛒', description: 'Online-Shops, Marktplaetze' },
{ id: 'tech', name: 'Technologie/Software', icon: '💻', description: 'Software-Entwickler, SaaS, Cloud' },
{ id: 'iot', name: 'IoT/Hardware', icon: '📱', description: 'Geraetehersteller, Smart Devices' },
{ id: 'ai', name: 'KI-Anbieter', icon: '🤖', description: 'KI-Entwickler und -Anwender' },
{ id: 'kritis', name: 'Kritische Infrastruktur', icon: '⚡', description: 'Energie, Wasser, Transport' },
{ id: 'media', name: 'Medien/Plattformen', icon: '📺', description: 'Social Media, Content-Plattformen' },
{ id: 'public', name: 'Oeffentlicher Sektor', icon: '🏛️', description: 'Behoerden, Verwaltung' },
]
// Mapping: Which regulations apply to which industries
const INDUSTRY_REGULATION_MAP: Record<string, string[]> = {
all: ['GDPR', 'EPRIVACY', 'TDDDG'],
health: ['GDPR', 'TDDDG', 'BSI-TR-03161-1', 'BSI-TR-03161-2', 'BSI-TR-03161-3', 'NIS2', 'AIACT', 'PLD', 'EHDS'],
finance: ['GDPR', 'TDDDG', 'NIS2', 'EUCSA', 'DSA', 'AIACT', 'DPF', 'DORA', 'PSD2', 'AMLR', 'MiCA'],
ecommerce: ['GDPR', 'TDDDG', 'DSA', 'GPSR', 'EAA', 'PLD', 'DPF', 'PSD2'],
tech: ['GDPR', 'TDDDG', 'CRA', 'AIACT', 'DPF', 'SCC', 'DATAACT', 'DSM', 'MiCA'],
iot: ['GDPR', 'CRA', 'GPSR', 'PLD', 'DATAACT', 'AIACT'],
ai: ['GDPR', 'AIACT', 'PLD', 'DSM', 'DATAACT'],
kritis: ['GDPR', 'NIS2', 'EUCSA', 'CRA', 'AIACT', 'DORA'],
media: ['GDPR', 'TDDDG', 'DSA', 'DSM', 'EAA', 'AIACT'],
public: ['GDPR', 'NIS2', 'EUCSA', 'EAA', 'DGA', 'AIACT', 'EHDS'],
}
// Thematic groupings showing overlaps
const THEMATIC_GROUPS = [
{
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'],
description: 'Sicherheitsanforderungen, CE-Kennzeichnung, 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'
},
]
// Key overlaps and intersections
const KEY_INTERSECTIONS = [
{
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
const FUTURE_OUTLOOK = [
{
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'
},
]
// Additional regulations that could be added to RAG (publicly available, commercial use permitted)
// Regulations now integrated in main RAG (previously listed as candidates)
const INTEGRATED_REGULATIONS = [
{
code: 'DORA',
name: 'Digital Operational Resilience Act',
status: 'integrated',
addedDate: 'Januar 2025',
description: 'Jetzt im RAG verfuegbar - Finanzsektor IT-Resilienz'
},
{
code: 'MiCA',
name: 'Markets in Crypto-Assets',
status: 'integrated',
addedDate: 'Januar 2025',
description: 'Jetzt im RAG verfuegbar - Krypto-Regulierung'
},
{
code: 'PSD2',
name: 'Payment Services Directive 2',
status: 'integrated',
addedDate: 'Januar 2025',
description: 'Jetzt im RAG verfuegbar - Zahlungsdienste'
},
{
code: 'AMLR',
name: 'AML-Verordnung',
status: 'integrated',
addedDate: 'Januar 2025',
description: 'Jetzt im RAG verfuegbar - Geldwaeschebekaempfung'
},
{
code: 'EHDS',
name: 'European Health Data Space',
status: 'integrated',
addedDate: 'Januar 2025',
description: 'Jetzt im RAG verfuegbar - Gesundheitsdatenraum'
},
]
// Potential future regulations (not yet integrated)
const ADDITIONAL_REGULATIONS = [
{
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
const LEGAL_BASIS_INFO = {
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.'
},
]
}
export default function RAGPage() {
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>('')
// 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 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) {
// Wait a moment then refresh pipeline status
setTimeout(() => {
fetchPipeline()
setPipelineStarting(false)
}, 2000)
} else {
setPipelineStarting(false)
}
} catch (error) {
console.error('Failed to start pipeline:', error)
setPipelineStarting(false)
}
}
useEffect(() => {
fetchStatus()
}, [fetchStatus])
useEffect(() => {
if (activeTab === 'pipeline') {
fetchPipeline()
}
}, [activeTab, fetchPipeline])
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() // Also refresh chunk counts
}, 5000) // Every 5 seconds
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])
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')])
// Poll for status
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
}
const tabs = [
{ id: 'overview' as TabId, name: 'Uebersicht', icon: '📊' },
{ id: 'regulations' as TabId, name: 'Regulierungen', icon: '📜' },
{ id: 'map' as TabId, name: 'Landkarte', icon: '🗺️' },
{ id: 'search' as TabId, name: 'Suche', icon: '🔍' },
{ id: 'data' as TabId, name: 'Daten', icon: '📁' },
{ id: 'ingestion' as TabId, name: 'Ingestion', icon: '⚙️' },
{ id: 'pipeline' as TabId, name: 'Pipeline', icon: '🔄' },
]
return (
<div className="min-h-screen bg-slate-50">
{/* Header */}
<div className="bg-white border-b px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">Daten & RAG</h1>
<p className="text-slate-600">Legal Corpus Management fuer Compliance</p>
</div>
<Link
href="/ai"
className="flex items-center gap-2 text-slate-600 hover:text-slate-800"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
KI & Automatisierung
</Link>
</div>
</div>
<div className="p-6">
{/* Page Purpose */}
<PagePurpose
title="Legal Corpus RAG"
purpose="Das Legal Corpus RAG System indexiert alle 19 relevanten Regulierungen (DSGVO, AI Act, CRA, BSI TR-03161, etc.) fuer semantische Suche waehrend UCCA-Assessments. Die Dokumente werden in Chunks aufgeteilt und mit BGE-M3 Embeddings indexiert."
audience={['DSB', 'Compliance Officer', 'Entwickler']}
gdprArticles={['§5 UrhG (Amtliche Werke)', 'Art. 5 DSGVO (Rechenschaftspflicht)']}
architecture={{
services: ['klausur-service (Python)', 'embedding-service (BGE-M3)', 'Qdrant (Vector DB)'],
databases: ['Qdrant Collection: bp_legal_corpus'],
}}
relatedPages={[
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Compliance-Dashboard' },
{ name: 'Requirements', href: '/compliance/requirements', description: 'Anforderungskatalog' },
]}
/>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl p-4 border border-slate-200">
<p className="text-sm text-slate-500">Regulierungen</p>
<p className="text-2xl font-bold text-slate-900">{REGULATIONS.length}</p>
</div>
<div className="bg-white rounded-xl p-4 border border-slate-200">
<p className="text-sm text-slate-500">Chunks Total</p>
<p className="text-2xl font-bold text-teal-600">{loading ? '-' : getTotalChunks().toLocaleString()}</p>
</div>
<div className="bg-white rounded-xl p-4 border border-slate-200">
<p className="text-sm text-slate-500">Vector Size</p>
<p className="text-2xl font-bold text-slate-700">{collectionStatus?.vectorSize || 1024}</p>
</div>
<div className={`bg-white rounded-xl p-4 border ${
collectionStatus?.status === 'green' ? 'border-green-200' : 'border-slate-200'
}`}>
<p className="text-sm text-slate-500">Status</p>
<p className={`text-2xl font-bold ${
collectionStatus?.status === 'green' ? 'text-green-600' : 'text-slate-600'
}`}>
{collectionStatus?.status === 'green' ? '✓ Ready' : loading ? '-' : collectionStatus?.status || 'N/A'}
</p>
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 border-b border-slate-200">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
activeTab === tab.id
? 'border-teal-600 text-teal-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.name}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* 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 totalChunks = regs.reduce((sum, r) => sum + getRegulationChunks(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">{regs.length} Dok.</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) => getRegulationChunks(b.code) - getRegulationChunks(a.code))
.slice(0, 5)
.map((reg) => {
const chunks = getRegulationChunks(reg.code)
return (
<div key={reg.code} className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<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 text-teal-600">{chunks.toLocaleString()} Chunks</span>
</div>
)
})}
</div>
</div>
</div>
)}
{activeTab === 'regulations' && (
<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</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-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 = getRegulationChunks(reg.code)
const ratio = chunks / (reg.expected * 10) // Rough estimate: 10 chunks per requirement
let statusColor = 'text-red-500'
let statusIcon = '❌'
if (ratio > 0.5) {
statusColor = 'text-green-500'
statusIcon = '✓'
} else if (ratio > 0.1) {
statusColor = 'text-yellow-500'
statusIcon = '⚠'
}
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 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">{chunks.toLocaleString()}</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={6} 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">
<span>In Kraft seit: {reg.effectiveDate}</span>
<button
onClick={(e) => {
e.stopPropagation()
setSearchQuery(reg.name)
setActiveTab('search')
}}
className="text-teal-600 hover:text-teal-700 font-medium"
>
In Chunks suchen
</button>
</div>
</div>
</td>
</tr>
)}
</React.Fragment>
)
})}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'map' && (
<div className="space-y-6">
{/* Industry Filter */}
<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) => (
<div
key={reg.code}
className="bg-white p-3 rounded-lg border 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>
</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>
{/* Thematic Groups */}
<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)
return (
<span
key={code}
className="px-3 py-1.5 bg-slate-100 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-200 cursor-pointer"
onClick={() => {
setActiveTab('regulations')
setExpandedRegulation(code)
}}
title={reg?.fullName || code}
>
{code}
</span>
)
})}
</div>
</div>
</div>
))}
</div>
</div>
{/* Key Intersections */}
<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 bg-teal-100 text-teal-700 rounded"
>
{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>
{/* Regulation Matrix */}
<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">Welche Regulierungen in welchen Branchen gelten</p>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead className="bg-slate-50 border-b">
<tr>
<th className="px-2 py-2 text-left font-medium text-slate-500 sticky left-0 bg-slate-50">Regulierung</th>
{INDUSTRIES.filter(i => i.id !== 'all').map((industry) => (
<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 className="divide-y">
{REGULATIONS.map((reg) => (
<tr key={reg.code} className="hover:bg-slate-50">
<td className="px-2 py-2 font-medium text-teal-600 sticky left-0 bg-white">
{reg.code}
</td>
{INDUSTRIES.filter(i => i.id !== 'all').map((industry) => {
const applies = INDUSTRY_REGULATION_MAP[industry.id]?.includes(reg.code)
return (
<td key={industry.id} className="px-2 py-2 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>
))}
</tbody>
</table>
</div>
</div>
{/* Future Outlook Section */}
<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>
{/* Integrated Regulations */}
<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">Neu integrierte Regulierungen</h3>
<p className="text-sm text-slate-500">Jetzt im RAG-System verfuegbar (Stand: Januar 2025)</p>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{INTEGRATED_REGULATIONS.map((reg) => (
<div key={reg.code} className="rounded-lg border border-green-200 bg-green-50 p-3 text-center">
<span className="px-2 py-1 text-sm font-bold bg-green-100 text-green-700 rounded">
{reg.code}
</span>
<p className="text-xs text-slate-600 mt-2">{reg.name}</p>
<p className="text-xs text-green-600 mt-1">Im RAG</p>
</div>
))}
</div>
</div>
{/* Potential Future Regulations */}
<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>
{/* Legal Basis Info */}
<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>
</div>
)}
{activeTab === 'search' && (
<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) =>
prev.includes(code) ? prev.filter((c) => 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>
)}
{activeTab === 'data' && (
<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>
)}
{activeTab === 'ingestion' && (
<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.824 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>
)}
{activeTab === 'pipeline' && (
<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>
{/* Running Time Indicator */}
{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">
{/* Auto-Refresh Toggle */}
<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>
{/* Start Pipeline Button */}
{(!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 ? (
<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.824 3 7.938l3-2.647z" />
</svg>
) : (
<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>
)}
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 ? (
<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.824 3 7.938l3-2.647z" />
</svg>
) : (
<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>
)}
Aktualisieren
</button>
</div>
</div>
{/* No Data */}
{(!pipelineState || pipelineState.status === 'no_data') && !pipelineLoading && (
<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.824 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>
)}
{/* Pipeline Status */}
{pipelineState && pipelineState.status !== 'no_data' && (
<>
{/* Status Card */}
<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.824 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>
{/* Current Progress - only when running */}
{pipelineState.status === 'running' && pipelineState.current_phase && (
<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>
)}
{/* Validation Summary */}
{pipelineState.validation_summary && (
<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">{pipelineState.validation_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">{pipelineState.validation_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">{pipelineState.validation_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">{pipelineState.validation_summary.total}</p>
</div>
</div>
)}
{/* Checkpoints */}
<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 ({pipelineState.checkpoints?.length || 0})</h3>
</div>
<div className="divide-y">
{pipelineState.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>
))}
{(!pipelineState.checkpoints || pipelineState.checkpoints.length === 0) && (
<div className="p-4 text-center text-slate-500">
Noch keine Checkpoints vorhanden.
</div>
)}
</div>
</div>
{/* Summary */}
{Object.keys(pipelineState.summary || {}).length > 0 && (
<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(pipelineState.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>
)}
</>
)}
</div>
)}
</div>
</div>
)
}