All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 38s
CI / test-python-backend-compliance (push) Successful in 33s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s
- Migration 028: ai_use_case_modules JSONB + section_8_complete auf compliance_dsfas - Neues ai-use-case-types.ts: AIUseCaseModule Interface, 8 Typen, Art22Assessment, AI Act Risikoklassen, WP248-Kriterien, Privacy by Design, createEmptyModule() Helper - types.ts: Section 8 in DSFA_SECTIONS, ai_use_case_modules im DSFA Interface, section_8_complete in DSFASectionProgress - api.ts: addAIUseCaseModule, updateAIUseCaseModule, removeAIUseCaseModule - 5 neue UI-Komponenten: AIUseCaseTypeSelector, Art22AssessmentPanel, AIRiskCriteriaChecklist, AIUseCaseModuleEditor (7 Tabs), AIUseCaseSection - DSFASidebar: Section 8 Eintrag + calculateSectionProgress case 8 - ReviewScheduleSection: ai_use_case_module Trigger-Typ ergänzt - page.tsx: Section 8 Rendering + Weiter-Button auf activeSection < 8 + KI-Module Counter - scripts/ingest-dsfa-bundesland.sh: WP248 + alle 17 Behörden → bp_dsfa_corpus - Docs: dsfa.md Section 8 + RAG-Corpus, Developer Portal DSFA mit AI-Modul-Code-Beispielen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
130 lines
5.1 KiB
TypeScript
130 lines
5.1 KiB
TypeScript
'use client'
|
||
|
||
import React from 'react'
|
||
import { AI_RISK_CRITERIA, AIUseCaseRiskCriterion } from '@/lib/sdk/dsfa/ai-use-case-types'
|
||
|
||
interface AIRiskCriteriaChecklistProps {
|
||
criteria: AIUseCaseRiskCriterion[]
|
||
onChange: (updated: AIUseCaseRiskCriterion[]) => void
|
||
readonly?: boolean
|
||
}
|
||
|
||
const SEVERITY_LABELS: Record<string, string> = {
|
||
low: 'Niedrig',
|
||
medium: 'Mittel',
|
||
high: 'Hoch',
|
||
}
|
||
|
||
const SEVERITY_COLORS: Record<string, string> = {
|
||
low: 'bg-green-100 text-green-700 border-green-200',
|
||
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||
high: 'bg-red-100 text-red-700 border-red-200',
|
||
}
|
||
|
||
export function AIRiskCriteriaChecklist({ criteria, onChange, readonly }: AIRiskCriteriaChecklistProps) {
|
||
const appliedCount = criteria.filter(c => c.applies).length
|
||
|
||
const updateCriterion = (id: string, updates: Partial<AIUseCaseRiskCriterion>) => {
|
||
onChange(criteria.map(c => c.id === id ? { ...c, ...updates } : c))
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
{/* Summary Banner */}
|
||
{appliedCount > 0 && (
|
||
<div className={`p-3 rounded-lg border text-sm ${
|
||
appliedCount >= 3
|
||
? 'bg-red-50 border-red-200 text-red-700'
|
||
: appliedCount >= 2
|
||
? 'bg-orange-50 border-orange-200 text-orange-700'
|
||
: 'bg-yellow-50 border-yellow-200 text-yellow-700'
|
||
}`}>
|
||
{appliedCount} von 6 Risikokriterien erfüllt
|
||
{appliedCount >= 2 && ' – DSFA ist erforderlich'}
|
||
{appliedCount >= 4 && ' – behördliche Konsultation prüfen'}
|
||
</div>
|
||
)}
|
||
|
||
{/* Criteria Cards */}
|
||
<div className="space-y-2">
|
||
{AI_RISK_CRITERIA.map(critDef => {
|
||
const criterion = criteria.find(c => c.id === critDef.id) || {
|
||
id: critDef.id,
|
||
applies: false,
|
||
severity: critDef.default_severity,
|
||
}
|
||
|
||
return (
|
||
<div
|
||
key={critDef.id}
|
||
className={`rounded-xl border p-4 transition-all ${
|
||
criterion.applies
|
||
? 'border-red-300 bg-red-50'
|
||
: 'border-gray-200 bg-white'
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
{/* Checkbox */}
|
||
<input
|
||
type="checkbox"
|
||
checked={criterion.applies}
|
||
onChange={e => updateCriterion(critDef.id, { applies: e.target.checked })}
|
||
disabled={readonly}
|
||
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||
/>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className={`font-medium text-sm ${criterion.applies ? 'text-red-800' : 'text-gray-900'}`}>
|
||
{critDef.label}
|
||
</span>
|
||
<span className={`text-[10px] px-1.5 py-0.5 rounded border ${SEVERITY_COLORS[criterion.severity]}`}>
|
||
{SEVERITY_LABELS[criterion.severity]}
|
||
</span>
|
||
<span className="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-500 rounded font-mono">
|
||
{critDef.gdpr_ref.split(',')[0]}
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-gray-500 mt-0.5">{critDef.description}</p>
|
||
|
||
{/* Justification (shown when applies) */}
|
||
{criterion.applies && (
|
||
<div className="mt-3 space-y-2">
|
||
<textarea
|
||
value={criterion.justification || ''}
|
||
onChange={e => updateCriterion(critDef.id, { justification: e.target.value })}
|
||
disabled={readonly}
|
||
rows={2}
|
||
placeholder="Begründung, warum dieses Kriterium zutrifft..."
|
||
className="w-full px-3 py-2 text-xs border border-red-200 rounded-lg bg-white focus:ring-2 focus:ring-red-400 focus:border-red-400 resize-none"
|
||
/>
|
||
|
||
{/* Severity Override */}
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-gray-500">Schwere:</span>
|
||
{(['low', 'medium', 'high'] as const).map(sev => (
|
||
<button
|
||
key={sev}
|
||
onClick={() => !readonly && updateCriterion(critDef.id, { severity: sev })}
|
||
className={`text-xs px-2 py-0.5 rounded border transition-colors ${
|
||
criterion.severity === sev
|
||
? SEVERITY_COLORS[sev]
|
||
: 'bg-white text-gray-500 border-gray-200 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{SEVERITY_LABELS[sev]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|