refactor(admin): split AIUseCaseModuleEditor, DataPointCatalog, ProjectSelector components
AIUseCaseModuleEditor (698 LOC) → thin orchestrator (187) + constants (29) + barrel tabs (4) + tabs implementation split into SystemData (261), PurposeAct (149), RisksReview (219). DataPointCatalog (658 LOC) → main (291) + helpers (190) + CategoryGroup (124) + Row (108). ProjectSelector (656 LOC) → main (211) + CreateProjectDialog (169) + ProjectActionDialog (140) + ProjectCard (128). All files now under 300 LOC soft target and 500 LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,7 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
AIUseCaseModule,
|
||||
AIUseCaseType,
|
||||
AIActRiskClass,
|
||||
AI_USE_CASE_TYPES,
|
||||
AI_ACT_RISK_CLASSES,
|
||||
PRIVACY_BY_DESIGN_CATEGORIES,
|
||||
PrivacyByDesignCategory,
|
||||
PrivacyByDesignMeasure,
|
||||
@@ -14,8 +11,16 @@ import {
|
||||
AIModuleReviewTriggerType,
|
||||
checkArt22Applicability,
|
||||
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||||
import { Art22AssessmentPanel } from './Art22AssessmentPanel'
|
||||
import { AIRiskCriteriaChecklist } from './AIRiskCriteriaChecklist'
|
||||
import { TABS, REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants'
|
||||
import {
|
||||
Tab1System,
|
||||
Tab2Data,
|
||||
Tab3Purpose,
|
||||
Tab4AIAct,
|
||||
Tab5Risks,
|
||||
Tab6PrivacyByDesign,
|
||||
Tab7Review,
|
||||
} from './AIUseCaseEditorTabs'
|
||||
|
||||
interface AIUseCaseModuleEditorProps {
|
||||
module: AIUseCaseModule
|
||||
@@ -24,34 +29,6 @@ interface AIUseCaseModuleEditorProps {
|
||||
isSaving?: boolean
|
||||
}
|
||||
|
||||
const TABS = [
|
||||
{ id: 1, label: 'System', icon: '🖥️' },
|
||||
{ id: 2, label: 'Daten', icon: '📊' },
|
||||
{ id: 3, label: 'Zweck & Art. 22', icon: '⚖️' },
|
||||
{ id: 4, label: 'KI-Kriterien', icon: '🔍' },
|
||||
{ id: 5, label: 'Risiken', icon: '⚠️' },
|
||||
{ id: 6, label: 'Maßnahmen', icon: '🛡️' },
|
||||
{ id: 7, label: 'Review', icon: '🔄' },
|
||||
]
|
||||
|
||||
const REVIEW_TRIGGER_TYPES: { value: AIModuleReviewTriggerType; label: string; icon: string }[] = [
|
||||
{ value: 'model_update', label: 'Modell-Update', icon: '🔄' },
|
||||
{ value: 'data_drift', label: 'Datendrift', icon: '📉' },
|
||||
{ value: 'accuracy_drop', label: 'Genauigkeitsabfall', icon: '📊' },
|
||||
{ value: 'new_use_case', label: 'Neuer Anwendungsfall', icon: '🎯' },
|
||||
{ value: 'regulatory_change', label: 'Regulatorische Änderung', icon: '📜' },
|
||||
{ value: 'incident', label: 'Sicherheitsvorfall', icon: '🚨' },
|
||||
{ value: 'periodic', label: 'Regelmäßig (zeitbasiert)', icon: '📅' },
|
||||
]
|
||||
|
||||
const LEGAL_BASES = [
|
||||
'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)',
|
||||
'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung)',
|
||||
'Art. 6 Abs. 1 lit. c DSGVO (Rechtliche Verpflichtung)',
|
||||
'Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse)',
|
||||
'Art. 9 Abs. 2 lit. a DSGVO (Ausdrückliche Einwilligung)',
|
||||
]
|
||||
|
||||
export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, isSaving }: AIUseCaseModuleEditorProps) {
|
||||
const [activeTab, setActiveTab] = useState(1)
|
||||
const [module, setModule] = useState<AIUseCaseModule>(initialModule)
|
||||
@@ -146,514 +123,26 @@ export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel,
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{/* Tab 1: Systembeschreibung */}
|
||||
{activeTab === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name des KI-Anwendungsfalls *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.name}
|
||||
onChange={e => update({ name: e.target.value })}
|
||||
placeholder={`z.B. ${typeInfo.label} für Kundenservice`}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Systembeschreibung *</label>
|
||||
<textarea
|
||||
value={module.model_description}
|
||||
onChange={e => update({ model_description: e.target.value })}
|
||||
rows={4}
|
||||
placeholder="Beschreiben Sie das KI-System: Funktionsweise, Input/Output, eingesetzte Algorithmen..."
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Modell-Typ</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.model_type || ''}
|
||||
onChange={e => update({ model_type: e.target.value })}
|
||||
placeholder="z.B. Random Forest, GPT-4, CNN"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Anbieter / Provider</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.provider || ''}
|
||||
onChange={e => update({ provider: e.target.value })}
|
||||
placeholder="z.B. Anthropic, OpenAI, intern"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Datenfluss-Beschreibung</label>
|
||||
<textarea
|
||||
value={module.data_flow_description || ''}
|
||||
onChange={e => update({ data_flow_description: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="Wie fließen Daten in das KI-System ein und aus? Gibt es Drittland-Transfers?"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="third_country"
|
||||
checked={module.third_country_transfer}
|
||||
onChange={e => update({ third_country_transfer: e.target.checked })}
|
||||
className="h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||
/>
|
||||
<label htmlFor="third_country" className="text-sm text-gray-700">
|
||||
Drittland-Transfer (außerhalb EU/EWR)
|
||||
</label>
|
||||
{module.third_country_transfer && (
|
||||
<input
|
||||
type="text"
|
||||
value={module.provider_country || ''}
|
||||
onChange={e => update({ provider_country: e.target.value })}
|
||||
placeholder="Land (z.B. USA)"
|
||||
className="ml-2 px-2 py-1 text-sm border border-orange-300 rounded focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 2: Daten & Betroffene */}
|
||||
{activeTab === 1 && <Tab1System module={module} update={update} typeInfo={typeInfo} />}
|
||||
{activeTab === 2 && (
|
||||
<div className="space-y-4">
|
||||
{/* Input Data Categories */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Input-Datenkategorien *</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newCategory}
|
||||
onChange={e => setNewCategory(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && addToList('input_data_categories', newCategory, setNewCategory)}
|
||||
placeholder="Datenkategorie hinzufügen..."
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addToList('input_data_categories', newCategory, setNewCategory)}
|
||||
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(module.input_data_categories || []).map((cat, i) => (
|
||||
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs">
|
||||
{cat}
|
||||
<button onClick={() => removeFromList('input_data_categories', i)} className="hover:text-purple-900">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output Data Categories */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Output-Datenkategorien</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newOutputCategory}
|
||||
onChange={e => setNewOutputCategory(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && addToList('output_data_categories', newOutputCategory, setNewOutputCategory)}
|
||||
placeholder="Output-Kategorie (z.B. Bewertung, Empfehlung)..."
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addToList('output_data_categories', newOutputCategory, setNewOutputCategory)}
|
||||
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(module.output_data_categories || []).map((cat, i) => (
|
||||
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs">
|
||||
{cat}
|
||||
<button onClick={() => removeFromList('output_data_categories', i)} className="hover:text-blue-900">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Special Categories */}
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg border border-gray-200">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="special_cats"
|
||||
checked={module.involves_special_categories}
|
||||
onChange={e => update({ involves_special_categories: e.target.checked })}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||
/>
|
||||
<label htmlFor="special_cats" className="flex-1">
|
||||
<div className="text-sm font-medium text-gray-900">Besondere Kategorien (Art. 9 DSGVO)</div>
|
||||
<p className="text-xs text-gray-500">Gesundheit, Biometrie, Religion, politische Meinung etc.</p>
|
||||
{module.involves_special_categories && (
|
||||
<textarea
|
||||
value={module.special_categories_justification || ''}
|
||||
onChange={e => update({ special_categories_justification: e.target.value })}
|
||||
rows={2}
|
||||
placeholder="Begründung nach Art. 9 Abs. 2 DSGVO..."
|
||||
className="mt-2 w-full px-3 py-2 text-xs border border-orange-300 rounded focus:ring-2 focus:ring-orange-400 resize-none"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Data Subjects */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Betroffenengruppen *</label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newSubject}
|
||||
onChange={e => setNewSubject(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && addToList('data_subjects', newSubject, setNewSubject)}
|
||||
placeholder="z.B. Kunden, Mitarbeiter, Nutzer..."
|
||||
className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() => addToList('data_subjects', newSubject, setNewSubject)}
|
||||
className="px-3 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(module.data_subjects || []).map((s, i) => (
|
||||
<span key={i} className="flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
|
||||
{s}
|
||||
<button onClick={() => removeFromList('data_subjects', i)} className="hover:text-green-900">×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Geschätztes Volumen</label>
|
||||
<input
|
||||
type="text"
|
||||
value={module.estimated_volume || ''}
|
||||
onChange={e => update({ estimated_volume: e.target.value })}
|
||||
placeholder="z.B. >10.000 Personen/Monat"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsdauer (Monate)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={module.data_retention_months || ''}
|
||||
onChange={e => update({ data_retention_months: parseInt(e.target.value) || undefined })}
|
||||
min={1}
|
||||
placeholder="z.B. 24"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 3: Zweck & Art. 22 */}
|
||||
{activeTab === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Verarbeitungszweck *</label>
|
||||
<textarea
|
||||
value={module.processing_purpose}
|
||||
onChange={e => update({ processing_purpose: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="Welchem Zweck dient dieses KI-System? Was soll erreicht werden?"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Rechtsgrundlage *</label>
|
||||
<select
|
||||
value={module.legal_basis}
|
||||
onChange={e => update({ legal_basis: e.target.value })}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
{LEGAL_BASES.map(lb => (
|
||||
<option key={lb} value={lb}>{lb}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{module.legal_basis && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Details zur Rechtsgrundlage</label>
|
||||
<textarea
|
||||
value={module.legal_basis_details || ''}
|
||||
onChange={e => update({ legal_basis_details: e.target.value })}
|
||||
rows={2}
|
||||
placeholder="Ergänzende Erläuterung zur Rechtsgrundlage..."
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Art. 22 Panel */}
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">
|
||||
Art. 22 DSGVO – Automatisierte Einzelentscheidung
|
||||
{art22Required && (
|
||||
<span className="ml-2 text-xs px-1.5 py-0.5 bg-red-100 text-red-700 rounded">
|
||||
Wahrscheinlich anwendbar
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
<Art22AssessmentPanel
|
||||
assessment={module.art22_assessment}
|
||||
onChange={a22 => update({ art22_assessment: a22 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 4: KI-Kriterien & AI Act */}
|
||||
{activeTab === 4 && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">WP248-Risikokriterien</h4>
|
||||
<AIRiskCriteriaChecklist
|
||||
criteria={module.risk_criteria}
|
||||
onChange={criteria => update({ risk_criteria: criteria })}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-gray-200 pt-4">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">EU AI Act – Risikoklasse</h4>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{(Object.entries(AI_ACT_RISK_CLASSES) as [AIActRiskClass, typeof AI_ACT_RISK_CLASSES[AIActRiskClass]][]).map(([cls, info]) => (
|
||||
<button
|
||||
key={cls}
|
||||
onClick={() => update({ ai_act_risk_class: cls })}
|
||||
className={`p-3 rounded-xl border-2 text-left transition-all ${
|
||||
module.ai_act_risk_class === cls
|
||||
? cls === 'unacceptable' ? 'border-red-500 bg-red-50'
|
||||
: cls === 'high_risk' ? 'border-orange-500 bg-orange-50'
|
||||
: cls === 'limited' ? 'border-yellow-500 bg-yellow-50'
|
||||
: 'border-green-500 bg-green-50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-sm text-gray-900">{info.labelDE}</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5 line-clamp-2">{info.description}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{module.ai_act_risk_class && (
|
||||
<div className="mt-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Begründung der Klassifizierung</label>
|
||||
<textarea
|
||||
value={module.ai_act_justification || ''}
|
||||
onChange={e => update({ ai_act_justification: e.target.value })}
|
||||
rows={2}
|
||||
placeholder="Warum wurde diese Risikoklasse gewählt?"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{module.ai_act_risk_class && AI_ACT_RISK_CLASSES[module.ai_act_risk_class].requirements.length > 0 && (
|
||||
<div className="mt-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-xs font-medium text-gray-700 mb-2">Anforderungen dieser Klasse:</div>
|
||||
<ul className="space-y-1">
|
||||
{AI_ACT_RISK_CLASSES[module.ai_act_risk_class].requirements.map((req, i) => (
|
||||
<li key={i} className="text-xs text-gray-600 flex items-start gap-1.5">
|
||||
<span className="text-purple-500 flex-shrink-0">•</span>
|
||||
{req}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 5: Risikoanalyse */}
|
||||
{activeTab === 5 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Spezifische Risiken für diesen KI-Anwendungsfall. Typische Risiken basierend auf dem gewählten Typ:
|
||||
</p>
|
||||
{typeInfo.typical_risks.length > 0 && (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div className="text-xs font-medium text-yellow-800 mb-1">Typische Risiken für {typeInfo.label}:</div>
|
||||
<ul className="space-y-0.5">
|
||||
{typeInfo.typical_risks.map((r, i) => (
|
||||
<li key={i} className="text-xs text-yellow-700 flex items-center gap-1.5">
|
||||
<span>⚠️</span> {r}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{(module.risks || []).map((risk, idx) => (
|
||||
<div key={idx} className="p-3 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-gray-900">{risk.description}</p>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<span className="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-600 rounded">W: {risk.likelihood}</span>
|
||||
<span className="text-xs px-1.5 py-0.5 bg-purple-100 text-purple-600 rounded">S: {risk.impact}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => update({ risks: module.risks.filter((_, i) => i !== idx) })}
|
||||
className="text-gray-400 hover:text-red-500 ml-2"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(module.risks || []).length === 0 && (
|
||||
<p className="text-sm text-gray-400 text-center py-4">Noch keine Risiken dokumentiert</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Add Risk */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const desc = prompt('Risiko-Beschreibung:')
|
||||
if (desc) {
|
||||
update({
|
||||
risks: [...(module.risks || []), {
|
||||
risk_id: crypto.randomUUID(),
|
||||
description: desc,
|
||||
likelihood: 'medium',
|
||||
impact: 'medium',
|
||||
mitigation_ids: [],
|
||||
}]
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="w-full py-2 border-2 border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-purple-400 hover:text-purple-600 transition-colors"
|
||||
>
|
||||
+ Risiko hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 6: Maßnahmen & Privacy by Design */}
|
||||
{activeTab === 6 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">Privacy by Design Maßnahmen</h4>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(Object.entries(PRIVACY_BY_DESIGN_CATEGORIES) as [PrivacyByDesignCategory, typeof PRIVACY_BY_DESIGN_CATEGORIES[PrivacyByDesignCategory]][]).map(([cat, info]) => {
|
||||
const measure = module.privacy_by_design_measures?.find(m => m.category === cat)
|
||||
return (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => togglePbdMeasure(cat)}
|
||||
className={`flex items-start gap-2 p-3 rounded-lg border text-left transition-all ${
|
||||
measure?.implemented
|
||||
? 'border-green-400 bg-green-50'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg flex-shrink-0">{info.icon}</span>
|
||||
<div>
|
||||
<div className={`text-xs font-medium ${measure?.implemented ? 'text-green-800' : 'text-gray-700'}`}>
|
||||
{info.label}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500 mt-0.5">{info.description}</p>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab 7: Review-Trigger */}
|
||||
{activeTab === 7 && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Wählen Sie die Ereignisse, die eine erneute Bewertung dieses KI-Anwendungsfalls auslösen sollen.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{REVIEW_TRIGGER_TYPES.map(rt => {
|
||||
const active = module.review_triggers?.some(t => t.type === rt.value)
|
||||
const trigger = module.review_triggers?.find(t => t.type === rt.value)
|
||||
return (
|
||||
<div key={rt.value} className={`rounded-lg border p-3 transition-all ${active ? 'border-purple-300 bg-purple-50' : 'border-gray-200'}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={active || false}
|
||||
onChange={() => toggleReviewTrigger(rt.value)}
|
||||
className="h-4 w-4 rounded border-gray-300 text-purple-600"
|
||||
/>
|
||||
<span className="text-base">{rt.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900">{rt.label}</span>
|
||||
</div>
|
||||
{active && (
|
||||
<div className="mt-2 ml-7 space-y-2">
|
||||
<input
|
||||
type="text"
|
||||
value={trigger?.threshold || ''}
|
||||
onChange={e => {
|
||||
const updated = (module.review_triggers || []).map(t =>
|
||||
t.type === rt.value ? { ...t, threshold: e.target.value } : t
|
||||
)
|
||||
update({ review_triggers: updated })
|
||||
}}
|
||||
placeholder="Schwellwert (z.B. Genauigkeit < 80%)"
|
||||
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={trigger?.monitoring_interval || ''}
|
||||
onChange={e => {
|
||||
const updated = (module.review_triggers || []).map(t =>
|
||||
t.type === rt.value ? { ...t, monitoring_interval: e.target.value } : t
|
||||
)
|
||||
update({ review_triggers: updated })
|
||||
}}
|
||||
placeholder="Monitoring-Intervall (z.B. wöchentlich)"
|
||||
className="w-full px-2 py-1 text-xs border border-purple-200 rounded focus:ring-2 focus:ring-purple-400"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Monitoring-Beschreibung</label>
|
||||
<textarea
|
||||
value={module.monitoring_description || ''}
|
||||
onChange={e => update({ monitoring_description: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="Wie wird das KI-System kontinuierlich überwacht? Welche Metriken werden erfasst?"
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Nächstes Review-Datum</label>
|
||||
<input
|
||||
type="date"
|
||||
value={module.next_review_date || ''}
|
||||
onChange={e => update({ next_review_date: e.target.value })}
|
||||
className="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Tab2Data
|
||||
module={module}
|
||||
update={update}
|
||||
newCategory={newCategory}
|
||||
setNewCategory={setNewCategory}
|
||||
newOutputCategory={newOutputCategory}
|
||||
setNewOutputCategory={setNewOutputCategory}
|
||||
newSubject={newSubject}
|
||||
setNewSubject={setNewSubject}
|
||||
addToList={addToList}
|
||||
removeFromList={removeFromList}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 3 && <Tab3Purpose module={module} update={update} art22Required={art22Required} />}
|
||||
{activeTab === 4 && <Tab4AIAct module={module} update={update} />}
|
||||
{activeTab === 5 && <Tab5Risks module={module} update={update} typeInfo={typeInfo} />}
|
||||
{activeTab === 6 && <Tab6PrivacyByDesign module={module} update={update} togglePbdMeasure={togglePbdMeasure} />}
|
||||
{activeTab === 7 && <Tab7Review module={module} update={update} toggleReviewTrigger={toggleReviewTrigger} />}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
Reference in New Issue
Block a user