'use client' import React, { useState, useEffect, useCallback } from 'react' // ============================================================================= // TYPES // ============================================================================= interface DecisionTreeQuestion { id: string axis: 'high_risk' | 'gpai' question: string description: string article_ref: string skip_if?: string } interface DecisionTreeDefinition { id: string name: string version: string questions: DecisionTreeQuestion[] } interface DecisionTreeAnswer { question_id: string value: boolean note?: string } interface GPAIClassification { is_gpai: boolean is_systemic_risk: boolean gpai_category: 'none' | 'standard' | 'systemic' applicable_articles: string[] obligations: string[] } interface DecisionTreeResult { id: string tenant_id: string system_name: string system_description?: string answers: Record high_risk_result: string gpai_result: GPAIClassification combined_obligations: string[] applicable_articles: string[] created_at: string } // ============================================================================= // CONSTANTS // ============================================================================= const RISK_LEVEL_CONFIG: Record = { unacceptable: { label: 'Unzulässig', color: 'text-red-700', bg: 'bg-red-50', border: 'border-red-200' }, high_risk: { label: 'Hochrisiko', color: 'text-orange-700', bg: 'bg-orange-50', border: 'border-orange-200' }, limited_risk: { label: 'Begrenztes Risiko', color: 'text-yellow-700', bg: 'bg-yellow-50', border: 'border-yellow-200' }, minimal_risk: { label: 'Minimales Risiko', color: 'text-green-700', bg: 'bg-green-50', border: 'border-green-200' }, not_applicable: { label: 'Nicht anwendbar', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' }, } const GPAI_CONFIG: Record = { none: { label: 'Kein GPAI', color: 'text-gray-500', bg: 'bg-gray-50', border: 'border-gray-200' }, standard: { label: 'GPAI Standard', color: 'text-blue-700', bg: 'bg-blue-50', border: 'border-blue-200' }, systemic: { label: 'GPAI Systemisches Risiko', color: 'text-purple-700', bg: 'bg-purple-50', border: 'border-purple-200' }, } // ============================================================================= // MAIN COMPONENT // ============================================================================= export default function DecisionTreeWizard() { const [definition, setDefinition] = useState(null) const [answers, setAnswers] = useState>({}) const [currentIdx, setCurrentIdx] = useState(0) const [systemName, setSystemName] = useState('') const [systemDescription, setSystemDescription] = useState('') const [result, setResult] = useState(null) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [phase, setPhase] = useState<'intro' | 'questions' | 'result'>('intro') // Load decision tree definition useEffect(() => { const load = async () => { try { const res = await fetch('/api/sdk/v1/ucca/decision-tree') if (res.ok) { const data = await res.json() setDefinition(data) } else { setError('Entscheidungsbaum konnte nicht geladen werden') } } catch { setError('Verbindung zum Backend fehlgeschlagen') } finally { setLoading(false) } } load() }, []) // Get visible questions (respecting skip logic) const getVisibleQuestions = useCallback((): DecisionTreeQuestion[] => { if (!definition) return [] return definition.questions.filter(q => { if (!q.skip_if) return true // Skip this question if the gate question was answered "no" const gateAnswer = answers[q.skip_if] if (gateAnswer && !gateAnswer.value) return false return true }) }, [definition, answers]) const visibleQuestions = getVisibleQuestions() const currentQuestion = visibleQuestions[currentIdx] const totalVisible = visibleQuestions.length const highRiskQuestions = visibleQuestions.filter(q => q.axis === 'high_risk') const gpaiQuestions = visibleQuestions.filter(q => q.axis === 'gpai') const handleAnswer = (value: boolean) => { if (!currentQuestion) return setAnswers(prev => ({ ...prev, [currentQuestion.id]: { question_id: currentQuestion.id, value, }, })) // Auto-advance if (currentIdx < totalVisible - 1) { setCurrentIdx(prev => prev + 1) } } const handleBack = () => { if (currentIdx > 0) { setCurrentIdx(prev => prev - 1) } } const handleSubmit = async () => { setSaving(true) setError(null) try { const res = await fetch('/api/sdk/v1/ucca/decision-tree/evaluate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ system_name: systemName, system_description: systemDescription, answers, }), }) if (res.ok) { const data = await res.json() setResult(data) setPhase('result') } else { const err = await res.json().catch(() => ({ error: 'Auswertung fehlgeschlagen' })) setError(err.error || 'Auswertung fehlgeschlagen') } } catch { setError('Verbindung zum Backend fehlgeschlagen') } finally { setSaving(false) } } const handleReset = () => { setAnswers({}) setCurrentIdx(0) setSystemName('') setSystemDescription('') setResult(null) setPhase('intro') setError(null) } const allAnswered = visibleQuestions.every(q => answers[q.id] !== undefined) if (loading) { return (

Entscheidungsbaum wird geladen...

) } if (error && !definition) { return (

{error}

Bitte stellen Sie sicher, dass der AI Compliance SDK Service läuft.

) } // ========================================================================= // INTRO PHASE // ========================================================================= if (phase === 'intro') { return (

AI Act Entscheidungsbaum

Klassifizieren Sie Ihr KI-System anhand von 12 Fragen auf zwei Achsen: High-Risk (Anhang III) und GPAI (Art. 51–56).

Achse 1: High-Risk

7 Fragen zu Anhang III Kategorien (Biometrie, kritische Infrastruktur, Bildung, Beschäftigung, etc.)

Achse 2: GPAI

5 Fragen zu General-Purpose AI (Foundation Models, systemisches Risiko, Art. 51–56)

setSystemName(e.target.value)} placeholder="z.B. Dokumenten-Analyse-KI, Chatbot-Service, Code-Assistent" className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />