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>
220 lines
8.9 KiB
TypeScript
220 lines
8.9 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
import {
|
||
AIUseCaseModule,
|
||
AI_USE_CASE_TYPES,
|
||
PRIVACY_BY_DESIGN_CATEGORIES,
|
||
PrivacyByDesignCategory,
|
||
AIModuleReviewTriggerType,
|
||
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||
import { REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants'
|
||
|
||
type UpdateFn = (updates: Partial<AIUseCaseModule>) => void
|
||
|
||
// =============================================================================
|
||
// TAB 5: Risikoanalyse
|
||
// =============================================================================
|
||
|
||
interface Tab5RisksProps {
|
||
module: AIUseCaseModule
|
||
update: UpdateFn
|
||
typeInfo: typeof AI_USE_CASE_TYPES[keyof typeof AI_USE_CASE_TYPES]
|
||
}
|
||
|
||
export function Tab5Risks({ module, update, typeInfo }: Tab5RisksProps) {
|
||
return (
|
||
<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>
|
||
<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
|
||
// =============================================================================
|
||
|
||
interface Tab6PrivacyByDesignProps {
|
||
module: AIUseCaseModule
|
||
update: UpdateFn
|
||
togglePbdMeasure: (category: PrivacyByDesignCategory) => void
|
||
}
|
||
|
||
export function Tab6PrivacyByDesign({ module, togglePbdMeasure }: Tab6PrivacyByDesignProps) {
|
||
return (
|
||
<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
|
||
// =============================================================================
|
||
|
||
interface Tab7ReviewProps {
|
||
module: AIUseCaseModule
|
||
update: UpdateFn
|
||
toggleReviewTrigger: (type: AIModuleReviewTriggerType) => void
|
||
}
|
||
|
||
export function Tab7Review({ module, update, toggleReviewTrigger }: Tab7ReviewProps) {
|
||
return (
|
||
<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>
|
||
)
|
||
}
|