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)/dsgvo/advisory-board/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

1955 lines
83 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.
'use client'
/**
* UCCA - Use-Case Compliance & Feasibility Advisor
*
* Wizard-based intake for AI use cases with result dashboard
* Supports Normal Mode (simple language) and Expert Mode (technical terms)
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import Link from 'next/link'
// ============================================================================
// Types
// ============================================================================
type WizardMode = 'normal' | 'expert'
interface DataTypes {
personal_data: boolean
article_9_data: boolean
minor_data: boolean
license_plates: boolean
images: boolean
audio: boolean
location_data: boolean
biometric_data: boolean
financial_data: boolean
employee_data: boolean
customer_data: boolean
public_data: boolean
}
interface Purpose {
customer_support: boolean
marketing: boolean
analytics: boolean
automation: boolean
evaluation_scoring: boolean
decision_making: boolean
profiling: boolean
research: boolean
internal_tools: boolean
public_service: boolean
}
interface Outputs {
recommendations_to_users: boolean
rankings_or_scores: boolean
legal_effects: boolean
access_decisions: boolean
content_generation: boolean
data_export: boolean
}
interface Hosting {
provider: string
region: string
data_residency: string
}
interface ModelUsage {
rag: boolean
finetune: boolean
training: boolean
inference: boolean
}
interface Retention {
store_prompts: boolean
store_responses: boolean
retention_days: number
anonymize_after_use: boolean
}
interface UseCaseIntake {
use_case_text: string
domain: string
title: string
data_types: DataTypes
purpose: Purpose
automation: string
outputs: Outputs
hosting: Hosting
model_usage: ModelUsage
retention: Retention
store_raw_text: boolean
}
interface TriggeredRule {
code: string
category: string
title: string
description: string
severity: 'INFO' | 'WARN' | 'BLOCK'
score_delta: number
gdpr_ref: string
rationale: string
}
interface RequiredControl {
id: string
title: string
description: string
severity: string
category: string
gdpr_ref: string
}
interface PatternRecommendation {
pattern_id: string
title: string
description: string
rationale: string
priority: number
}
interface ForbiddenPattern {
pattern_id: string
title: string
description: string
reason: string
gdpr_ref: string
}
interface ExampleMatch {
example_id: string
title: string
description: string
similarity: number
outcome: string
lessons: string
}
interface AssessmentResult {
feasibility: 'YES' | 'CONDITIONAL' | 'NO'
risk_level: 'MINIMAL' | 'LOW' | 'MEDIUM' | 'HIGH' | 'UNACCEPTABLE'
complexity: 'LOW' | 'MEDIUM' | 'HIGH'
risk_score: number
triggered_rules: TriggeredRule[]
required_controls: RequiredControl[]
recommended_architecture: PatternRecommendation[]
forbidden_patterns: ForbiddenPattern[]
example_matches: ExampleMatch[]
dsfa_recommended: boolean
art22_risk: boolean
training_allowed: string
summary: string
recommendation: string
alternative_approach?: string
}
interface Assessment {
id: string
title: string
created_at: string
feasibility: string
risk_level: string
risk_score: number
domain: string
explanation_text?: string
}
// Problem-Solution types from API
interface ProblemTrigger {
rule: string
without_control?: string
}
interface Solution {
id: string
title: string
pattern?: string
control?: string
removes_problem: boolean
team_question: string
}
interface ProblemSolution {
problem_id: string
title: string
triggers: ProblemTrigger[]
solutions: Solution[]
}
// ============================================================================
// Domain Configuration with Keywords for Auto-Detection
// ============================================================================
interface DomainConfig {
value: string
label: string
labelExpert?: string
keywords: string[]
category: string
}
const DOMAINS: DomainConfig[] = [
// Industrie & Produktion
{ value: 'automotive', label: 'Automobil & Fahrzeugbau', keywords: ['auto', 'fahrzeug', 'kfz', 'pkw', 'lkw', 'motor', 'antrieb', 'karosserie', 'zulieferer', 'oem', 'tier1', 'tier2', 'werkstatt', 'autohaus'], category: 'Industrie' },
{ value: 'mechanical_engineering', label: 'Maschinenbau', keywords: ['maschine', 'maschinenbau', 'anlage', 'fertigung', 'produktion', 'werkzeug', 'cnc', 'roboter', 'automatisierung'], category: 'Industrie' },
{ value: 'plant_engineering', label: 'Anlagenbau', keywords: ['anlagenbau', 'grossanlage', 'industrieanlage', 'kraftwerk', 'raffinerie', 'chemieanlage'], category: 'Industrie' },
{ value: 'electrical_engineering', label: 'Elektrotechnik', keywords: ['elektro', 'elektronik', 'schaltung', 'steuerung', 'sps', 'antrieb', 'motor', 'transformator'], category: 'Industrie' },
{ value: 'aerospace', label: 'Luft- & Raumfahrt', keywords: ['flugzeug', 'luftfahrt', 'raumfahrt', 'satellit', 'rakete', 'aviation', 'aerospace', 'airline'], category: 'Industrie' },
{ value: 'chemicals', label: 'Chemie & Pharma', keywords: ['chemie', 'pharma', 'labor', 'wirkstoff', 'medikament', 'arzneimittel', 'kosmetik', 'kunststoff'], category: 'Industrie' },
{ value: 'food_beverage', label: 'Lebensmittel & Getraenke', keywords: ['lebensmittel', 'nahrung', 'getraenk', 'brauerei', 'baeckerei', 'fleisch', 'molkerei', 'food'], category: 'Industrie' },
{ value: 'textiles', label: 'Textil & Bekleidung', keywords: ['textil', 'bekleidung', 'mode', 'fashion', 'stoff', 'naeherei', 'konfektion'], category: 'Industrie' },
{ value: 'packaging', label: 'Verpackung', keywords: ['verpackung', 'packaging', 'karton', 'folie', 'etikette', 'abfuellung'], category: 'Industrie' },
// Energie & Versorgung
{ value: 'utilities', label: 'Stadtwerke & Versorgung', keywords: ['stadtwerk', 'versorgung', 'wasser', 'gas', 'strom', 'fernwaerme', 'entsorgung', 'abfall', 'muell'], category: 'Energie' },
{ value: 'energy', label: 'Energie & Kraftwerke', keywords: ['energie', 'kraftwerk', 'solar', 'wind', 'photovoltaik', 'erneuerbar', 'netz', 'einspeise'], category: 'Energie' },
{ value: 'oil_gas', label: 'Oel & Gas', keywords: ['oel', 'gas', 'pipeline', 'raffinerie', 'tankstelle', 'bohren', 'foerder'], category: 'Energie' },
// Land- & Forstwirtschaft
{ value: 'agriculture', label: 'Landwirtschaft & Agrar', keywords: ['agrar', 'landwirt', 'bauer', 'acker', 'ernte', 'saat', 'duenger', 'pflanzenschutz', 'traktor', 'wetterstation', 'precision farming', 'smart farming'], category: 'Agrar' },
{ value: 'forestry', label: 'Forstwirtschaft', keywords: ['forst', 'wald', 'holz', 'saege', 'foerster', 'jagd'], category: 'Agrar' },
{ value: 'fishing', label: 'Fischerei & Aquakultur', keywords: ['fisch', 'aquakultur', 'meeresfruchte', 'fischzucht', 'trawler'], category: 'Agrar' },
// Bau & Immobilien
{ value: 'construction', label: 'Bauwesen & Tiefbau', keywords: ['bau', 'baustelle', 'hochbau', 'tiefbau', 'architekt', 'statik', 'beton', 'fundament'], category: 'Bau' },
{ value: 'real_estate', label: 'Immobilien', keywords: ['immobilie', 'makler', 'wohnung', 'miete', 'vermietung', 'hausverwaltung', 'wohnungsbau'], category: 'Bau' },
{ value: 'facility_management', label: 'Facility Management', keywords: ['facility', 'gebaeudemanagement', 'hausmeister', 'reinigung', 'wartung', 'instandhaltung'], category: 'Bau' },
// Gesundheit & Soziales
{ value: 'healthcare', label: 'Gesundheit & Medizin', keywords: ['gesundheit', 'medizin', 'arzt', 'krankenhaus', 'klinik', 'praxis', 'patient', 'diagnose', 'therapie', 'pflege'], category: 'Gesundheit' },
{ value: 'medical_devices', label: 'Medizintechnik', keywords: ['medizintechnik', 'medtech', 'implantat', 'roentgen', 'mrt', 'ct', 'ultraschall', 'beatmung', 'dialyse'], category: 'Gesundheit' },
{ value: 'pharma', label: 'Pharmaindustrie', keywords: ['pharma', 'arzneimittel', 'medikament', 'zulassung', 'klinische studie', 'wirkstoff'], category: 'Gesundheit' },
{ value: 'elderly_care', label: 'Altenpflege & Betreuung', keywords: ['altenpflege', 'seniorenheim', 'pflegeheim', 'betreuung', 'demenz', 'ambulante pflege'], category: 'Gesundheit' },
{ value: 'social_services', label: 'Soziale Dienste', keywords: ['sozial', 'jugendamt', 'beratung', 'hilfe', 'integration', 'behinderung', 'inklusion'], category: 'Gesundheit' },
// Bildung & Forschung
{ value: 'education', label: 'Bildung & Schule', keywords: ['schule', 'bildung', 'lehrer', 'schueler', 'unterricht', 'abitur', 'klausur', 'gymnasium', 'grundschule'], category: 'Bildung' },
{ value: 'higher_education', label: 'Hochschule & Universitaet', keywords: ['universitaet', 'hochschule', 'studium', 'student', 'professor', 'forschung', 'promotion'], category: 'Bildung' },
{ value: 'vocational_training', label: 'Berufsausbildung', keywords: ['ausbildung', 'azubi', 'lehrling', 'berufsschule', 'ihk', 'handwerkskammer', 'meister'], category: 'Bildung' },
{ value: 'research', label: 'Forschung & Entwicklung', keywords: ['forschung', 'entwicklung', 'f&e', 'r&d', 'labor', 'innovation', 'patent'], category: 'Bildung' },
// Finanzen & Versicherung
{ value: 'finance', label: 'Finanzen & Buchhaltung', keywords: ['finanzen', 'buchhaltung', 'controlling', 'bilanz', 'rechnungswesen', 'steuer', 'wirtschaftspruef'], category: 'Finanzen' },
{ value: 'banking', label: 'Banken & Kreditinstitute', keywords: ['bank', 'kredit', 'darlehen', 'hypothek', 'sparkasse', 'volksbank', 'girokonto', 'depot'], category: 'Finanzen' },
{ value: 'insurance', label: 'Versicherungen', keywords: ['versicherung', 'police', 'schaden', 'regulierung', 'praemie', 'lebensversicherung', 'krankenversicherung'], category: 'Finanzen' },
{ value: 'investment', label: 'Investment & Vermoegensverwaltung', keywords: ['investment', 'fonds', 'aktie', 'wertpapier', 'portfolio', 'asset', 'vermoegen'], category: 'Finanzen' },
// Handel & Logistik
{ value: 'retail', label: 'Einzelhandel', keywords: ['einzelhandel', 'geschaeft', 'laden', 'filiale', 'supermarkt', 'kasse', 'warenhaus'], category: 'Handel' },
{ value: 'ecommerce', label: 'E-Commerce & Online-Handel', keywords: ['ecommerce', 'online', 'shop', 'webshop', 'bestellung', 'versand', 'retoure', 'warenkorb'], category: 'Handel' },
{ value: 'wholesale', label: 'Grosshandel', keywords: ['grosshandel', 'b2b', 'distributor', 'haendler', 'lieferant', 'einkauf'], category: 'Handel' },
{ value: 'logistics', label: 'Logistik & Transport', keywords: ['logistik', 'transport', 'spedition', 'lager', 'lieferkette', 'supply chain', 'fracht', 'versand'], category: 'Handel' },
// IT & Telekommunikation
{ value: 'it_services', label: 'IT & Software', keywords: ['it', 'software', 'entwicklung', 'programmierung', 'cloud', 'saas', 'rechenzentrum', 'server'], category: 'IT' },
{ value: 'telecom', label: 'Telekommunikation', keywords: ['telekom', 'mobilfunk', 'netz', 'provider', 'glasfaser', 'breitband', 'telefon'], category: 'IT' },
{ value: 'cybersecurity', label: 'IT-Sicherheit', keywords: ['sicherheit', 'security', 'cyber', 'firewall', 'penetration', 'hacking', 'datenschutz'], category: 'IT' },
// Recht & Beratung
{ value: 'legal', label: 'Recht & Anwaelte', keywords: ['recht', 'anwalt', 'kanzlei', 'vertrag', 'klage', 'gericht', 'notar', 'jurist'], category: 'Beratung' },
{ value: 'consulting', label: 'Unternehmensberatung', keywords: ['beratung', 'consulting', 'strategie', 'management', 'transformation', 'prozess'], category: 'Beratung' },
{ value: 'tax_advisory', label: 'Steuerberatung', keywords: ['steuerberater', 'steuerkanzlei', 'finanzamt', 'steuererklaerung', 'lohnsteuer', 'umsatzsteuer'], category: 'Beratung' },
// Oeffentlicher Sektor
{ value: 'public_sector', label: 'Behoerden & Verwaltung', keywords: ['behoerde', 'verwaltung', 'amt', 'rathaus', 'buergeramt', 'gemeinde', 'kommune', 'oeffentlich'], category: 'Oeffentlich' },
{ value: 'defense', label: 'Verteidigung & Sicherheit', keywords: ['bundeswehr', 'militaer', 'verteidigung', 'ruestung', 'polizei', 'sicherheitsbehoerde'], category: 'Oeffentlich' },
{ value: 'justice', label: 'Justiz', keywords: ['justiz', 'gericht', 'staatsanwalt', 'richter', 'strafverfolgung', 'jva'], category: 'Oeffentlich' },
// Marketing & Medien
{ value: 'marketing', label: 'Werbung & Marketing', keywords: ['marketing', 'werbung', 'kampagne', 'marke', 'brand', 'agentur', 'media', 'seo', 'social media'], category: 'Medien' },
{ value: 'media', label: 'Medien & Verlage', keywords: ['medien', 'verlag', 'zeitung', 'magazin', 'redaktion', 'journalist', 'presse', 'rundfunk', 'tv', 'radio'], category: 'Medien' },
{ value: 'entertainment', label: 'Unterhaltung & Gaming', keywords: ['unterhaltung', 'spiel', 'gaming', 'film', 'musik', 'event', 'konzert', 'kino'], category: 'Medien' },
// HR & Personal
{ value: 'hr', label: 'Personalwesen', keywords: ['personal', 'hr', 'human resources', 'mitarbeiter', 'recruiting', 'bewerbung', 'gehalt', 'lohn', 'personalentwicklung'], category: 'HR' },
{ value: 'recruiting', label: 'Recruiting & Stellenvermittlung', keywords: ['recruiting', 'headhunter', 'stellenvermittlung', 'jobboerse', 'bewerber', 'talent'], category: 'HR' },
// Tourismus & Gastronomie
{ value: 'hospitality', label: 'Hotellerie & Gastronomie', keywords: ['hotel', 'restaurant', 'gastronomie', 'gastro', 'kueche', 'koch', 'rezeption', 'zimmer', 'buchung'], category: 'Tourismus' },
{ value: 'tourism', label: 'Tourismus & Reise', keywords: ['tourismus', 'reise', 'urlaub', 'reisebuero', 'flug', 'pauschalreise', 'kreuzfahrt', 'tourist'], category: 'Tourismus' },
// Sonstige
{ value: 'nonprofit', label: 'Gemeinnuetzig & NGO', keywords: ['gemeinnuetzig', 'ngo', 'verein', 'stiftung', 'spende', 'ehrenamt', 'wohltaetig'], category: 'Sonstige' },
{ value: 'sports', label: 'Sport & Fitness', keywords: ['sport', 'fitness', 'verein', 'trainer', 'athlet', 'stadion', 'wettkampf'], category: 'Sonstige' },
{ value: 'general', label: 'Allgemein / Branchenuebergreifend', keywords: ['allgemein', 'sonstig', 'andere', 'uebergreifend'], category: 'Sonstige' },
]
// Group domains by category for the dropdown
const DOMAIN_CATEGORIES = [...new Set(DOMAINS.map(d => d.category))]
/**
* Analyzes text and suggests matching domains based on keywords
*/
function suggestDomainsFromText(text: string): { value: string; label: string; score: number }[] {
if (!text || text.length < 3) return []
const normalizedText = text.toLowerCase()
const suggestions: { value: string; label: string; score: number }[] = []
for (const domain of DOMAINS) {
let score = 0
for (const keyword of domain.keywords) {
if (normalizedText.includes(keyword.toLowerCase())) {
// Longer keywords get higher scores (more specific)
score += keyword.length
}
}
if (score > 0) {
suggestions.push({ value: domain.value, label: domain.label, score })
}
}
// Sort by score descending, take top 3
return suggestions.sort((a, b) => b.score - a.score).slice(0, 3)
}
// ============================================================================
// Label Maps for Normal vs Expert Mode
// ============================================================================
const LABELS = {
// Domains - now dynamically generated from DOMAINS array
domains: {
normal: DOMAINS.map(d => ({ value: d.value, label: d.label, category: d.category })),
expert: DOMAINS.map(d => ({ value: d.value, label: d.labelExpert || d.label, category: d.category })),
},
// Data Types
dataTypes: {
normal: [
{ key: 'personal_data', label: 'Namen, E-Mails, Adressen', hint: 'Alles womit man Personen identifizieren kann' },
{ key: 'article_9_data', label: 'Gesundheit, Religion, politische Meinung', hint: 'Besonders geschuetzte Daten', risk: true },
{ key: 'minor_data', label: 'Daten von Kindern/Jugendlichen', hint: 'Unter 18 Jahre', risk: true },
{ key: 'license_plates', label: 'Auto-Kennzeichen', hint: 'KFZ-Nummernschilder' },
{ key: 'images', label: 'Fotos von Personen', hint: 'Gesichter erkennbar' },
{ key: 'audio', label: 'Sprachaufnahmen', hint: 'Gespraeche, Telefonate' },
{ key: 'location_data', label: 'Standorte & Bewegungsdaten', hint: 'GPS, Aufenthaltsorte' },
{ key: 'biometric_data', label: 'Fingerabdruecke, Gesichtserkennung', hint: 'Koerperliche Merkmale', risk: true },
{ key: 'financial_data', label: 'Gehaelter, Kontodaten', hint: 'Finanzielle Informationen' },
{ key: 'employee_data', label: 'Mitarbeiter-Informationen', hint: 'Personalakten, Bewertungen' },
{ key: 'customer_data', label: 'Kundendaten', hint: 'Bestellungen, Kontaktdaten' },
{ key: 'public_data', label: 'Nur oeffentliche Daten', hint: 'Keine personenbezogenen Daten', safe: true },
],
expert: [
{ key: 'personal_data', label: 'Personenbezogene Daten', hint: 'Art. 4(1) DSGVO' },
{ key: 'article_9_data', label: 'Besondere Kategorien (Art. 9)', hint: 'Gesundheit, Religion, etc.', risk: true },
{ key: 'minor_data', label: 'Daten von Minderjaehrigen', hint: 'Art. 8 DSGVO', risk: true },
{ key: 'license_plates', label: 'KFZ-Kennzeichen', hint: 'Personenbezug moeglich' },
{ key: 'images', label: 'Bilder von Personen', hint: 'Biometrische Daten moeglich' },
{ key: 'audio', label: 'Audioaufnahmen', hint: 'Stimmprofile moeglich' },
{ key: 'location_data', label: 'Standortdaten', hint: 'Bewegungsprofile' },
{ key: 'biometric_data', label: 'Biometrische Daten', hint: 'Art. 9(1) DSGVO', risk: true },
{ key: 'financial_data', label: 'Finanzdaten', hint: 'Bankdaten, Gehaelter' },
{ key: 'employee_data', label: 'Mitarbeiterdaten', hint: 'Beschaeftigtendatenschutz' },
{ key: 'customer_data', label: 'Kundendaten', hint: 'B2C/B2B Kontakte' },
{ key: 'public_data', label: 'Nur oeffentliche Daten', hint: 'Kein Personenbezug', safe: true },
],
},
// Purpose
purpose: {
normal: [
{ key: 'customer_support', label: 'Kundenservice & Support', hint: 'Fragen beantworten, Hilfe anbieten' },
{ key: 'marketing', label: 'Werbung & Newsletter', hint: 'Kunden ansprechen, Kampagnen' },
{ key: 'analytics', label: 'Auswertungen & Berichte', hint: 'Zahlen analysieren, Trends erkennen' },
{ key: 'automation', label: 'Arbeitsablaeufe automatisieren', hint: 'Routineaufgaben von KI erledigen lassen' },
{ key: 'evaluation_scoring', label: 'Personen bewerten oder einstufen', hint: 'Noten, Scores, Rankings', risk: true },
{ key: 'decision_making', label: 'Entscheidungen treffen', hint: 'Genehmigungen, Ablehnungen', risk: true },
{ key: 'profiling', label: 'Personenprofile erstellen', hint: 'Verhaltensmuster analysieren', risk: true },
{ key: 'research', label: 'Forschung & Entwicklung', hint: 'Neue Erkenntnisse gewinnen' },
{ key: 'internal_tools', label: 'Interne Werkzeuge', hint: 'Nur fuer Mitarbeiter' },
{ key: 'public_service', label: 'Service fuer Buerger/Kunden', hint: 'Oeffentlich zugaenglich' },
],
expert: [
{ key: 'customer_support', label: 'Kundenservice', hint: 'Support, Chatbots' },
{ key: 'marketing', label: 'Marketing', hint: 'Kampagnen, Targeting' },
{ key: 'analytics', label: 'Analyse', hint: 'Business Intelligence' },
{ key: 'automation', label: 'Automatisierung', hint: 'Prozessautomation' },
{ key: 'evaluation_scoring', label: 'Bewertung / Scoring', hint: 'Art. 22 relevant', risk: true },
{ key: 'decision_making', label: 'Entscheidungsfindung', hint: 'Automated Decision Making', risk: true },
{ key: 'profiling', label: 'Profiling', hint: 'Art. 4(4) DSGVO', risk: true },
{ key: 'research', label: 'Forschung', hint: 'Art. 89 DSGVO' },
{ key: 'internal_tools', label: 'Interne Tools', hint: 'Mitarbeiter-only' },
{ key: 'public_service', label: 'Oeffentlicher Service', hint: 'Extern zugaenglich' },
],
},
// Automation Levels
automation: {
normal: [
{
value: 'assistive',
label: 'KI macht Vorschlaege',
description: 'Wie eine Rechtschreibpruefung: Sie sehen Vorschlaege und entscheiden selbst',
example: 'Beispiel: KI schlaegt Antworten vor, Mitarbeiter waehlt aus',
safe: true,
risk: false,
},
{
value: 'semi_automated',
label: 'KI filtert vor, Mensch prueft',
description: 'Wie ein Spam-Filter: KI sortiert vor, Sie schauen drueber',
example: 'Beispiel: KI ordnet Anfragen ein, Sachbearbeiter bestaetigt',
safe: false,
risk: false,
},
{
value: 'fully_automated',
label: 'KI entscheidet alleine',
description: 'Wie automatische Kreditpruefung: Keine menschliche Pruefung mehr',
example: 'Beispiel: KI lehnt Antrag ab ohne dass jemand draufschaut',
safe: false,
risk: true,
},
],
expert: [
{
value: 'assistive',
label: 'Assistierend',
description: 'KI macht Vorschlaege, Mensch entscheidet',
example: 'Human-in-the-loop garantiert',
safe: false,
risk: false,
},
{
value: 'semi_automated',
label: 'Teilautomatisiert',
description: 'KI trifft Vorentscheidungen, Mensch prueft',
example: 'Human-on-the-loop',
safe: false,
risk: false,
},
{
value: 'fully_automated',
label: 'Vollautomatisiert',
description: 'KI entscheidet ohne menschliche Pruefung',
example: 'Art. 22 DSGVO beachten!',
safe: false,
risk: true,
},
],
},
// Outputs
outputs: {
normal: [
{ key: 'recommendations_to_users', label: 'Empfehlungen an Nutzer', hint: 'z.B. Produktvorschlaege' },
{ key: 'rankings_or_scores', label: 'Punkte, Noten oder Ranglisten', hint: 'Personen werden eingestuft', risk: true },
{ key: 'legal_effects', label: 'Rechtliche Auswirkungen', hint: 'Vertraege, Kuendigungen', risk: true },
{ key: 'access_decisions', label: 'Zugang erlauben/verweigern', hint: 'Tueroeffnung, Login', risk: true },
{ key: 'content_generation', label: 'Texte oder Bilder erstellen', hint: 'Automatisch generierte Inhalte' },
{ key: 'data_export', label: 'Daten exportieren', hint: 'Berichte, Downloads' },
],
expert: [
{ key: 'recommendations_to_users', label: 'Empfehlungen an Nutzer', hint: 'Recommendation Systems' },
{ key: 'rankings_or_scores', label: 'Rankings oder Scores', hint: 'Scoring-Systeme', risk: true },
{ key: 'legal_effects', label: 'Rechtliche Auswirkungen', hint: 'Art. 22(1) DSGVO', risk: true },
{ key: 'access_decisions', label: 'Zugriffsentscheidungen', hint: 'Access Control', risk: true },
{ key: 'content_generation', label: 'Content-Generierung', hint: 'Generative AI' },
{ key: 'data_export', label: 'Datenexport', hint: 'Reporting' },
],
},
// Hosting Region
hosting: {
normal: [
{
value: 'eu',
label: 'In Deutschland / EU',
description: 'Daten bleiben in Europa - einfachste Loesung',
safe: true,
risk: false,
},
{
value: 'third_country',
label: 'Ausserhalb der EU (z.B. USA)',
description: 'Zusaetzliche Vertraege und Schutzmassnahmen noetig',
safe: false,
risk: true,
},
{
value: 'on_prem',
label: 'Auf unseren eigenen Servern',
description: 'Volle Kontrolle, aber mehr technischer Aufwand',
safe: false,
risk: false,
},
],
expert: [
{
value: 'eu',
label: 'EU/EWR',
description: 'Hosting innerhalb der EU',
safe: false,
risk: false,
},
{
value: 'third_country',
label: 'Drittland',
description: 'Hosting ausserhalb der EU (Art. 44ff DSGVO)',
safe: false,
risk: true,
},
{
value: 'on_prem',
label: 'On-Premise',
description: 'Eigene Server / Private Cloud',
safe: false,
risk: false,
},
],
},
// Model Usage - This is the key difference!
modelUsage: {
normal: [
{
id: 'search_only',
label: 'KI durchsucht meine Dokumente',
description: 'Die KI findet Informationen in Ihren Unterlagen und formuliert Antworten. Ihre Daten werden NICHT zum Training verwendet.',
example: 'Wie eine intelligente Suchmaschine fuer Ihre Dateien',
maps_to: { rag: true, finetune: false, training: false, inference: true },
safe: true,
},
{
id: 'use_only',
label: 'KI nur nutzen (ohne meine Daten)',
description: 'Sie nutzen eine fertige KI wie ChatGPT. Keine Ihrer Daten fliessen in das Training.',
example: 'Wie ein Taschenrechner: Sie geben etwas ein, bekommen Ergebnis',
maps_to: { rag: false, finetune: false, training: false, inference: true },
safe: true,
},
{
id: 'learn_from_data',
label: 'KI soll aus meinen Daten lernen',
description: 'Die KI wird mit Ihren Daten trainiert und passt ihr Verhalten an. Hoeheres Datenschutz-Risiko!',
example: 'Die KI "merkt" sich Muster aus Ihren Daten',
maps_to: { rag: false, finetune: true, training: true, inference: true },
risk: true,
},
],
expert: [
{ key: 'rag', label: 'RAG (Retrieval-Augmented Generation)', description: 'Anreicherung mit Kontextdaten aus Vektordatenbank' },
{ key: 'finetune', label: 'Fine-Tuning', description: 'Modell mit eigenen Daten anpassen (Transfer Learning)' },
{ key: 'training', label: 'Vollstaendiges Training', description: 'Modell von Grund auf trainieren' },
{ key: 'inference', label: 'Nur Inferenz', description: 'Nur Nutzung des bestehenden Modells ohne Training' },
],
},
// Retention
retention: {
normal: {
store_prompts: { label: 'Anfragen speichern', hint: 'Was Nutzer der KI schreiben' },
store_responses: { label: 'Antworten speichern', hint: 'Was die KI zurueckgibt' },
retention_days: { label: 'Wie lange aufbewahren?', hint: 'Anzahl Tage, dann automatisch loeschen' },
},
expert: {
store_prompts: { label: 'Prompts speichern', hint: 'User-Eingaben persistieren' },
store_responses: { label: 'Responses speichern', hint: 'Model-Outputs persistieren' },
retention_days: { label: 'Aufbewahrungsdauer (Tage)', hint: 'Retention Period' },
},
},
}
// ============================================================================
// Constants
// ============================================================================
const SDK_BASE_URL = process.env.NEXT_PUBLIC_SDK_URL || 'https://macmini:8093/sdk/v1'
// Default UUIDs for development/testing when localStorage is empty
const DEFAULT_TENANT_ID = 'e4ed7450-ad19-4be3-bea9-83aab6e1c21d'
const DEFAULT_USER_ID = '00000000-0000-0000-0000-000000000001'
// Helper to get tenant/user IDs with fallbacks
const getTenantId = () => {
if (typeof window === 'undefined') return DEFAULT_TENANT_ID
return localStorage.getItem('bp_tenant_id') || DEFAULT_TENANT_ID
}
const getUserId = () => {
if (typeof window === 'undefined') return DEFAULT_USER_ID
return localStorage.getItem('bp_user_id') || DEFAULT_USER_ID
}
// ============================================================================
// Initial State
// ============================================================================
const initialIntake: UseCaseIntake = {
use_case_text: '',
domain: 'general',
title: '',
data_types: {
personal_data: false,
article_9_data: false,
minor_data: false,
license_plates: false,
images: false,
audio: false,
location_data: false,
biometric_data: false,
financial_data: false,
employee_data: false,
customer_data: false,
public_data: false,
},
purpose: {
customer_support: false,
marketing: false,
analytics: false,
automation: false,
evaluation_scoring: false,
decision_making: false,
profiling: false,
research: false,
internal_tools: false,
public_service: false,
},
automation: 'assistive',
outputs: {
recommendations_to_users: false,
rankings_or_scores: false,
legal_effects: false,
access_decisions: false,
content_generation: false,
data_export: false,
},
hosting: {
provider: '',
region: 'eu',
data_residency: '',
},
model_usage: {
rag: true,
finetune: false,
training: false,
inference: true,
},
retention: {
store_prompts: false,
store_responses: false,
retention_days: 0,
anonymize_after_use: false,
},
store_raw_text: false,
}
// ============================================================================
// Main Component
// ============================================================================
export default function AdvisoryBoardPage() {
const [mode, setMode] = useState<WizardMode>('normal')
const [step, setStep] = useState(0) // 0 = overview, 1-5 = wizard steps, 6 = result
const [intake, setIntake] = useState<UseCaseIntake>(initialIntake)
const [result, setResult] = useState<AssessmentResult | null>(null)
const [assessmentId, setAssessmentId] = useState<string | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [history, setHistory] = useState<Assessment[]>([])
const [loadingHistory, setLoadingHistory] = useState(true)
const [expandedRuleCategory, setExpandedRuleCategory] = useState<string | null>(null)
const [generatingExplanation, setGeneratingExplanation] = useState(false)
const [explanation, setExplanation] = useState<string | null>(null)
const [problemSolutions, setProblemSolutions] = useState<ProblemSolution[]>([])
const [matchedProblems, setMatchedProblems] = useState<ProblemSolution[]>([])
// For Normal mode Step 5: which simplified option is selected
const [normalModelChoice, setNormalModelChoice] = useState<string>('search_only')
// Load assessment history on mount
useEffect(() => {
loadHistory()
fetchProblemSolutions()
}, [])
// Sync normalModelChoice with intake.model_usage when switching modes
useEffect(() => {
if (mode === 'normal') {
// Determine which normal option matches current state
if (intake.model_usage.finetune || intake.model_usage.training) {
setNormalModelChoice('learn_from_data')
} else if (intake.model_usage.rag) {
setNormalModelChoice('search_only')
} else {
setNormalModelChoice('use_only')
}
}
}, [mode])
const loadHistory = async () => {
try {
const res = await fetch(`${SDK_BASE_URL}/ucca/assessments`, {
headers: {
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
}
})
if (res.ok) {
const data = await res.json()
setHistory(data.assessments || [])
}
} catch (err) {
console.error('Failed to load history:', err)
} finally {
setLoadingHistory(false)
}
}
// Fetch problem-solutions catalog from API
const fetchProblemSolutions = async () => {
try {
const res = await fetch(`${SDK_BASE_URL}/ucca/problem-solutions`, {
headers: {
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
},
})
if (res.ok) {
const data = await res.json()
setProblemSolutions(data.problem_solutions || [])
}
} catch (err) {
console.error('Failed to load problem-solutions:', err)
}
}
// Match triggered rules with problems and find applicable solutions
const matchProblemsToResult = (triggeredRules: TriggeredRule[], requiredControls: RequiredControl[]) => {
if (!problemSolutions.length || !triggeredRules.length) {
setMatchedProblems([])
return
}
const triggeredRuleIds = new Set(triggeredRules.map(r => r.code))
const controlIds = new Set(requiredControls.map(c => c.id))
const matched = problemSolutions.filter(ps => {
// Check if any trigger condition matches
return ps.triggers.some(trigger => {
// Rule must be triggered
if (!triggeredRuleIds.has(trigger.rule)) {
return false
}
// If without_control is specified, that control must NOT be present
if (trigger.without_control && controlIds.has(trigger.without_control)) {
return false
}
return true
})
})
setMatchedProblems(matched)
}
const submitAssessment = async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(`${SDK_BASE_URL}/ucca/assess`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
},
body: JSON.stringify(intake),
})
if (!res.ok) {
const errData = await res.json()
throw new Error(errData.error || 'Assessment failed')
}
const data = await res.json()
setResult(data.result)
setAssessmentId(data.assessment.id)
setStep(6)
loadHistory()
// Match problems to triggered rules
matchProblemsToResult(data.result.triggered_rules, data.result.required_controls)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setLoading(false)
}
}
const generateExplanation = async () => {
if (!assessmentId) return
setGeneratingExplanation(true)
try {
const res = await fetch(`${SDK_BASE_URL}/ucca/assessments/${assessmentId}/explain`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
},
body: JSON.stringify({ language: 'de' }),
})
if (res.ok) {
const data = await res.json()
setExplanation(data.explanation_text)
}
} catch (err) {
console.error('Failed to generate explanation:', err)
} finally {
setGeneratingExplanation(false)
}
}
const exportAssessment = async (format: 'json' | 'md') => {
if (!assessmentId) return
const res = await fetch(`${SDK_BASE_URL}/ucca/export/${assessmentId}?format=${format}`, {
headers: {
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
}
})
if (res.ok) {
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ucca_assessment_${assessmentId.slice(0, 8)}.${format}`
a.click()
}
}
const startNew = () => {
setStep(1)
setIntake(initialIntake)
setResult(null)
setAssessmentId(null)
setExplanation(null)
setNormalModelChoice('search_only')
}
const loadAssessment = async (id: string) => {
try {
const res = await fetch(`${SDK_BASE_URL}/ucca/assessments/${id}`, {
headers: {
'X-Tenant-ID': getTenantId(),
'X-User-ID': getUserId(),
}
})
if (res.ok) {
const assessment = await res.json()
setResult({
feasibility: assessment.feasibility,
risk_level: assessment.risk_level,
complexity: assessment.complexity,
risk_score: assessment.risk_score,
triggered_rules: assessment.triggered_rules || [],
required_controls: assessment.required_controls || [],
recommended_architecture: assessment.recommended_architecture || [],
forbidden_patterns: assessment.forbidden_patterns || [],
example_matches: assessment.example_matches || [],
dsfa_recommended: assessment.dsfa_recommended,
art22_risk: assessment.art22_risk,
training_allowed: assessment.training_allowed,
summary: '',
recommendation: '',
})
setAssessmentId(assessment.id)
setExplanation(assessment.explanation_text || null)
setStep(6)
}
} catch (err) {
console.error('Failed to load assessment:', err)
}
}
// Handle Normal mode model choice change
const handleNormalModelChoice = (choiceId: string) => {
setNormalModelChoice(choiceId)
const choice = LABELS.modelUsage.normal.find(c => c.id === choiceId)
if (choice) {
setIntake({
...intake,
model_usage: choice.maps_to
})
}
}
// ============================================================================
// Render Functions
// ============================================================================
const renderModeToggle = () => (
<div className="flex items-center justify-center gap-4 p-3 bg-slate-100 rounded-lg mb-6">
<span className="text-sm text-slate-600">Ansicht:</span>
<button
onClick={() => setMode('normal')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
mode === 'normal'
? 'bg-white text-primary-700 shadow-sm'
: 'text-slate-600 hover:text-slate-800'
}`}
>
Einfach
<span className="ml-1 text-xs text-slate-400">(empfohlen)</span>
</button>
<button
onClick={() => setMode('expert')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
mode === 'expert'
? 'bg-white text-primary-700 shadow-sm'
: 'text-slate-600 hover:text-slate-800'
}`}
>
Experten-Modus
<span className="ml-1 text-xs text-slate-400">(Fachbegriffe)</span>
</button>
</div>
)
const renderOverview = () => (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-800">Advisory Board</h2>
<div className="flex items-center gap-3">
<Link
href="/dsgvo/advisory-board/documentation"
className="px-4 py-2 text-slate-600 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors flex items-center gap-2"
>
<span>📋</span>
Dokumentation
</Link>
<button
onClick={startNew}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
>
Neues Assessment starten
</button>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-slate-800">{history.length}</div>
<div className="text-sm text-slate-500">Assessments gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-green-600">
{history.filter(a => a.feasibility === 'YES').length}
</div>
<div className="text-sm text-slate-500">Zulaessig</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-yellow-600">
{history.filter(a => a.feasibility === 'CONDITIONAL').length}
</div>
<div className="text-sm text-slate-500">Mit Auflagen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-red-600">
{history.filter(a => a.feasibility === 'NO').length}
</div>
<div className="text-sm text-slate-500">Nicht zulaessig</div>
</div>
</div>
{/* History */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200">
<h3 className="font-medium text-slate-800">Bisherige Assessments</h3>
</div>
{loadingHistory ? (
<div className="p-8 text-center text-slate-500">Lade...</div>
) : history.length === 0 ? (
<div className="p-8 text-center text-slate-500">
Noch keine Assessments vorhanden. Starten Sie Ihr erstes Assessment!
</div>
) : (
<div className="divide-y divide-slate-100">
{history.map((assessment) => (
<div
key={assessment.id}
className="px-4 py-3 flex items-center justify-between hover:bg-slate-50 cursor-pointer"
onClick={() => loadAssessment(assessment.id)}
>
<div>
<div className="font-medium text-slate-800">{assessment.title}</div>
<div className="text-sm text-slate-500">
{new Date(assessment.created_at).toLocaleDateString('de-DE')} - {assessment.domain}
</div>
</div>
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${
assessment.feasibility === 'YES' ? 'bg-green-100 text-green-700' :
assessment.feasibility === 'CONDITIONAL' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{assessment.feasibility === 'YES' ? 'Zulaessig' :
assessment.feasibility === 'CONDITIONAL' ? 'Mit Auflagen' : 'Nicht zulaessig'}
</span>
<span className="text-sm text-slate-500">
Score: {assessment.risk_score}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
const renderStep1 = () => {
const domains = LABELS.domains[mode]
const domainSuggestions = suggestDomainsFromText(intake.use_case_text)
const currentDomain = DOMAINS.find(d => d.value === intake.domain)
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">
Schritt 1: {mode === 'normal' ? 'Was moechten Sie mit KI machen?' : 'Use Case beschreiben'}
</h3>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{mode === 'normal'
? 'Beschreiben Sie in eigenen Worten, was die KI fuer Sie tun soll'
: 'Beschreiben Sie Ihren geplanten KI-Use-Case'}
</label>
<textarea
value={intake.use_case_text}
onChange={(e) => setIntake({ ...intake, use_case_text: e.target.value })}
rows={6}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder={mode === 'normal'
? 'z.B. Ich moechte einen Chatbot, der unseren Kunden bei Fragen zu unseren Produkten hilft...'
: 'z.B. Wir moechten einen Chatbot einsetzen, der Kunden bei der Tarifauswahl unterstuetzt...'}
/>
</div>
{/* Auto-suggestions based on text */}
{domainSuggestions.length > 0 && (
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<div className="text-sm font-medium text-blue-800 mb-2">
💡 {mode === 'normal' ? 'Vorschlag basierend auf Ihrer Beschreibung:' : 'Erkannte Branche(n):'}
</div>
<div className="flex flex-wrap gap-2">
{domainSuggestions.map(suggestion => (
<button
key={suggestion.value}
onClick={() => setIntake({ ...intake, domain: suggestion.value })}
className={`px-3 py-1.5 rounded-lg text-sm transition-colors ${
intake.domain === suggestion.value
? 'bg-blue-600 text-white'
: 'bg-white text-blue-700 border border-blue-300 hover:bg-blue-100'
}`}
>
{suggestion.label}
{intake.domain === suggestion.value && ' ✓'}
</button>
))}
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{mode === 'normal' ? 'In welchem Bereich wird die KI eingesetzt?' : 'Domain / Branche'}
{currentDomain && (
<span className="ml-2 text-xs text-slate-500">
(Kategorie: {currentDomain.category})
</span>
)}
</label>
<select
value={intake.domain}
onChange={(e) => setIntake({ ...intake, domain: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
{DOMAIN_CATEGORIES.map(category => (
<optgroup key={category} label={`── ${category} ──`}>
{domains.filter(d => d.category === category).map(d => (
<option key={d.value} value={d.value}>{d.label}</option>
))}
</optgroup>
))}
</select>
<p className="mt-1 text-xs text-slate-500">
{mode === 'normal'
? `${DOMAINS.length} Branchen verfuegbar - tippen Sie oben und wir schlagen passende vor`
: `${DOMAINS.length} Domains mit branchenspezifischen Regeln`}
</p>
</div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="store_raw_text"
checked={intake.store_raw_text}
onChange={(e) => setIntake({ ...intake, store_raw_text: e.target.checked })}
className="h-4 w-4 text-primary-600 rounded"
/>
<label htmlFor="store_raw_text" className="text-sm text-slate-600">
{mode === 'normal'
? 'Meine Beschreibung fuer spaeter speichern'
: 'Beschreibungstext speichern (sonst nur Hash fuer Deduplizierung)'}
</label>
</div>
</div>
)
}
const renderStep2 = () => {
const dataTypeLabels = LABELS.dataTypes[mode]
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">
Schritt 2: {mode === 'normal' ? 'Welche Daten werden verwendet?' : 'Datentypen'}
</h3>
<p className="text-sm text-slate-600">
{mode === 'normal'
? 'Waehlen Sie alle Arten von Daten aus, die die KI verarbeiten wird'
: 'Welche Daten werden verarbeitet?'}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{dataTypeLabels.map(({ key, label, hint, risk, safe }) => (
<label
key={key}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
intake.data_types[key as keyof DataTypes]
? risk ? 'bg-red-50 border-2 border-red-300' : safe ? 'bg-green-50 border-2 border-green-300' : 'bg-primary-50 border-2 border-primary-300'
: 'bg-slate-50 hover:bg-slate-100 border-2 border-transparent'
}`}
>
<input
type="checkbox"
checked={intake.data_types[key as keyof DataTypes]}
onChange={(e) => setIntake({
...intake,
data_types: { ...intake.data_types, [key]: e.target.checked }
})}
className="mt-0.5 h-4 w-4 text-primary-600 rounded"
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-700">{label}</span>
{risk && <span className="text-xs text-red-600 font-medium">Hohes Risiko</span>}
{safe && <span className="text-xs text-green-600 font-medium">Geringes Risiko</span>}
</div>
{hint && <div className="text-xs text-slate-500 mt-0.5">{hint}</div>}
</div>
</label>
))}
</div>
</div>
)
}
const renderStep3 = () => {
const purposeLabels = LABELS.purpose[mode]
const automationLabels = LABELS.automation[mode]
const outputLabels = LABELS.outputs[mode]
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">
Schritt 3: {mode === 'normal' ? 'Wofuer wird die KI verwendet?' : 'Zweck & Automatisierung'}
</h3>
<div>
<p className="text-sm text-slate-600 mb-3">
{mode === 'normal' ? 'Was soll die KI fuer Sie erledigen?' : 'Verarbeitungszweck'}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{purposeLabels.map(({ key, label, hint, risk }) => (
<label
key={key}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
intake.purpose[key as keyof Purpose]
? risk ? 'bg-red-50 border-2 border-red-300' : 'bg-primary-50 border-2 border-primary-300'
: 'bg-slate-50 hover:bg-slate-100 border-2 border-transparent'
}`}
>
<input
type="checkbox"
checked={intake.purpose[key as keyof Purpose]}
onChange={(e) => setIntake({
...intake,
purpose: { ...intake.purpose, [key]: e.target.checked }
})}
className="mt-0.5 h-4 w-4 text-primary-600 rounded"
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-700">{label}</span>
{risk && <span className="text-xs text-red-600 font-medium">Pruefung noetig</span>}
</div>
{hint && <div className="text-xs text-slate-500 mt-0.5">{hint}</div>}
</div>
</label>
))}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-3">
{mode === 'normal' ? 'Wie selbststaendig soll die KI arbeiten?' : 'Automatisierungsgrad'}
</p>
<div className="space-y-2">
{automationLabels.map(({ value, label, description, example, risk, safe }) => (
<label
key={value}
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
intake.automation === value
? risk ? 'border-red-400 bg-red-50' : safe ? 'border-green-400 bg-green-50' : 'border-primary-500 bg-primary-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<input
type="radio"
name="automation"
value={value}
checked={intake.automation === value}
onChange={(e) => setIntake({ ...intake, automation: e.target.value })}
className="mt-1 h-4 w-4 text-primary-600"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-800">{label}</span>
{risk && <span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded">Hohes Risiko</span>}
{safe && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Empfohlen</span>}
</div>
<div className="text-sm text-slate-600 mt-1">{description}</div>
{example && <div className="text-xs text-slate-500 mt-1 italic">{example}</div>}
</div>
</label>
))}
</div>
</div>
<div>
<p className="text-sm text-slate-600 mb-3">
{mode === 'normal' ? 'Was gibt die KI aus?' : 'Output-Charakteristik'}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{outputLabels.map(({ key, label, hint, risk }) => (
<label
key={key}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
intake.outputs[key as keyof Outputs]
? risk ? 'bg-red-50 border-2 border-red-300' : 'bg-primary-50 border-2 border-primary-300'
: 'bg-slate-50 hover:bg-slate-100 border-2 border-transparent'
}`}
>
<input
type="checkbox"
checked={intake.outputs[key as keyof Outputs]}
onChange={(e) => setIntake({
...intake,
outputs: { ...intake.outputs, [key]: e.target.checked }
})}
className="mt-0.5 h-4 w-4 text-primary-600 rounded"
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-700">{label}</span>
{risk && <span className="text-xs text-red-600 font-medium">Pruefung noetig</span>}
</div>
{hint && <div className="text-xs text-slate-500 mt-0.5">{hint}</div>}
</div>
</label>
))}
</div>
</div>
</div>
)
}
const renderStep4 = () => {
const hostingLabels = LABELS.hosting[mode]
const retentionLabels = LABELS.retention[mode]
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">
Schritt 4: {mode === 'normal' ? 'Wo laeuft die KI?' : 'Hosting & Speicherung'}
</h3>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{mode === 'normal' ? 'Welchen Anbieter nutzen Sie? (optional)' : 'Provider (optional)'}
</label>
<input
type="text"
value={intake.hosting.provider}
onChange={(e) => setIntake({
...intake,
hosting: { ...intake.hosting, provider: e.target.value }
})}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
placeholder={mode === 'normal' ? 'z.B. Microsoft, Google, Amazon...' : 'z.B. Azure, AWS, Hetzner...'}
/>
</div>
<div>
<p className="text-sm text-slate-600 mb-3">
{mode === 'normal' ? 'Wo werden die Daten gespeichert?' : 'Region'}
</p>
<div className="space-y-2">
{hostingLabels.map(({ value, label, description, risk, safe }) => (
<label
key={value}
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
intake.hosting.region === value
? risk ? 'border-red-400 bg-red-50' : safe ? 'border-green-400 bg-green-50' : 'border-primary-500 bg-primary-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<input
type="radio"
name="region"
value={value}
checked={intake.hosting.region === value}
onChange={(e) => setIntake({
...intake,
hosting: { ...intake.hosting, region: e.target.value }
})}
className="mt-1 h-4 w-4 text-primary-600"
/>
<div>
<div className="flex items-center gap-2">
<span className="font-medium text-slate-800">{label}</span>
{risk && <span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded">Zusaetzliche Pruefung</span>}
{safe && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Empfohlen</span>}
</div>
<div className="text-sm text-slate-500 mt-1">{description}</div>
</div>
</label>
))}
</div>
</div>
<div className="space-y-3">
<p className="text-sm text-slate-600">
{mode === 'normal' ? 'Sollen Gespraeche mit der KI gespeichert werden?' : 'Datenspeicherung'}
</p>
<label className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg cursor-pointer hover:bg-slate-100">
<input
type="checkbox"
checked={intake.retention.store_prompts}
onChange={(e) => setIntake({
...intake,
retention: { ...intake.retention, store_prompts: e.target.checked }
})}
className="h-4 w-4 text-primary-600 rounded"
/>
<div>
<span className="text-sm text-slate-700">{retentionLabels.store_prompts.label}</span>
<span className="text-xs text-slate-500 ml-2">({retentionLabels.store_prompts.hint})</span>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg cursor-pointer hover:bg-slate-100">
<input
type="checkbox"
checked={intake.retention.store_responses}
onChange={(e) => setIntake({
...intake,
retention: { ...intake.retention, store_responses: e.target.checked }
})}
className="h-4 w-4 text-primary-600 rounded"
/>
<div>
<span className="text-sm text-slate-700">{retentionLabels.store_responses.label}</span>
<span className="text-xs text-slate-500 ml-2">({retentionLabels.store_responses.hint})</span>
</div>
</label>
{(intake.retention.store_prompts || intake.retention.store_responses) && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{retentionLabels.retention_days.label}
</label>
<div className="flex items-center gap-2">
<input
type="number"
value={intake.retention.retention_days || ''}
onChange={(e) => setIntake({
...intake,
retention: { ...intake.retention, retention_days: parseInt(e.target.value) || 0 }
})}
className="w-32 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
min="0"
placeholder="0"
/>
<span className="text-sm text-slate-500">Tage (0 = unbegrenzt)</span>
</div>
</div>
)}
</div>
</div>
)
}
const renderStep5 = () => {
if (mode === 'normal') {
// Simplified Normal Mode with 3 clear options
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">
Schritt 5: Wie nutzt die KI Ihre Daten?
</h3>
<p className="text-sm text-slate-600">
Waehlen Sie die Option, die am besten zu Ihrem Vorhaben passt
</p>
<div className="space-y-3">
{LABELS.modelUsage.normal.map(({ id, label, description, example, risk, safe }) => (
<label
key={id}
className={`flex items-start gap-3 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
normalModelChoice === id
? risk ? 'border-red-400 bg-red-50' : safe ? 'border-green-400 bg-green-50' : 'border-primary-500 bg-primary-50'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<input
type="radio"
name="model_usage_normal"
value={id}
checked={normalModelChoice === id}
onChange={() => handleNormalModelChoice(id)}
className="mt-1 h-4 w-4 text-primary-600"
/>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium text-slate-800">{label}</span>
{risk && <span className="text-xs bg-red-100 text-red-700 px-2 py-0.5 rounded">Hoeheres Risiko</span>}
{safe && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Empfohlen</span>}
</div>
<div className="text-sm text-slate-600 mt-1">{description}</div>
<div className="text-xs text-slate-500 mt-2 italic">{example}</div>
</div>
</label>
))}
</div>
{/* Info box for the "learn from data" option */}
{normalModelChoice === 'learn_from_data' && (
<div className="p-4 bg-yellow-50 border border-yellow-300 rounded-lg">
<div className="flex items-start gap-2">
<span className="text-yellow-600 text-lg"></span>
<div>
<div className="font-medium text-yellow-800">Hinweis zum KI-Training</div>
<div className="text-sm text-yellow-700 mt-1">
Wenn die KI aus Ihren Daten lernt, werden diese Daten Teil des Modells.
Das erfordert besondere Datenschutz-Massnahmen und ist bei personenbezogenen
Daten oft problematisch.
</div>
</div>
</div>
</div>
)}
</div>
)
}
// Expert Mode with all checkboxes
return (
<div className="space-y-6">
<h3 className="text-lg font-medium text-slate-800">Schritt 5: Modell-Nutzung</h3>
<p className="text-sm text-slate-600">Wie wird das KI-Modell eingesetzt?</p>
<div className="space-y-3">
{LABELS.modelUsage.expert.map(({ key, label, description }) => (
<label
key={key}
className={`flex items-start gap-3 p-4 rounded-lg cursor-pointer transition-colors ${
intake.model_usage[key as keyof ModelUsage]
? 'bg-primary-50 border-2 border-primary-300'
: 'bg-slate-50 hover:bg-slate-100 border-2 border-transparent'
}`}
>
<input
type="checkbox"
checked={intake.model_usage[key as keyof ModelUsage]}
onChange={(e) => setIntake({
...intake,
model_usage: { ...intake.model_usage, [key]: e.target.checked }
})}
className="mt-1 h-4 w-4 text-primary-600 rounded"
/>
<div>
<div className="font-medium text-slate-700">{label}</div>
<div className="text-sm text-slate-500">{description}</div>
</div>
</label>
))}
</div>
</div>
)
}
const renderResult = () => {
if (!result) return null
const severityColor = (severity: string) => {
switch (severity) {
case 'BLOCK': return 'text-red-600 bg-red-50 border-red-200'
case 'WARN': return 'text-yellow-700 bg-yellow-50 border-yellow-200'
default: return 'text-blue-600 bg-blue-50 border-blue-200'
}
}
const feasibilityBadge = () => {
switch (result.feasibility) {
case 'YES': return 'bg-green-500'
case 'CONDITIONAL': return 'bg-yellow-500'
case 'NO': return 'bg-red-500'
}
}
const feasibilityText = () => {
if (mode === 'normal') {
switch (result.feasibility) {
case 'YES': return { main: 'Zulaessig', sub: 'Sie koennen loslegen!' }
case 'CONDITIONAL': return { main: 'Mit Auflagen moeglich', sub: 'Einige Massnahmen sind erforderlich' }
case 'NO': return { main: 'So nicht moeglich', sub: 'Aenderungen am Konzept noetig' }
}
}
switch (result.feasibility) {
case 'YES': return { main: 'YES', sub: 'Use Case ist zulaessig' }
case 'CONDITIONAL': return { main: 'CONDITIONAL', sub: 'Unter Auflagen umsetzbar' }
case 'NO': return { main: 'NO', sub: 'Nicht DSGVO-konform' }
}
}
// Group rules by category
const rulesByCategory = result.triggered_rules.reduce((acc, rule) => {
if (!acc[rule.category]) acc[rule.category] = []
acc[rule.category].push(rule)
return acc
}, {} as Record<string, TriggeredRule[]>)
const verdictText = feasibilityText()
return (
<div className="space-y-6">
{/* Header with main verdict */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-800">
{mode === 'normal' ? 'Ihr Ergebnis' : 'Assessment Ergebnis'}
</h2>
<div className="flex gap-2">
<button
onClick={() => exportAssessment('json')}
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-50"
>
JSON Export
</button>
<button
onClick={() => exportAssessment('md')}
className="px-3 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-50"
>
Markdown Export
</button>
<button
onClick={startNew}
className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700"
>
Neues Assessment
</button>
</div>
</div>
{/* Main Verdict Badge */}
<div className={`flex items-center justify-center py-8 rounded-xl ${feasibilityBadge()}`}>
<div className="text-center text-white">
<div className="text-4xl font-bold">{verdictText.main}</div>
<div className="text-lg opacity-90 mt-1">{verdictText.sub}</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-slate-800">{result.risk_score}/100</div>
<div className="text-sm text-slate-500">Risiko-Score</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-slate-800">
{mode === 'normal'
? (result.risk_level === 'MINIMAL' ? 'Sehr gering' :
result.risk_level === 'LOW' ? 'Gering' :
result.risk_level === 'MEDIUM' ? 'Mittel' :
result.risk_level === 'HIGH' ? 'Hoch' : 'Sehr hoch')
: result.risk_level}
</div>
<div className="text-sm text-slate-500">Risikostufe</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-slate-800">
{mode === 'normal'
? (result.complexity === 'LOW' ? 'Einfach' :
result.complexity === 'MEDIUM' ? 'Mittel' : 'Komplex')
: result.complexity}
</div>
<div className="text-sm text-slate-500">
{mode === 'normal' ? 'Aufwand' : 'Komplexitaet'}
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-2xl font-bold text-slate-800">{result.triggered_rules.length}</div>
<div className="text-sm text-slate-500">
{mode === 'normal' ? 'Hinweise' : 'Ausgeloeste Regeln'}
</div>
</div>
</div>
{/* Flags */}
<div className="flex flex-wrap gap-3">
{result.dsfa_recommended && (
<div className="px-3 py-2 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded-lg text-sm">
{mode === 'normal' ? 'Datenschutz-Pruefung empfohlen' : 'DSFA empfohlen'}
</div>
)}
{result.art22_risk && (
<div className="px-3 py-2 bg-red-100 border border-red-300 text-red-800 rounded-lg text-sm">
{mode === 'normal' ? 'Achtung: Automatische Entscheidungen!' : 'Art. 22 DSGVO Risiko'}
</div>
)}
<div className={`px-3 py-2 rounded-lg text-sm ${
result.training_allowed === 'YES' ? 'bg-green-100 border border-green-300 text-green-800' :
result.training_allowed === 'CONDITIONAL' ? 'bg-yellow-100 border border-yellow-300 text-yellow-800' :
'bg-red-100 border border-red-300 text-red-800'
}`}>
{mode === 'normal'
? `KI-Training: ${result.training_allowed === 'YES' ? 'Moeglich' : result.training_allowed === 'CONDITIONAL' ? 'Eingeschraenkt' : 'Nicht moeglich'}`
: `Training: ${result.training_allowed}`}
</div>
</div>
{/* Triggered Rules by Category */}
{Object.keys(rulesByCategory).length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200">
<h3 className="font-medium text-slate-800">
{mode === 'normal' ? 'Details zur Bewertung' : 'Ausgeloeste Regeln'}
</h3>
</div>
<div className="divide-y divide-slate-100">
{Object.entries(rulesByCategory).map(([category, rules]) => (
<div key={category}>
<button
onClick={() => setExpandedRuleCategory(expandedRuleCategory === category ? null : category)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
>
<div className="flex items-center gap-3">
<span className="font-medium text-slate-700">{category}</span>
<span className="text-sm text-slate-500">({rules.length} {mode === 'normal' ? 'Punkte' : 'Regeln'})</span>
</div>
<span className="text-slate-400">
{expandedRuleCategory === category ? '-' : '+'}
</span>
</button>
{expandedRuleCategory === category && (
<div className="px-4 pb-3 space-y-2">
{rules.map(rule => (
<div key={rule.code} className={`p-3 rounded-lg border ${severityColor(rule.severity)}`}>
<div className="flex items-center justify-between">
<div className="font-medium">{mode === 'expert' ? `${rule.code}: ` : ''}{rule.title}</div>
<span className="text-xs px-2 py-0.5 rounded bg-white/50">
{rule.severity === 'BLOCK' ? 'Kritisch' : rule.severity === 'WARN' ? 'Warnung' : 'Info'} {mode === 'expert' && `| +${rule.score_delta}`}
</span>
</div>
<div className="text-sm mt-1 opacity-80">{rule.rationale}</div>
{mode === 'expert' && rule.gdpr_ref && (
<div className="text-xs mt-1 opacity-60">{rule.gdpr_ref}</div>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Required Controls */}
{result.required_controls.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200">
<h3 className="font-medium text-slate-800">
{mode === 'normal' ? 'Das muessen Sie tun' : 'Erforderliche Kontrollen'}
</h3>
</div>
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-3">
{result.required_controls.map(control => (
<div key={control.id} className="p-3 bg-slate-50 rounded-lg">
<div className="font-medium text-slate-800">{control.title}</div>
<div className="text-sm text-slate-600 mt-1">{control.description}</div>
{mode === 'expert' && control.gdpr_ref && (
<div className="text-xs text-slate-500 mt-1">{control.gdpr_ref}</div>
)}
</div>
))}
</div>
</div>
)}
{/* Problems + Solutions - Key new feature */}
{matchedProblems.length > 0 && (
<div className="bg-white rounded-xl border border-amber-200 shadow-sm">
<div className="px-4 py-3 border-b border-amber-200 bg-amber-50">
<h3 className="font-medium text-amber-800 flex items-center gap-2">
<span></span>
{mode === 'normal' ? 'Probleme und Loesungsvorschlaege' : 'Identifizierte Probleme mit Loesungen'}
</h3>
<p className="text-sm text-amber-700 mt-1">
{mode === 'normal'
? 'Fuer diese Punkte gibt es konkrete Loesungsmoeglichkeiten. Besprechen Sie die Fragen mit Ihrem Team.'
: 'Folgende Probleme wurden erkannt. Jedes Problem hat mindestens eine Loesungsoption.'}
</p>
</div>
<div className="divide-y divide-amber-100">
{matchedProblems.map(problem => (
<div key={problem.problem_id} className="p-4">
<div className="font-medium text-amber-900 mb-3">{problem.title}</div>
<div className="space-y-3">
{problem.solutions.map(solution => (
<div key={solution.id} className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-green-100 text-green-700 rounded-full flex items-center justify-center text-sm">
💡
</div>
<div className="flex-1">
<div className="font-medium text-green-800">{solution.title}</div>
{solution.pattern && (
<div className="text-sm text-green-700 mt-1">
{mode === 'normal' ? 'Technische Loesung:' : 'Pattern:'} {solution.pattern}
</div>
)}
{solution.control && (
<div className="text-sm text-green-700 mt-1">
{mode === 'normal' ? 'Erforderliche Massnahme:' : 'Control:'} {solution.control}
</div>
)}
<div className="mt-3 p-3 bg-white border border-green-300 rounded-lg">
<div className="text-sm font-medium text-slate-700 mb-1">
{mode === 'normal' ? 'Frage an Ihr Team:' : 'Team-Entscheidung:'}
</div>
<div className="text-slate-800">
{solution.team_question}
</div>
</div>
{solution.removes_problem && (
<div className="text-xs text-green-600 mt-2">
{mode === 'normal' ? 'Diese Loesung behebt das Problem vollstaendig' : 'Loest das Problem bei Umsetzung'}
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
)}
{/* Recommended Architecture */}
{result.recommended_architecture.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200">
<h3 className="font-medium text-slate-800">
{mode === 'normal' ? 'Empfohlene Loesungen' : 'Empfohlene Architektur-Patterns'}
</h3>
</div>
<div className="p-4 space-y-3">
{result.recommended_architecture.map(pattern => (
<div key={pattern.pattern_id} className="p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="font-medium text-green-800">{pattern.title}</div>
<div className="text-sm text-green-700 mt-1">{pattern.description}</div>
</div>
))}
</div>
</div>
)}
{/* Forbidden Patterns */}
{result.forbidden_patterns.length > 0 && (
<div className="bg-white rounded-xl border border-red-200 shadow-sm">
<div className="px-4 py-3 border-b border-red-200 bg-red-50">
<h3 className="font-medium text-red-800">
{mode === 'normal' ? 'Das duerfen Sie nicht tun' : 'Verbotene Muster'}
</h3>
</div>
<div className="p-4 space-y-3">
{result.forbidden_patterns.map(pattern => (
<div key={pattern.pattern_id} className="p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="font-medium text-red-800">{pattern.title}</div>
<div className="text-sm text-red-700 mt-1">{pattern.reason}</div>
{mode === 'expert' && pattern.gdpr_ref && (
<div className="text-xs text-red-600 mt-1">{pattern.gdpr_ref}</div>
)}
</div>
))}
</div>
</div>
)}
{/* Example Matches */}
{result.example_matches.length > 0 && (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200">
<h3 className="font-medium text-slate-800">
{mode === 'normal' ? 'Aehnliche Faelle' : 'Aehnliche Beispiele'}
</h3>
</div>
<div className="p-4 space-y-3">
{result.example_matches.map(example => (
<div key={example.example_id} className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-center justify-between">
<div className="font-medium text-blue-800">{example.title}</div>
<span className="text-xs text-blue-600">
{Math.round(example.similarity * 100)}% {mode === 'normal' ? 'aehnlich' : 'Aehnlichkeit'}
</span>
</div>
<div className="text-sm text-blue-700 mt-1">{example.description}</div>
<div className="text-sm text-blue-800 mt-2 font-medium">
{mode === 'normal' ? 'Bewertung' : 'Ergebnis'}: {example.outcome}
</div>
<div className="text-sm text-blue-600 mt-1">{example.lessons}</div>
</div>
))}
</div>
</div>
)}
{/* LLM Explanation */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-medium text-slate-800">
{mode === 'normal' ? 'Ausfuehrliche Erklaerung' : 'KI-Erklaerung'}
</h3>
{!explanation && (
<button
onClick={generateExplanation}
disabled={generatingExplanation}
className="px-3 py-1.5 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{generatingExplanation ? 'Generiere...' : mode === 'normal' ? 'Erklaerung anfordern' : 'Erklaerung generieren'}
</button>
)}
</div>
<div className="p-4">
{explanation ? (
<div className="prose prose-sm max-w-none text-slate-700 whitespace-pre-wrap">
{explanation}
</div>
) : (
<p className="text-slate-500 text-sm">
{mode === 'normal'
? 'Klicken Sie auf "Erklaerung anfordern", um eine verstaendliche Zusammenfassung zu erhalten.'
: 'Klicken Sie auf "Erklaerung generieren", um eine detaillierte Erklaerung per KI zu erhalten.'}
</p>
)}
</div>
</div>
</div>
)
}
const renderWizardNavigation = () => (
<div className="flex items-center justify-between pt-6 border-t border-slate-200">
<button
onClick={() => setStep(step - 1)}
className="px-4 py-2 text-slate-600 hover:text-slate-800"
disabled={step <= 1}
>
Zurueck
</button>
<div className="flex items-center gap-2">
{[1, 2, 3, 4, 5].map(s => (
<div
key={s}
className={`w-2 h-2 rounded-full ${s === step ? 'bg-primary-600' : s < step ? 'bg-primary-300' : 'bg-slate-300'}`}
/>
))}
</div>
{step < 5 ? (
<button
onClick={() => setStep(step + 1)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
>
Weiter
</button>
) : (
<button
onClick={submitAssessment}
disabled={loading}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{loading ? 'Bewerte...' : mode === 'normal' ? 'Jetzt pruefen' : 'Assessment starten'}
</button>
)}
</div>
)
// ============================================================================
// Main Render
// ============================================================================
return (
<div className="space-y-6">
<PagePurpose
title="Advisory Board"
purpose={mode === 'normal'
? "Pruefen Sie einfach und schnell, ob Ihr KI-Projekt datenschutzkonform ist. Beantworten Sie einige Fragen und erhalten Sie eine klare Einschaetzung."
: "Bewerten Sie geplante KI-Use-Cases auf DSGVO-Konformitaet. Die deterministische Rule Engine analysiert Machbarkeit, Risiko und Komplexitaet und gibt konkrete Architektur-Empfehlungen."}
audience={mode === 'normal'
? ['Alle Mitarbeiter', 'Fachabteilungen', 'Projektleiter']
: ['Datenschutzbeauftragter', 'Projektleiter', 'Entwickler']}
gdprArticles={['Art. 5', 'Art. 6', 'Art. 9', 'Art. 22', 'Art. 35']}
collapsible={true}
defaultCollapsed={true}
/>
{error && (
<div className="p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
{error}
</div>
)}
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
{step >= 1 && step <= 5 && renderModeToggle()}
{step === 0 && renderOverview()}
{step === 1 && renderStep1()}
{step === 2 && renderStep2()}
{step === 3 && renderStep3()}
{step === 4 && renderStep4()}
{step === 5 && renderStep5()}
{step === 6 && renderResult()}
{step >= 1 && step <= 5 && renderWizardNavigation()}
</div>
</div>
)
}