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>
1955 lines
83 KiB
TypeScript
1955 lines
83 KiB
TypeScript
'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>
|
||
)
|
||
}
|