Files
breakpilot-compliance/admin-compliance/app/sdk/ai-act/page.tsx
Sharang Parnerkar 653fa07f57 refactor(admin): split academy/[id], iace/hazards, ai-act pages
Extracted components and constants into _components/ subdirectories
to bring all three pages under the 300 LOC soft target (was 651/628/612,
now 255/232/278 LOC respectively). Zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 13:14:50 +02:00

279 lines
11 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { AISystem } from './_components/types'
import { LoadingSkeleton } from './_components/LoadingSkeleton'
import { RiskPyramid } from './_components/RiskPyramid'
import { AddSystemForm } from './_components/AddSystemForm'
import { AISystemCard } from './_components/AISystemCard'
export default function AIActPage() {
const { state } = useSDK()
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)
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">
<StepHeader
stepId="ai-act"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<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>
{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>
)}
{showAddForm && (
<AddSystemForm
onSubmit={handleAddSystem}
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
initialData={editingSystem}
/>
)}
<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>
<RiskPyramid systems={systems} />
<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 && <LoadingSkeleton />}
{!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>
)}
</div>
)
}