Extract the monolithic company-profile wizard into _components/ and _hooks/ following Next.js 15 conventions from AGENTS.typescript.md: - _components/constants.ts: wizard steps, legal forms, industries, certifications - _components/types.ts: local interfaces (ProcessingActivity, AISystem, etc.) - _components/activity-data.ts: DSGVO data categories, department/activity templates - _components/ai-system-data.ts: AI system template catalog - _components/StepBasicInfo.tsx: step 1 (company name, legal form, industry) - _components/StepBusinessModel.tsx: step 2 (B2B/B2C, offerings) - _components/StepCompanySize.tsx: step 3 (size, revenue) - _components/StepLocations.tsx: step 4 (headquarters, target markets) - _components/StepDataProtection.tsx: step 5 (DSGVO roles, DPO) - _components/StepProcessing.tsx: processing activities with category checkboxes - _components/StepAISystems.tsx: AI system inventory - _components/StepLegalFramework.tsx: certifications and contacts - _components/StepMachineBuilder.tsx: machine builder profile (step 7) - _components/ProfileSummary.tsx: completion summary view - _hooks/useCompanyProfileForm.ts: form state, auto-save, navigation logic - page.tsx: thin orchestrator (160 LOC), imports and composes sections All 16 files are under 500 LOC (largest: StepProcessing at 343). Build verified: npx next build passes cleanly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
208 lines
13 KiB
TypeScript
208 lines
13 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { CompanyProfile } from '@/lib/sdk/types'
|
|
import { AISystem, AISystemTemplate } from './types'
|
|
import { AI_SYSTEM_TEMPLATES } from './ai-system-data'
|
|
|
|
export function StepAISystems({
|
|
data,
|
|
onChange,
|
|
}: {
|
|
data: Partial<CompanyProfile> & { aiSystems?: AISystem[] }
|
|
onChange: (updates: Record<string, unknown>) => void
|
|
}) {
|
|
const aiSystems: AISystem[] = (data as any).aiSystems || []
|
|
const [expandedSystem, setExpandedSystem] = useState<string | null>(null)
|
|
const [collapsedCategories, setCollapsedCategories] = useState<Set<string>>(new Set())
|
|
|
|
const activeIds = new Set(aiSystems.map(a => a.id))
|
|
|
|
const toggleTemplateSystem = (template: AISystemTemplate) => {
|
|
if (activeIds.has(template.id)) {
|
|
onChange({ aiSystems: aiSystems.filter(a => a.id !== template.id) })
|
|
if (expandedSystem === template.id) setExpandedSystem(null)
|
|
} else {
|
|
const newSystem: AISystem = {
|
|
id: template.id, name: template.name, vendor: template.vendor,
|
|
purpose: template.typicalPurposes.join(', '), purposes: [],
|
|
processes_personal_data: template.processes_personal_data_likely, isCustom: false,
|
|
}
|
|
onChange({ aiSystems: [...aiSystems, newSystem] })
|
|
setExpandedSystem(template.id)
|
|
}
|
|
}
|
|
|
|
const updateAISystem = (id: string, updates: Partial<AISystem>) => {
|
|
onChange({ aiSystems: aiSystems.map(a => a.id === id ? { ...a, ...updates } : a) })
|
|
}
|
|
|
|
const togglePurpose = (systemId: string, purpose: string) => {
|
|
const system = aiSystems.find(a => a.id === systemId)
|
|
if (!system) return
|
|
const purposes = system.purposes || []
|
|
const updated = purposes.includes(purpose) ? purposes.filter(p => p !== purpose) : [...purposes, purpose]
|
|
updateAISystem(systemId, { purposes: updated, purpose: updated.join(', ') })
|
|
}
|
|
|
|
const addCustomSystem = () => {
|
|
const id = `custom_ai_${Date.now()}`
|
|
onChange({ aiSystems: [...aiSystems, { id, name: '', vendor: '', purpose: '', processes_personal_data: false, isCustom: true }] })
|
|
setExpandedSystem(id)
|
|
}
|
|
|
|
const removeSystem = (id: string) => {
|
|
onChange({ aiSystems: aiSystems.filter(a => a.id !== id) })
|
|
if (expandedSystem === id) setExpandedSystem(null)
|
|
}
|
|
|
|
const toggleCategoryCollapse = (category: string) => {
|
|
setCollapsedCategories(prev => { const next = new Set(prev); if (next.has(category)) next.delete(category); else next.add(category); return next })
|
|
}
|
|
|
|
const categoryActiveCount = (systems: AISystemTemplate[]) => systems.filter(s => activeIds.has(s.id)).length
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-700 mb-1">KI-Systeme im Einsatz</h3>
|
|
<p className="text-xs text-gray-500 mb-4">
|
|
Waehlen Sie die KI-Systeme aus, die in Ihrem Unternehmen eingesetzt werden. Dies dient der Erfassung fuer den EU AI Act und die DSGVO-Dokumentation.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{AI_SYSTEM_TEMPLATES.map(group => {
|
|
const isCollapsed = collapsedCategories.has(group.category)
|
|
const activeCount = categoryActiveCount(group.systems)
|
|
|
|
return (
|
|
<div key={group.category} className="border border-gray-200 rounded-lg overflow-hidden">
|
|
<button type="button" onClick={() => toggleCategoryCollapse(group.category)} className="w-full flex items-center gap-3 px-4 py-3 bg-gray-50 hover:bg-gray-100 transition-colors text-left">
|
|
<span className="text-base">{group.icon}</span>
|
|
<span className="text-sm font-medium text-gray-900 flex-1">{group.category}</span>
|
|
{activeCount > 0 && <span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full">{activeCount} aktiv</span>}
|
|
<svg className={`w-4 h-4 text-gray-400 transition-transform ${isCollapsed ? '' : 'rotate-180'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{!isCollapsed && (
|
|
<div className="p-3 space-y-2">
|
|
{group.systems.map(template => {
|
|
const isActive = activeIds.has(template.id)
|
|
const system = aiSystems.find(a => a.id === template.id)
|
|
const isExpanded = expandedSystem === template.id
|
|
|
|
return (
|
|
<div key={template.id}>
|
|
<div
|
|
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${isActive ? 'border-purple-500 bg-purple-50' : 'border-gray-100 hover:border-purple-300'}`}
|
|
onClick={() => { if (!isActive) { toggleTemplateSystem(template) } else { setExpandedSystem(isExpanded ? null : template.id) } }}
|
|
>
|
|
<input type="checkbox" checked={isActive} onChange={e => { e.stopPropagation(); toggleTemplateSystem(template) }} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-gray-900">{template.name}</div>
|
|
<p className="text-xs text-gray-500">{template.vendor}</p>
|
|
</div>
|
|
{isActive && (
|
|
<svg className={`w-4 h-4 text-gray-400 transition-transform ${isExpanded ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
)}
|
|
</div>
|
|
|
|
{isActive && isExpanded && system && (
|
|
<div className="ml-4 mt-2 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-600 mb-2">Einsatzzweck</label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{template.typicalPurposes.map(purpose => (
|
|
<button key={purpose} type="button" onClick={() => togglePurpose(template.id, purpose)}
|
|
className={`px-3 py-1.5 text-xs rounded-full border transition-all ${(system.purposes || []).includes(purpose) ? 'bg-purple-100 border-purple-300 text-purple-700' : 'bg-white border-gray-200 text-gray-600 hover:border-purple-200'}`}>
|
|
{purpose}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<input type="text" value={system.notes || ''} onChange={e => updateAISystem(template.id, { notes: e.target.value })} placeholder="Weitere Einsatzzwecke / Anmerkungen..." className="mt-2 w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
</div>
|
|
|
|
{template.dataWarning && (
|
|
<div className={`flex items-start gap-2 px-3 py-2 rounded-lg ${template.dataWarning.includes('EU') || template.dataWarning.includes('Deutscher Anbieter') || template.dataWarning.includes('NICHT') ? 'bg-blue-50 border border-blue-200' : 'bg-amber-50 border border-amber-200'}`}>
|
|
<span className="text-sm mt-0.5">{template.dataWarning.includes('EU') || template.dataWarning.includes('Deutscher Anbieter') ? '\u2139\uFE0F' : '\u26A0\uFE0F'}</span>
|
|
<span className="text-xs text-gray-800">{template.dataWarning}</span>
|
|
</div>
|
|
)}
|
|
|
|
<label className="flex items-center gap-2 px-1 cursor-pointer">
|
|
<input type="checkbox" checked={system.processes_personal_data} onChange={e => updateAISystem(template.id, { processes_personal_data: e.target.checked })} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500" />
|
|
<span className="text-sm text-gray-700">Verarbeitet personenbezogene Daten</span>
|
|
</label>
|
|
|
|
<button type="button" onClick={() => removeSystem(template.id)} className="text-xs text-red-500 hover:text-red-700">KI-System entfernen</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{aiSystems.filter(a => a.isCustom).map(system => (
|
|
<div key={system.id} className="mt-2">
|
|
<div className="flex items-center gap-3 p-3 rounded-lg border-2 border-purple-500 bg-purple-50 cursor-pointer" onClick={() => setExpandedSystem(expandedSystem === system.id ? null : system.id)}>
|
|
<span className="w-4 h-4 flex items-center justify-center text-purple-600 flex-shrink-0 text-sm">+</span>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-sm font-medium text-gray-900">{system.name || 'Neues KI-System'}</span>
|
|
{system.vendor && <span className="text-xs text-gray-500 ml-2">({system.vendor})</span>}
|
|
</div>
|
|
<svg className={`w-4 h-4 text-gray-400 transition-transform ${expandedSystem === system.id ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</div>
|
|
{expandedSystem === system.id && (
|
|
<div className="ml-4 mt-2 p-4 bg-gray-50 rounded-lg border border-gray-200 space-y-3">
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<input type="text" value={system.name} onChange={e => updateAISystem(system.id, { name: e.target.value })} placeholder="Name (z.B. ChatGPT, Copilot)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<input type="text" value={system.vendor} onChange={e => updateAISystem(system.id, { vendor: e.target.value })} placeholder="Anbieter (z.B. OpenAI, Microsoft)" className="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
</div>
|
|
<input type="text" value={system.purpose} onChange={e => updateAISystem(system.id, { purpose: e.target.value })} placeholder="Einsatzzweck (z.B. Kundensupport, Code-Assistenz)" className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
|
|
<label className="flex items-center gap-2 px-1 cursor-pointer">
|
|
<input type="checkbox" checked={system.processes_personal_data} onChange={e => updateAISystem(system.id, { processes_personal_data: e.target.checked })} className="w-4 h-4 text-purple-600 rounded focus:ring-purple-500" />
|
|
<span className="text-sm text-gray-700">Verarbeitet personenbezogene Daten</span>
|
|
</label>
|
|
<button type="button" onClick={() => removeSystem(system.id)} className="text-xs text-red-500 hover:text-red-700">KI-System entfernen</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
<button type="button" onClick={addCustomSystem} className="w-full mt-3 px-3 py-2 text-sm text-purple-700 bg-purple-50 border-2 border-dashed border-purple-200 rounded-lg hover:bg-purple-100 hover:border-purple-300 transition-colors">
|
|
+ Eigenes KI-System hinzufuegen
|
|
</button>
|
|
|
|
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<div className="flex items-start gap-3">
|
|
<span className="text-lg">{'\u2139\uFE0F'}</span>
|
|
<div>
|
|
<h4 className="text-sm font-medium text-blue-900 mb-1">AI Act Risikoeinstufung</h4>
|
|
<p className="text-xs text-blue-800 mb-3">
|
|
Die detaillierte Risikoeinstufung Ihrer KI-Systeme nach EU AI Act (verboten / hochriskant / begrenzt / minimal) erfolgt automatisch im AI-Act-Modul.
|
|
</p>
|
|
<a href="/sdk/ai-act" className="inline-flex items-center gap-1 text-sm font-medium text-blue-700 hover:text-blue-900">
|
|
Zum AI-Act-Modul
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|