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>
188 lines
7.3 KiB
TypeScript
188 lines
7.3 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import {
|
|
AIUseCaseModule,
|
|
AI_USE_CASE_TYPES,
|
|
PRIVACY_BY_DESIGN_CATEGORIES,
|
|
PrivacyByDesignCategory,
|
|
PrivacyByDesignMeasure,
|
|
AIModuleReviewTrigger,
|
|
AIModuleReviewTriggerType,
|
|
checkArt22Applicability,
|
|
} from '@/lib/sdk/dsfa/ai-use-case-types'
|
|
import { TABS, REVIEW_TRIGGER_TYPES } from './AIUseCaseEditorConstants'
|
|
import {
|
|
Tab1System,
|
|
Tab2Data,
|
|
Tab3Purpose,
|
|
Tab4AIAct,
|
|
Tab5Risks,
|
|
Tab6PrivacyByDesign,
|
|
Tab7Review,
|
|
} from './AIUseCaseEditorTabs'
|
|
|
|
interface AIUseCaseModuleEditorProps {
|
|
module: AIUseCaseModule
|
|
onSave: (module: AIUseCaseModule) => void
|
|
onCancel: () => void
|
|
isSaving?: boolean
|
|
}
|
|
|
|
export function AIUseCaseModuleEditor({ module: initialModule, onSave, onCancel, isSaving }: AIUseCaseModuleEditorProps) {
|
|
const [activeTab, setActiveTab] = useState(1)
|
|
const [module, setModule] = useState<AIUseCaseModule>(initialModule)
|
|
const [newCategory, setNewCategory] = useState('')
|
|
const [newOutputCategory, setNewOutputCategory] = useState('')
|
|
const [newSubject, setNewSubject] = useState('')
|
|
|
|
const typeInfo = AI_USE_CASE_TYPES[module.use_case_type]
|
|
const art22Required = checkArt22Applicability(module)
|
|
|
|
const update = (updates: Partial<AIUseCaseModule>) => {
|
|
setModule(prev => ({ ...prev, ...updates }))
|
|
}
|
|
|
|
const addToList = (field: keyof AIUseCaseModule, value: string, setter: (v: string) => void) => {
|
|
if (!value.trim()) return
|
|
const current = (module[field] as string[]) || []
|
|
update({ [field]: [...current, value.trim()] } as Partial<AIUseCaseModule>)
|
|
setter('')
|
|
}
|
|
|
|
const removeFromList = (field: keyof AIUseCaseModule, idx: number) => {
|
|
const current = (module[field] as string[]) || []
|
|
update({ [field]: current.filter((_, i) => i !== idx) } as Partial<AIUseCaseModule>)
|
|
}
|
|
|
|
const togglePbdMeasure = (category: PrivacyByDesignCategory) => {
|
|
const existing = module.privacy_by_design_measures || []
|
|
const found = existing.find(m => m.category === category)
|
|
if (found) {
|
|
update({ privacy_by_design_measures: existing.map(m =>
|
|
m.category === category ? { ...m, implemented: !m.implemented } : m
|
|
)})
|
|
} else {
|
|
const newMeasure: PrivacyByDesignMeasure = {
|
|
category,
|
|
description: PRIVACY_BY_DESIGN_CATEGORIES[category].description,
|
|
implemented: true,
|
|
}
|
|
update({ privacy_by_design_measures: [...existing, newMeasure] })
|
|
}
|
|
}
|
|
|
|
const toggleReviewTrigger = (type: AIModuleReviewTriggerType) => {
|
|
const existing = module.review_triggers || []
|
|
const found = existing.find(t => t.type === type)
|
|
if (found) {
|
|
update({ review_triggers: existing.filter(t => t.type !== type) })
|
|
} else {
|
|
const label = REVIEW_TRIGGER_TYPES.find(rt => rt.value === type)?.label || type
|
|
const newTrigger: AIModuleReviewTrigger = { type, description: label }
|
|
update({ review_triggers: [...existing, newTrigger] })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col overflow-hidden">
|
|
{/* Header */}
|
|
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between flex-shrink-0">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">{typeInfo.icon}</span>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">{module.name || typeInfo.label}</h2>
|
|
<p className="text-xs text-gray-500">{typeInfo.label} — KI-Anwendungsfall-Anhang</p>
|
|
</div>
|
|
</div>
|
|
<button onClick={onCancel} className="text-gray-400 hover:text-gray-600">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tab Bar */}
|
|
<div className="flex gap-1 px-4 py-2 border-b border-gray-200 overflow-x-auto flex-shrink-0">
|
|
{TABS.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${
|
|
activeTab === tab.id
|
|
? 'bg-purple-100 text-purple-700'
|
|
: 'text-gray-600 hover:bg-gray-100'
|
|
}`}
|
|
>
|
|
<span>{tab.icon}</span>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 overflow-y-auto p-6">
|
|
{activeTab === 1 && <Tab1System module={module} update={update} typeInfo={typeInfo} />}
|
|
{activeTab === 2 && (
|
|
<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 */}
|
|
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between flex-shrink-0">
|
|
<div className="flex gap-2">
|
|
{activeTab > 1 && (
|
|
<button
|
|
onClick={() => setActiveTab(activeTab - 1)}
|
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
← Zurück
|
|
</button>
|
|
)}
|
|
{activeTab < 7 && (
|
|
<button
|
|
onClick={() => setActiveTab(activeTab + 1)}
|
|
className="px-4 py-2 text-sm bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg transition-colors"
|
|
>
|
|
Weiter →
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={() => onSave(module)}
|
|
disabled={isSaving || !module.name || !module.model_description}
|
|
className="px-6 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors font-medium"
|
|
>
|
|
{isSaving ? 'Speichert...' : 'Modul speichern'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|