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 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 028: ai_use_case_modules JSONB + section_8_complete auf compliance_dsfas - Neues ai-use-case-types.ts: AIUseCaseModule Interface, 8 Typen, Art22Assessment, AI Act Risikoklassen, WP248-Kriterien, Privacy by Design, createEmptyModule() Helper - types.ts: Section 8 in DSFA_SECTIONS, ai_use_case_modules im DSFA Interface, section_8_complete in DSFASectionProgress - api.ts: addAIUseCaseModule, updateAIUseCaseModule, removeAIUseCaseModule - 5 neue UI-Komponenten: AIUseCaseTypeSelector, Art22AssessmentPanel, AIRiskCriteriaChecklist, AIUseCaseModuleEditor (7 Tabs), AIUseCaseSection - DSFASidebar: Section 8 Eintrag + calculateSectionProgress case 8 - ReviewScheduleSection: ai_use_case_module Trigger-Typ ergänzt - page.tsx: Section 8 Rendering + Weiter-Button auf activeSection < 8 + KI-Module Counter - scripts/ingest-dsfa-bundesland.sh: WP248 + alle 17 Behörden → bp_dsfa_corpus - Docs: dsfa.md Section 8 + RAG-Corpus, Developer Portal DSFA mit AI-Modul-Code-Beispielen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
265 lines
11 KiB
TypeScript
265 lines
11 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState } from 'react'
|
||
import { DSFA } from '@/lib/sdk/dsfa/types'
|
||
import {
|
||
AIUseCaseModule,
|
||
AIUseCaseType,
|
||
AI_USE_CASE_TYPES,
|
||
AI_ACT_RISK_CLASSES,
|
||
createEmptyModule,
|
||
calculateModuleRiskLevel,
|
||
getModuleCompletionPercentage,
|
||
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||
import { AIUseCaseTypeSelector } from './AIUseCaseTypeSelector'
|
||
import { AIUseCaseModuleEditor } from './AIUseCaseModuleEditor'
|
||
|
||
interface AIUseCaseSectionProps {
|
||
dsfa: DSFA
|
||
onUpdate: (data: Record<string, unknown>) => Promise<void>
|
||
isSubmitting: boolean
|
||
}
|
||
|
||
const RISK_LEVEL_LABELS: Record<string, string> = {
|
||
low: 'Niedrig',
|
||
medium: 'Mittel',
|
||
high: 'Hoch',
|
||
very_high: 'Sehr hoch',
|
||
}
|
||
|
||
const RISK_LEVEL_COLORS: Record<string, string> = {
|
||
low: 'bg-green-100 text-green-700 border-green-200',
|
||
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||
high: 'bg-orange-100 text-orange-700 border-orange-200',
|
||
very_high: 'bg-red-100 text-red-700 border-red-200',
|
||
}
|
||
|
||
export function AIUseCaseSection({ dsfa, onUpdate, isSubmitting }: AIUseCaseSectionProps) {
|
||
const [showTypeSelector, setShowTypeSelector] = useState(false)
|
||
const [editingModule, setEditingModule] = useState<AIUseCaseModule | null>(null)
|
||
const [isSavingModule, setIsSavingModule] = useState(false)
|
||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null)
|
||
|
||
const modules = dsfa.ai_use_case_modules || []
|
||
|
||
// Aggregate AI risk overview
|
||
const highRiskCount = modules.filter(m => {
|
||
const level = calculateModuleRiskLevel(m)
|
||
return level === 'high' || level === 'very_high'
|
||
}).length
|
||
|
||
const art22Count = modules.filter(m => m.art22_assessment?.applies).length
|
||
|
||
const handleSelectType = (type: AIUseCaseType) => {
|
||
setShowTypeSelector(false)
|
||
const newModule = createEmptyModule(type)
|
||
setEditingModule(newModule)
|
||
}
|
||
|
||
const handleSaveModule = async (savedModule: AIUseCaseModule) => {
|
||
setIsSavingModule(true)
|
||
try {
|
||
const existing = dsfa.ai_use_case_modules || []
|
||
const idx = existing.findIndex(m => m.id === savedModule.id)
|
||
const updated = idx >= 0
|
||
? existing.map(m => m.id === savedModule.id ? savedModule : m)
|
||
: [...existing, savedModule]
|
||
|
||
await onUpdate({ ai_use_case_modules: updated })
|
||
setEditingModule(null)
|
||
} finally {
|
||
setIsSavingModule(false)
|
||
}
|
||
}
|
||
|
||
const handleDeleteModule = async (moduleId: string) => {
|
||
const updated = modules.filter(m => m.id !== moduleId)
|
||
await onUpdate({ ai_use_case_modules: updated })
|
||
setConfirmDeleteId(null)
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header & Info */}
|
||
<div className="p-4 bg-purple-50 border border-purple-200 rounded-xl">
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-2xl">🤖</span>
|
||
<div className="flex-1">
|
||
<h3 className="font-semibold text-purple-900">KI-Anwendungsfälle (Section 8)</h3>
|
||
<p className="text-sm text-purple-700 mt-1">
|
||
Dokumentieren Sie KI-spezifische Verarbeitungen mit modularen Anhängen nach Art. 35 DSGVO,
|
||
Art. 22 DSGVO und den Transparenzanforderungen des EU AI Act.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Warning: involves_ai but no modules */}
|
||
{dsfa.involves_ai && modules.length === 0 && (
|
||
<div className="flex items-start gap-3 p-4 bg-yellow-50 border border-yellow-300 rounded-xl">
|
||
<svg className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
<div>
|
||
<p className="text-sm font-medium text-yellow-800">KI-Einsatz dokumentiert, aber keine Module vorhanden</p>
|
||
<p className="text-xs text-yellow-700 mt-0.5">
|
||
In Section 3 wurde KI-Einsatz festgestellt. Fügen Sie mindestens ein KI-Modul hinzu.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Aggregated Risk Overview (only when modules exist) */}
|
||
{modules.length > 0 && (
|
||
<div className="grid grid-cols-3 gap-3">
|
||
<div className="p-3 bg-white border border-gray-200 rounded-xl text-center">
|
||
<div className="text-2xl font-bold text-purple-700">{modules.length}</div>
|
||
<div className="text-xs text-gray-500 mt-0.5">KI-Module</div>
|
||
</div>
|
||
<div className={`p-3 rounded-xl text-center border ${highRiskCount > 0 ? 'bg-red-50 border-red-200' : 'bg-green-50 border-green-200'}`}>
|
||
<div className={`text-2xl font-bold ${highRiskCount > 0 ? 'text-red-700' : 'text-green-700'}`}>{highRiskCount}</div>
|
||
<div className="text-xs text-gray-500 mt-0.5">Hoch-Risiko-Module</div>
|
||
</div>
|
||
<div className={`p-3 rounded-xl text-center border ${art22Count > 0 ? 'bg-orange-50 border-orange-200' : 'bg-gray-50 border-gray-200'}`}>
|
||
<div className={`text-2xl font-bold ${art22Count > 0 ? 'text-orange-700' : 'text-gray-400'}`}>{art22Count}</div>
|
||
<div className="text-xs text-gray-500 mt-0.5">Art. 22 DSGVO</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Module Cards */}
|
||
<div className="space-y-3">
|
||
{modules.map(module => {
|
||
const riskLevel = calculateModuleRiskLevel(module)
|
||
const completion = getModuleCompletionPercentage(module)
|
||
const typeInfo = AI_USE_CASE_TYPES[module.use_case_type]
|
||
const aiActInfo = AI_ACT_RISK_CLASSES[module.ai_act_risk_class]
|
||
|
||
return (
|
||
<div
|
||
key={module.id}
|
||
className="bg-white border border-gray-200 rounded-xl p-4 hover:border-purple-300 transition-colors"
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<span className="text-2xl flex-shrink-0">{typeInfo.icon}</span>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-medium text-gray-900">{module.name}</span>
|
||
<span className={`text-xs px-1.5 py-0.5 rounded border ${RISK_LEVEL_COLORS[riskLevel]}`}>
|
||
{RISK_LEVEL_LABELS[riskLevel]}
|
||
</span>
|
||
<span className="text-xs px-1.5 py-0.5 bg-gray-100 text-gray-500 rounded">
|
||
{typeInfo.label}
|
||
</span>
|
||
{module.art22_assessment?.applies && (
|
||
<span className="text-xs px-1.5 py-0.5 bg-orange-100 text-orange-700 rounded border border-orange-200">
|
||
Art. 22
|
||
</span>
|
||
)}
|
||
{module.ai_act_risk_class !== 'minimal' && (
|
||
<span className={`text-xs px-1.5 py-0.5 rounded border ${
|
||
module.ai_act_risk_class === 'unacceptable' ? 'bg-red-100 text-red-700 border-red-200' :
|
||
module.ai_act_risk_class === 'high_risk' ? 'bg-orange-100 text-orange-700 border-orange-200' :
|
||
'bg-yellow-100 text-yellow-700 border-yellow-200'
|
||
}`}>
|
||
AI Act: {aiActInfo.labelDE}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{module.model_description && (
|
||
<p className="text-xs text-gray-500 mt-1 line-clamp-1">{module.model_description}</p>
|
||
)}
|
||
{/* Progress Bar */}
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<div className="flex-1 h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-purple-500 transition-all"
|
||
style={{ width: `${completion}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-xs text-gray-400">{completion}%</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-1 flex-shrink-0">
|
||
<button
|
||
onClick={() => setEditingModule(module)}
|
||
className="px-3 py-1.5 text-xs bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
|
||
>
|
||
Bearbeiten
|
||
</button>
|
||
<button
|
||
onClick={() => setConfirmDeleteId(module.id)}
|
||
className="px-2 py-1.5 text-xs text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Delete confirmation */}
|
||
{confirmDeleteId === module.id && (
|
||
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||
<p className="text-sm text-red-700 mb-2">Modul wirklich löschen?</p>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => handleDeleteModule(module.id)}
|
||
disabled={isSubmitting}
|
||
className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700"
|
||
>
|
||
Löschen
|
||
</button>
|
||
<button
|
||
onClick={() => setConfirmDeleteId(null)}
|
||
className="px-3 py-1 text-xs bg-white border border-gray-300 rounded hover:bg-gray-50"
|
||
>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
|
||
{/* Empty State */}
|
||
{modules.length === 0 && !dsfa.involves_ai && (
|
||
<div className="text-center py-8 text-gray-400">
|
||
<div className="text-4xl mb-2">🤖</div>
|
||
<p className="text-sm">Keine KI-Anwendungsfälle dokumentiert</p>
|
||
<p className="text-xs mt-1">Optional: Fügen Sie KI-Module hinzu, wenn dieses DSFA KI-Einsatz umfasst</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Add Module Button */}
|
||
<button
|
||
onClick={() => setShowTypeSelector(true)}
|
||
disabled={isSubmitting}
|
||
className="w-full py-3 border-2 border-dashed border-purple-300 rounded-xl text-sm text-purple-600 hover:border-purple-500 hover:bg-purple-50 transition-all font-medium"
|
||
>
|
||
+ KI-Anwendungsfall-Modul hinzufügen
|
||
</button>
|
||
|
||
{/* Type Selector Modal */}
|
||
{showTypeSelector && (
|
||
<AIUseCaseTypeSelector
|
||
onSelect={handleSelectType}
|
||
onCancel={() => setShowTypeSelector(false)}
|
||
/>
|
||
)}
|
||
|
||
{/* Module Editor Modal */}
|
||
{editingModule && (
|
||
<AIUseCaseModuleEditor
|
||
module={editingModule}
|
||
onSave={handleSaveModule}
|
||
onCancel={() => setEditingModule(null)}
|
||
isSaving={isSavingModule}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|