All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
613 lines
23 KiB
TypeScript
613 lines
23 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
|
|
|
// =============================================================================
|
|
// 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
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN PAGE
|
|
// =============================================================================
|
|
|
|
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)
|
|
|
|
// 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) {
|
|
// Edit existing system via PUT
|
|
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 {
|
|
// Fallback: update locally
|
|
setSystems(prev => prev.map(s =>
|
|
s.id === editingSystem.id ? { ...s, ...data } : s
|
|
))
|
|
}
|
|
setEditingSystem(null)
|
|
} else {
|
|
// Create new system via POST
|
|
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 {
|
|
// Fallback: add locally
|
|
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}
|
|
>
|
|
<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 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">×</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|