feat: Analyse-Module auf 100% Runde 2 — CREATE-Forms, Button-Handler, Persistenz
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 36s
CI / test-python-backend-compliance (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
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 36s
CI / test-python-backend-compliance (push) Successful in 36s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s
Requirements: ADD-Form + Details-Panel mit Controls/Status-Anzeige Controls: ADD-Form + Effectiveness-Persistenz via PUT Evidence: Anzeigen/Herunterladen-Buttons mit fileUrl + disabled-State Risks: RiskMatrix Cell-Click filtert Risiko-Liste mit Badge + Reset AI Act: Mock-Daten entfernt, Loading-Skeleton, Edit/Delete-Handler Audit Checklist: JSON-Export, debounced Notes-Persistenz, Neue Checkliste Audit Report: Animiertes Skeleton statt Loading-Text Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
||||
|
||||
@@ -22,47 +22,32 @@ interface AISystem {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL DATA
|
||||
// LOADING SKELETON
|
||||
// =============================================================================
|
||||
|
||||
const initialSystems: AISystem[] = [
|
||||
{
|
||||
id: 'ai-1',
|
||||
name: 'Kundenservice Chatbot',
|
||||
description: 'KI-gestuetzter Chatbot fuer Kundenanfragen',
|
||||
classification: 'limited-risk',
|
||||
purpose: 'Automatisierte Beantwortung von Kundenanfragen',
|
||||
sector: 'Kundenservice',
|
||||
status: 'classified',
|
||||
obligations: ['Transparenzpflicht', 'Kennzeichnung als KI-System'],
|
||||
assessmentDate: new Date('2024-01-15'),
|
||||
assessmentResult: null,
|
||||
},
|
||||
{
|
||||
id: 'ai-2',
|
||||
name: 'Bewerber-Screening',
|
||||
description: 'KI-System zur Vorauswahl von Bewerbungen',
|
||||
classification: 'high-risk',
|
||||
purpose: 'Automatisierte Bewertung von Bewerbungsunterlagen',
|
||||
sector: 'Personal',
|
||||
status: 'non-compliant',
|
||||
obligations: ['Risikomanagementsystem', 'Datenlenkung', 'Technische Dokumentation', 'Menschliche Aufsicht', 'Transparenz'],
|
||||
assessmentDate: new Date('2024-01-10'),
|
||||
assessmentResult: null,
|
||||
},
|
||||
{
|
||||
id: 'ai-3',
|
||||
name: 'Empfehlungsalgorithmus',
|
||||
description: 'Personalisierte Produktempfehlungen',
|
||||
classification: 'minimal-risk',
|
||||
purpose: 'Verbesserung der Kundenerfahrung durch personalisierte Empfehlungen',
|
||||
sector: 'E-Commerce',
|
||||
status: 'compliant',
|
||||
obligations: [],
|
||||
assessmentDate: new Date('2024-01-05'),
|
||||
assessmentResult: null,
|
||||
},
|
||||
]
|
||||
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
|
||||
@@ -103,23 +88,25 @@ function RiskPyramid({ systems }: { systems: AISystem[] }) {
|
||||
function AddSystemForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
initialData,
|
||||
}: {
|
||||
onSubmit: (system: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => void
|
||||
onCancel: () => void
|
||||
initialData?: AISystem | null
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
purpose: '',
|
||||
sector: '',
|
||||
classification: 'unclassified' as AISystem['classification'],
|
||||
status: 'draft' as AISystem['status'],
|
||||
obligations: [] as string[],
|
||||
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">Neues KI-System registrieren</h3>
|
||||
<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>
|
||||
@@ -189,7 +176,7 @@ function AddSystemForm({
|
||||
formData.name ? 'bg-purple-600 text-white hover:bg-purple-700' : 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Registrieren
|
||||
{initialData ? 'Speichern' : 'Registrieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,11 +187,13 @@ function AISystemCard({
|
||||
system,
|
||||
onAssess,
|
||||
onEdit,
|
||||
onDelete,
|
||||
assessing,
|
||||
}: {
|
||||
system: AISystem
|
||||
onAssess: () => void
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
assessing: boolean
|
||||
}) {
|
||||
const classificationColors = {
|
||||
@@ -306,6 +295,12 @@ function AISystemCard({
|
||||
>
|
||||
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>
|
||||
)
|
||||
@@ -317,23 +312,57 @@ function AISystemCard({
|
||||
|
||||
export default function AIActPage() {
|
||||
const { state } = useSDK()
|
||||
const [systems, setSystems] = useState<AISystem[]>(initialSystems)
|
||||
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)
|
||||
|
||||
// Load systems from SDK state on mount
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
// Try to load from SDK state (aiSystems if available)
|
||||
const sdkState = state as unknown as Record<string, unknown>
|
||||
if (Array.isArray(sdkState.aiSystems) && sdkState.aiSystems.length > 0) {
|
||||
setSystems(sdkState.aiSystems as AISystem[])
|
||||
}
|
||||
setLoading(false)
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleAddSystem = (data: Omit<AISystem, 'id' | 'assessmentDate' | 'assessmentResult'>) => {
|
||||
const newSystem: AISystem = {
|
||||
...data,
|
||||
id: `ai-${Date.now()}`,
|
||||
assessmentDate: data.classification !== 'unclassified' ? new Date() : null,
|
||||
assessmentResult: null,
|
||||
if (editingSystem) {
|
||||
// Edit existing system
|
||||
setSystems(prev => prev.map(s =>
|
||||
s.id === editingSystem.id
|
||||
? { ...s, ...data }
|
||||
: s
|
||||
))
|
||||
setEditingSystem(null)
|
||||
} else {
|
||||
// Create new system
|
||||
const newSystem: AISystem = {
|
||||
...data,
|
||||
id: `ai-${Date.now()}`,
|
||||
assessmentDate: data.classification !== 'unclassified' ? new Date() : null,
|
||||
assessmentResult: null,
|
||||
}
|
||||
setSystems(prev => [...prev, newSystem])
|
||||
}
|
||||
setSystems(prev => [...prev, newSystem])
|
||||
setShowAddForm(false)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
if (!confirm('Moechten Sie dieses KI-System wirklich loeschen?')) return
|
||||
setSystems(prev => prev.filter(s => s.id !== id))
|
||||
}
|
||||
|
||||
const handleEdit = (system: AISystem) => {
|
||||
setEditingSystem(system)
|
||||
setShowAddForm(true)
|
||||
}
|
||||
|
||||
const handleAssess = async (systemId: string) => {
|
||||
const system = systems.find(s => s.id === systemId)
|
||||
if (!system) return
|
||||
@@ -420,11 +449,12 @@ export default function AIActPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add System Form */}
|
||||
{/* Add/Edit System Form */}
|
||||
{showAddForm && (
|
||||
<AddSystemForm
|
||||
onSubmit={handleAddSystem}
|
||||
onCancel={() => setShowAddForm(false)}
|
||||
onCancel={() => { setShowAddForm(false); setEditingSystem(null) }}
|
||||
initialData={editingSystem}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -474,20 +504,26 @@ export default function AIActPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI Systems List */}
|
||||
<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={() => {/* Edit handler */}}
|
||||
assessing={assessingId === system.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Loading */}
|
||||
{loading && <LoadingSkeleton />}
|
||||
|
||||
{filteredSystems.length === 0 && (
|
||||
{/* 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">
|
||||
|
||||
Reference in New Issue
Block a user