Files
breakpilot-compliance/admin-compliance/app/sdk/ai-act/page.tsx
Benjamin Admin bc75b4455d feat: AI Act Decision Tree — Zwei-Achsen-Klassifikation (GPAI + High-Risk)
Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation
auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12).
Deterministische Auswertung ohne LLM.

Backend (Go):
- Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult
- Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree()
- Store-Methoden für CRUD der Ergebnisse
- API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results
- 12 Unit Tests (alle bestanden)

Frontend (Next.js):
- DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht
- AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse)
- Proxy-Route für decision-tree Endpoints

Migration 083: ai_act_decision_tree_results Tabelle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 10:14:09 +02:00

813 lines
31 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import DecisionTreeWizard from '@/components/sdk/ai-act/DecisionTreeWizard'
// =============================================================================
// TYPES
// =============================================================================
interface AISystem {
id: string
name: string
description: string
classification: 'prohibited' | 'high-risk' | 'limited-risk' | 'minimal-risk' | 'unclassified'
purpose: string
sector: string
status: 'draft' | 'classified' | 'compliant' | 'non-compliant'
obligations: string[]
assessmentDate: Date | null
assessmentResult: Record<string, unknown> | null
}
type TabId = 'overview' | 'decision-tree' | 'results'
// =============================================================================
// LOADING SKELETON
// =============================================================================
function LoadingSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[1, 2, 3, 4].map(i => (
<div key={i} className="bg-white rounded-xl border border-gray-200 p-6 animate-pulse">
<div className="flex items-center gap-2 mb-3">
<div className="h-5 w-24 bg-gray-200 rounded-full" />
<div className="h-5 w-20 bg-gray-200 rounded-full" />
</div>
<div className="h-6 w-3/4 bg-gray-200 rounded mb-2" />
<div className="h-4 w-full bg-gray-100 rounded mb-4" />
<div className="h-4 w-1/2 bg-gray-100 rounded" />
<div className="mt-4 pt-4 border-t border-gray-100">
<div className="flex gap-2">
<div className="h-8 flex-1 bg-gray-200 rounded-lg" />
<div className="h-8 w-24 bg-gray-200 rounded-lg" />
</div>
</div>
</div>
))}
</div>
)
}
// =============================================================================
// COMPONENTS
// =============================================================================
function RiskPyramid({ systems }: { systems: AISystem[] }) {
const counts = {
prohibited: systems.filter(s => s.classification === 'prohibited').length,
'high-risk': systems.filter(s => s.classification === 'high-risk').length,
'limited-risk': systems.filter(s => s.classification === 'limited-risk').length,
'minimal-risk': systems.filter(s => s.classification === 'minimal-risk').length,
}
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">AI Act Risikopyramide</h3>
<div className="flex flex-col items-center space-y-1">
<div className="w-24 h-12 bg-red-500 text-white flex items-center justify-center rounded-t-lg text-sm font-medium">
Verboten ({counts.prohibited})
</div>
<div className="w-40 h-12 bg-orange-500 text-white flex items-center justify-center text-sm font-medium">
Hochrisiko ({counts['high-risk']})
</div>
<div className="w-56 h-12 bg-yellow-500 text-white flex items-center justify-center text-sm font-medium">
Begrenztes Risiko ({counts['limited-risk']})
</div>
<div className="w-72 h-12 bg-green-500 text-white flex items-center justify-center rounded-b-lg text-sm font-medium">
Minimales Risiko ({counts['minimal-risk']})
</div>
</div>
<div className="mt-4 text-center text-sm text-gray-500">
{systems.filter(s => s.classification === 'unclassified').length} System(e) noch nicht klassifiziert
</div>
</div>
)
}
function AddSystemForm({
onSubmit,
onCancel,
initialData,
}: {
onSubmit: (system: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => void
onCancel: () => void
initialData?: AISystem | null
}) {
const [formData, setFormData] = useState({
name: initialData?.name || '',
description: initialData?.description || '',
purpose: initialData?.purpose || '',
sector: initialData?.sector || '',
classification: (initialData?.classification || 'unclassified') as AISystem['classification'],
status: (initialData?.status || 'draft') as AISystem['status'],
obligations: initialData?.obligations || [] as string[],
})
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{initialData ? 'KI-System bearbeiten' : 'Neues KI-System registrieren'}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={e => setFormData({ ...formData, name: e.target.value })}
placeholder="z.B. Dokumenten-Scanner"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={e => setFormData({ ...formData, description: e.target.value })}
placeholder="Beschreiben Sie das KI-System..."
rows={2}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einsatzzweck</label>
<input
type="text"
value={formData.purpose}
onChange={e => setFormData({ ...formData, purpose: e.target.value })}
placeholder="z.B. Texterkennung"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sektor</label>
<input
type="text"
value={formData.sector}
onChange={e => setFormData({ ...formData, sector: e.target.value })}
placeholder="z.B. Verwaltung"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Vorklassifizierung</label>
<select
value={formData.classification}
onChange={e => setFormData({ ...formData, classification: e.target.value as AISystem['classification'] })}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
>
<option value="unclassified">Noch nicht klassifiziert</option>
<option value="minimal-risk">Minimales Risiko</option>
<option value="limited-risk">Begrenztes Risiko</option>
<option value="high-risk">Hochrisiko</option>
<option value="prohibited">Verboten</option>
</select>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-3">
<button onClick={onCancel} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Abbrechen
</button>
<button
onClick={() => onSubmit(formData)}
disabled={!formData.name}
className={`px-6 py-2 rounded-lg font-medium transition-colors ${
formData.name ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
}`}
>
{initialData ? 'Speichern' : 'Registrieren'}
</button>
</div>
</div>
)
}
function AISystemCard({
system,
onAssess,
onEdit,
onDelete,
assessing,
}: {
system: AISystem
onAssess: () => void
onEdit: () => void
onDelete: () => void
assessing: boolean
}) {
const classificationColors = {
prohibited: 'bg-red-100 text-red-700 border-red-200',
'high-risk': 'bg-orange-100 text-orange-700 border-orange-200',
'limited-risk': 'bg-yellow-100 text-yellow-700 border-yellow-200',
'minimal-risk': 'bg-green-100 text-green-700 border-green-200',
unclassified: 'bg-gray-100 text-gray-500 border-gray-200',
}
const classificationLabels = {
prohibited: 'Verboten',
'high-risk': 'Hochrisiko',
'limited-risk': 'Begrenztes Risiko',
'minimal-risk': 'Minimales Risiko',
unclassified: 'Nicht klassifiziert',
}
const statusColors = {
draft: 'bg-gray-100 text-gray-500',
classified: 'bg-blue-100 text-blue-700',
compliant: 'bg-green-100 text-green-700',
'non-compliant': 'bg-red-100 text-red-700',
}
const statusLabels = {
draft: 'Entwurf',
classified: 'Klassifiziert',
compliant: 'Konform',
'non-compliant': 'Nicht konform',
}
return (
<div className={`bg-white rounded-xl border-2 p-6 ${
system.classification === 'high-risk' ? 'border-orange-200' :
system.classification === 'prohibited' ? 'border-red-200' : 'border-gray-200'
}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className={`px-2 py-1 text-xs rounded-full ${classificationColors[system.classification]}`}>
{classificationLabels[system.classification]}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[system.status]}`}>
{statusLabels[system.status]}
</span>
</div>
<h3 className="text-lg font-semibold text-gray-900">{system.name}</h3>
<p className="text-sm text-gray-500 mt-1">{system.description}</p>
<div className="mt-2 text-sm text-gray-500">
<span>Sektor: {system.sector}</span>
{system.assessmentDate && (
<span className="ml-4">Klassifiziert: {system.assessmentDate.toLocaleDateString('de-DE')}</span>
)}
</div>
</div>
</div>
{system.obligations.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-100">
<p className="text-sm font-medium text-gray-700 mb-2">Pflichten nach AI Act:</p>
<div className="flex flex-wrap gap-2">
{system.obligations.map(obl => (
<span key={obl} className="px-2 py-1 text-xs bg-purple-50 text-purple-700 rounded">
{obl}
</span>
))}
</div>
</div>
)}
{system.assessmentResult && (
<div className="mt-3 p-3 bg-blue-50 rounded-lg">
<p className="text-xs font-medium text-blue-700">KI-Risikobewertung abgeschlossen</p>
</div>
)}
<div className="mt-4 flex items-center gap-2">
<button
onClick={onAssess}
disabled={assessing}
className="flex-1 px-4 py-2 text-sm bg-purple-50 text-purple-700 rounded-lg hover:bg-purple-100 transition-colors disabled:opacity-50"
>
{assessing ? (
<span className="flex items-center justify-center gap-2">
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Bewertung laeuft...
</span>
) : (
system.classification === 'unclassified' ? 'Klassifizierung starten' : 'Risikobewertung starten'
)}
</button>
<button
onClick={onEdit}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Bearbeiten
</button>
<button
onClick={onDelete}
className="px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
Loeschen
</button>
</div>
</div>
)
}
// =============================================================================
// SAVED RESULTS TAB
// =============================================================================
interface SavedResult {
id: string
system_name: string
system_description?: string
high_risk_result: string
gpai_result: { gpai_category: string; is_systemic_risk: boolean }
combined_obligations: string[]
created_at: string
}
function SavedResultsTab() {
const [results, setResults] = useState<SavedResult[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const load = async () => {
try {
const res = await fetch('/api/sdk/v1/ucca/decision-tree/results')
if (res.ok) {
const data = await res.json()
setResults(data.results || [])
}
} catch {
// Ignore
} finally {
setLoading(false)
}
}
load()
}, [])
const handleDelete = async (id: string) => {
if (!confirm('Ergebnis wirklich löschen?')) return
try {
const res = await fetch(`/api/sdk/v1/ucca/decision-tree/results/${id}`, { method: 'DELETE' })
if (res.ok) {
setResults(prev => prev.filter(r => r.id !== id))
}
} catch {
// Ignore
}
}
const riskLabels: Record<string, string> = {
unacceptable: 'Unzulässig',
high_risk: 'Hochrisiko',
limited_risk: 'Begrenztes Risiko',
minimal_risk: 'Minimales Risiko',
not_applicable: 'Nicht anwendbar',
}
const riskColors: Record<string, string> = {
unacceptable: 'bg-red-100 text-red-700',
high_risk: 'bg-orange-100 text-orange-700',
limited_risk: 'bg-yellow-100 text-yellow-700',
minimal_risk: 'bg-green-100 text-green-700',
not_applicable: 'bg-gray-100 text-gray-500',
}
const gpaiLabels: Record<string, string> = {
none: 'Kein GPAI',
standard: 'GPAI Standard',
systemic: 'GPAI Systemisch',
}
const gpaiColors: Record<string, string> = {
none: 'bg-gray-100 text-gray-500',
standard: 'bg-blue-100 text-blue-700',
systemic: 'bg-purple-100 text-purple-700',
}
if (loading) {
return <LoadingSkeleton />
}
if (results.length === 0) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Ergebnisse vorhanden</h3>
<p className="mt-2 text-gray-500">Nutzen Sie den Entscheidungsbaum, um KI-Systeme zu klassifizieren.</p>
</div>
)
}
return (
<div className="space-y-4">
{results.map(r => (
<div key={r.id} className="bg-white rounded-xl border border-gray-200 p-5">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold text-gray-900">{r.system_name}</h4>
{r.system_description && (
<p className="text-sm text-gray-500 mt-0.5">{r.system_description}</p>
)}
<div className="flex items-center gap-2 mt-2">
<span className={`px-2 py-1 text-xs rounded-full ${riskColors[r.high_risk_result] || 'bg-gray-100 text-gray-500'}`}>
{riskLabels[r.high_risk_result] || r.high_risk_result}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${gpaiColors[r.gpai_result?.gpai_category] || 'bg-gray-100 text-gray-500'}`}>
{gpaiLabels[r.gpai_result?.gpai_category] || 'Kein GPAI'}
</span>
{r.gpai_result?.is_systemic_risk && (
<span className="px-2 py-1 text-xs rounded-full bg-red-100 text-red-700">Systemisch</span>
)}
</div>
<div className="text-xs text-gray-400 mt-2">
{r.combined_obligations?.length || 0} Pflichten &middot; {new Date(r.created_at).toLocaleDateString('de-DE')}
</div>
</div>
<button
onClick={() => handleDelete(r.id)}
className="px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors"
>
Löschen
</button>
</div>
</div>
))}
</div>
)
}
// =============================================================================
// TABS
// =============================================================================
const TABS: { id: TabId; label: string; icon: React.ReactNode }[] = [
{
id: 'overview',
label: 'Übersicht',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
),
},
{
id: 'decision-tree',
label: 'Entscheidungsbaum',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 010 3.75H5.625a1.875 1.875 0 010-3.75z" />
</svg>
),
},
{
id: 'results',
label: 'Ergebnisse',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25z" />
</svg>
),
},
]
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function AIActPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [systems, setSystems] = useState<AISystem[]>([])
const [filter, setFilter] = useState<string>('all')
const [showAddForm, setShowAddForm] = useState(false)
const [editingSystem, setEditingSystem] = useState<AISystem | null>(null)
const [assessingId, setAssessingId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
// Fetch systems from backend on mount
useEffect(() => {
const fetchSystems = async () => {
setLoading(true)
try {
const res = await fetch('/api/sdk/v1/compliance/ai/systems')
if (res.ok) {
const data = await res.json()
const backendSystems = data.systems || []
setSystems(backendSystems.map((s: Record<string, unknown>) => ({
id: s.id as string,
name: s.name as string,
description: (s.description || '') as string,
purpose: (s.purpose || '') as string,
sector: (s.sector || '') as string,
classification: (s.classification || 'unclassified') as AISystem['classification'],
status: (s.status || 'draft') as AISystem['status'],
obligations: (s.obligations || []) as string[],
assessmentDate: s.assessment_date ? new Date(s.assessment_date as string) : null,
assessmentResult: (s.assessment_result || null) as Record<string, unknown> | null,
})))
}
} catch {
// Backend unavailable — start with empty list
} finally {
setLoading(false)
}
}
fetchSystems()
}, [])
const handleAddSystem = async (data: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => {
setError(null)
if (editingSystem) {
try {
const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${editingSystem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: data.name,
description: data.description,
purpose: data.purpose,
sector: data.sector,
classification: data.classification,
status: data.status,
obligations: data.obligations,
}),
})
if (res.ok) {
const updated = await res.json()
setSystems(prev => prev.map(s =>
s.id === editingSystem.id
? { ...s, ...data, id: updated.id || editingSystem.id }
: s
))
} else {
setError('Speichern fehlgeschlagen')
}
} catch {
setSystems(prev => prev.map(s =>
s.id === editingSystem.id ? { ...s, ...data } : s
))
}
setEditingSystem(null)
} else {
try {
const res = await fetch('/api/sdk/v1/compliance/ai/systems', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: data.name,
description: data.description,
purpose: data.purpose,
sector: data.sector,
classification: data.classification,
status: data.status,
obligations: data.obligations,
}),
})
if (res.ok) {
const created = await res.json()
const newSystem: AISystem = {
...data,
id: created.id,
assessmentDate: created.assessment_date ? new Date(created.assessment_date) : null,
assessmentResult: created.assessment_result || null,
}
setSystems(prev => [...prev, newSystem])
} else {
setError('Registrierung fehlgeschlagen')
}
} catch {
const newSystem: AISystem = {
...data,
id: `ai-${Date.now()}`,
assessmentDate: data.classification !== 'unclassified' ? new Date() : null,
assessmentResult: null,
}
setSystems(prev => [...prev, newSystem])
}
}
setShowAddForm(false)
}
const handleDelete = async (id: string) => {
if (!confirm('Moechten Sie dieses KI-System wirklich loeschen?')) return
try {
const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${id}`, { method: 'DELETE' })
if (res.ok) {
setSystems(prev => prev.filter(s => s.id !== id))
} else {
setError('Loeschen fehlgeschlagen')
}
} catch {
setError('Backend nicht erreichbar')
}
}
const handleEdit = (system: AISystem) => {
setEditingSystem(system)
setShowAddForm(true)
}
const handleAssess = async (systemId: string) => {
setAssessingId(systemId)
setError(null)
try {
const res = await fetch(`/api/sdk/v1/compliance/ai/systems/${systemId}/assess`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
if (res.ok) {
const result = await res.json()
setSystems(prev => prev.map(s =>
s.id === systemId
? {
...s,
assessmentDate: result.assessment_date ? new Date(result.assessment_date) : new Date(),
assessmentResult: result.assessment_result || result,
classification: (result.classification || s.classification) as AISystem['classification'],
status: (result.status || 'classified') as AISystem['status'],
obligations: result.obligations || s.obligations,
}
: s
))
} else {
const errData = await res.json().catch(() => ({ error: 'Bewertung fehlgeschlagen' }))
setError(errData.error || errData.detail || 'Bewertung fehlgeschlagen')
}
} catch {
setError('Verbindung zum KI-Service fehlgeschlagen. Bitte versuchen Sie es spaeter erneut.')
} finally {
setAssessingId(null)
}
}
const filteredSystems = filter === 'all'
? systems
: systems.filter(s => s.classification === filter || s.status === filter)
const highRiskCount = systems.filter(s => s.classification === 'high-risk').length
const compliantCount = systems.filter(s => s.status === 'compliant').length
const unclassifiedCount = systems.filter(s => s.classification === 'unclassified').length
const stepInfo = STEP_EXPLANATIONS['ai-act']
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="ai-act"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
{activeTab === 'overview' && (
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
KI-System registrieren
</button>
)}
</StepHeader>
{/* Tabs */}
<div className="flex items-center gap-1 bg-gray-100 p-1 rounded-lg w-fit">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-md transition-colors ${
activeTab === tab.id
? 'bg-white text-purple-700 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</div>
{/* Error Banner */}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
<span>{error}</span>
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">&times;</button>
</div>
)}
{/* Tab: Overview */}
{activeTab === 'overview' && (
<>
{/* Add/Edit System Form */}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
)}
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">KI-Systeme gesamt</div>
<div className="text-3xl font-bold text-gray-900">{systems.length}</div>
</div>
<div className="bg-white rounded-xl border border-orange-200 p-6">
<div className="text-sm text-orange-600">Hochrisiko</div>
<div className="text-3xl font-bold text-orange-600">{highRiskCount}</div>
</div>
<div className="bg-white rounded-xl border border-green-200 p-6">
<div className="text-sm text-green-600">Konform</div>
<div className="text-3xl font-bold text-green-600">{compliantCount}</div>
</div>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="text-sm text-gray-500">Nicht klassifiziert</div>
<div className="text-3xl font-bold text-gray-500">{unclassifiedCount}</div>
</div>
</div>
{/* Risk Pyramid */}
<RiskPyramid systems={systems} />
{/* Filter */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{['all', 'high-risk', 'limited-risk', 'minimal-risk', 'unclassified', 'compliant', 'non-compliant'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'high-risk' ? 'Hochrisiko' :
f === 'limited-risk' ? 'Begrenztes Risiko' :
f === 'minimal-risk' ? 'Minimales Risiko' :
f === 'unclassified' ? 'Nicht klassifiziert' :
f === 'compliant' ? 'Konform' : 'Nicht konform'}
</button>
))}
</div>
{/* Loading */}
{loading && <LoadingSkeleton />}
{/* AI Systems List */}
{!loading && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{filteredSystems.map(system => (
<AISystemCard
key={system.id}
system={system}
onAssess={() => handleAssess(system.id)}
onEdit={() => handleEdit(system)}
onDelete={() => handleDelete(system.id)}
assessing={assessingId === system.id}
/>
))}
</div>
)}
{!loading && filteredSystems.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine KI-Systeme gefunden</h3>
<p className="mt-2 text-gray-500">Passen Sie den Filter an oder registrieren Sie ein neues KI-System.</p>
</div>
)}
</>
)}
{/* Tab: Decision Tree */}
{activeTab === 'decision-tree' && (
<DecisionTreeWizard />
)}
{/* Tab: Results */}
{activeTab === 'results' && (
<SavedResultsTab />
)}
</div>
)
}