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>
2232 lines
104 KiB
TypeScript
2232 lines
104 KiB
TypeScript
'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>
|
||
)
|
||
}
|