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>
262 lines
11 KiB
TypeScript
262 lines
11 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
import {
|
||
AIUseCaseModule,
|
||
AI_USE_CASE_TYPES,
|
||
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
||
|
||
type UpdateFn = (updates: Partial<AIUseCaseModule>) => void
|
||
type AddToListFn = (field: keyof AIUseCaseModule, value: string, setter: (v: string) => void) => void
|
||
type RemoveFromListFn = (field: keyof AIUseCaseModule, idx: number) => void
|
||
|
||
// =============================================================================
|
||
// TAB 1: System
|
||
// =============================================================================
|
||
|
||
interface Tab1SystemProps {
|
||
module: AIUseCaseModule
|
||
update: UpdateFn
|
||
typeInfo: typeof AI_USE_CASE_TYPES[keyof typeof AI_USE_CASE_TYPES]
|
||
}
|
||
|
||
export function Tab1System({ module, update, typeInfo }: Tab1SystemProps) {
|
||
return (
|
||
<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
|
||
// =============================================================================
|
||
|
||
interface Tab2DataProps {
|
||
module: AIUseCaseModule
|
||
update: UpdateFn
|
||
newCategory: string
|
||
setNewCategory: (v: string) => void
|
||
newOutputCategory: string
|
||
setNewOutputCategory: (v: string) => void
|
||
newSubject: string
|
||
setNewSubject: (v: string) => void
|
||
addToList: AddToListFn
|
||
removeFromList: RemoveFromListFn
|
||
}
|
||
|
||
export function Tab2Data({
|
||
module, update,
|
||
newCategory, setNewCategory,
|
||
newOutputCategory, setNewOutputCategory,
|
||
newSubject, setNewSubject,
|
||
addToList, removeFromList,
|
||
}: Tab2DataProps) {
|
||
return (
|
||
<div className="space-y-4">
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
)
|
||
}
|