feat(admin-v2): Major SDK/Compliance overhaul and new modules
SDK modules added/enhanced: - compliance-hub, compliance-scope, consent-management, notfallplan - audit-report, workflow, source-policy, dsms - advisory-board documentation section - TOM dashboard components, TOM generator SDM mapping - DSFA: mitigation library, risk catalog, threshold analysis, source attribution - VVT: baseline catalog, profiling engine, types - Loeschfristen: baseline catalog, compliance engine, export, profiling, types - Compliance scope: engine, profiling, golden tests, types Existing SDK pages updated: - dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality - SDKSidebar, StepHeader — new navigation items and layout - SDK layout, context, types — expanded type system Other admin-v2 changes: - AI agents page, RAG pipeline DSFA integration - GridOverlay component updates - Companion feature (development + education) - Compliance advisor SOUL definition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
320
admin-v2/components/sdk/dsfa/SourceAttribution.tsx
Normal file
320
admin-v2/components/sdk/dsfa/SourceAttribution.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { BookOpen, ExternalLink, Scale, ChevronDown, ChevronUp, Info } from 'lucide-react'
|
||||
import {
|
||||
DSFALicenseCode,
|
||||
DSFA_LICENSE_LABELS,
|
||||
SourceAttributionProps
|
||||
} from '@/lib/sdk/types'
|
||||
|
||||
/**
|
||||
* Get license badge color based on license type
|
||||
*/
|
||||
function getLicenseBadgeColor(licenseCode: DSFALicenseCode): string {
|
||||
switch (licenseCode) {
|
||||
case 'DL-DE-BY-2.0':
|
||||
case 'DL-DE-ZERO-2.0':
|
||||
return 'bg-blue-100 text-blue-700 border-blue-200'
|
||||
case 'CC-BY-4.0':
|
||||
return 'bg-green-100 text-green-700 border-green-200'
|
||||
case 'EDPB-LICENSE':
|
||||
return 'bg-purple-100 text-purple-700 border-purple-200'
|
||||
case 'PUBLIC_DOMAIN':
|
||||
return 'bg-gray-100 text-gray-700 border-gray-200'
|
||||
case 'PROPRIETARY':
|
||||
return 'bg-amber-100 text-amber-700 border-amber-200'
|
||||
default:
|
||||
return 'bg-slate-100 text-slate-700 border-slate-200'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license URL based on license code
|
||||
*/
|
||||
function getLicenseUrl(licenseCode: DSFALicenseCode): string | null {
|
||||
switch (licenseCode) {
|
||||
case 'DL-DE-BY-2.0':
|
||||
return 'https://www.govdata.de/dl-de/by-2-0'
|
||||
case 'DL-DE-ZERO-2.0':
|
||||
return 'https://www.govdata.de/dl-de/zero-2-0'
|
||||
case 'CC-BY-4.0':
|
||||
return 'https://creativecommons.org/licenses/by/4.0/'
|
||||
case 'EDPB-LICENSE':
|
||||
return 'https://edpb.europa.eu/about-edpb/legal-notice_en'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* License badge component
|
||||
*/
|
||||
function LicenseBadge({ licenseCode }: { licenseCode: DSFALicenseCode }) {
|
||||
const licenseUrl = getLicenseUrl(licenseCode)
|
||||
const colorClass = getLicenseBadgeColor(licenseCode)
|
||||
const label = DSFA_LICENSE_LABELS[licenseCode] || licenseCode
|
||||
|
||||
if (licenseUrl) {
|
||||
return (
|
||||
<a
|
||||
href={licenseUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs border ${colorClass} hover:opacity-80 transition-opacity`}
|
||||
>
|
||||
<Scale className="w-3 h-3" />
|
||||
{label}
|
||||
<ExternalLink className="w-2.5 h-2.5" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs border ${colorClass}`}>
|
||||
<Scale className="w-3 h-3" />
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Single source item in the attribution list
|
||||
*/
|
||||
function SourceItem({
|
||||
source,
|
||||
index,
|
||||
showScore
|
||||
}: {
|
||||
source: SourceAttributionProps['sources'][0]
|
||||
index: number
|
||||
showScore: boolean
|
||||
}) {
|
||||
return (
|
||||
<li className="text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="text-slate-400 font-mono text-xs mt-0.5 min-w-[1.5rem]">
|
||||
{index + 1}.
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{source.sourceUrl ? (
|
||||
<a
|
||||
href={source.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800 hover:underline font-medium truncate"
|
||||
>
|
||||
{source.sourceName}
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-slate-700 font-medium truncate">
|
||||
{source.sourceName}
|
||||
</span>
|
||||
)}
|
||||
{showScore && source.score !== undefined && (
|
||||
<span className="text-xs text-slate-400 font-mono">
|
||||
({(source.score * 100).toFixed(0)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">
|
||||
{source.attributionText}
|
||||
</p>
|
||||
<div className="mt-1.5">
|
||||
<LicenseBadge licenseCode={source.licenseCode} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact source badge for inline display
|
||||
*/
|
||||
function CompactSourceBadge({
|
||||
source
|
||||
}: {
|
||||
source: SourceAttributionProps['sources'][0]
|
||||
}) {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-slate-100 text-slate-600 border border-slate-200">
|
||||
<BookOpen className="w-3 h-3" />
|
||||
{source.sourceCode}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* SourceAttribution component - displays source/license information for DSFA RAG results
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <SourceAttribution
|
||||
* sources={[
|
||||
* {
|
||||
* sourceCode: "WP248",
|
||||
* sourceName: "WP248 rev.01 - Leitlinien zur DSFA",
|
||||
* attributionText: "Quelle: WP248 rev.01, Artikel-29-Datenschutzgruppe (2017)",
|
||||
* licenseCode: "EDPB-LICENSE",
|
||||
* sourceUrl: "https://ec.europa.eu/newsroom/article29/items/611236/en",
|
||||
* score: 0.87
|
||||
* }
|
||||
* ]}
|
||||
* showScores
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function SourceAttribution({
|
||||
sources,
|
||||
compact = false,
|
||||
showScores = false
|
||||
}: SourceAttributionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(!compact)
|
||||
|
||||
if (!sources || sources.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Compact mode - just show badges
|
||||
if (compact && !isExpanded) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
className="inline-flex items-center gap-1 text-xs text-slate-500 hover:text-slate-700"
|
||||
>
|
||||
<Info className="w-3 h-3" />
|
||||
Quellen ({sources.length})
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
{sources.slice(0, 3).map((source, i) => (
|
||||
<CompactSourceBadge key={i} source={source} />
|
||||
))}
|
||||
{sources.length > 3 && (
|
||||
<span className="text-xs text-slate-400">+{sources.length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mt-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium text-slate-700 flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Quellen & Lizenzen
|
||||
</h4>
|
||||
{compact && (
|
||||
<button
|
||||
onClick={() => setIsExpanded(false)}
|
||||
className="text-xs text-slate-400 hover:text-slate-600 flex items-center gap-1"
|
||||
>
|
||||
Einklappen
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="mt-3 space-y-3">
|
||||
{sources.map((source, i) => (
|
||||
<SourceItem
|
||||
key={i}
|
||||
source={source}
|
||||
index={i}
|
||||
showScore={showScores}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* Aggregated license notice */}
|
||||
{sources.length > 1 && (
|
||||
<div className="mt-4 pt-3 border-t border-slate-200">
|
||||
<p className="text-xs text-slate-500">
|
||||
<strong>Hinweis:</strong> Die angezeigten Inhalte stammen aus {sources.length} verschiedenen Quellen
|
||||
mit unterschiedlichen Lizenzen. Bitte beachten Sie die jeweiligen Attributionsanforderungen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline source reference for use within text
|
||||
*/
|
||||
export function InlineSourceRef({
|
||||
sourceCode,
|
||||
sourceName,
|
||||
sourceUrl
|
||||
}: {
|
||||
sourceCode: string
|
||||
sourceName: string
|
||||
sourceUrl?: string
|
||||
}) {
|
||||
if (sourceUrl) {
|
||||
return (
|
||||
<a
|
||||
href={sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-0.5 text-blue-600 hover:text-blue-800 text-sm"
|
||||
title={sourceName}
|
||||
>
|
||||
[{sourceCode}]
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-slate-600 text-sm" title={sourceName}>
|
||||
[{sourceCode}]
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribution footer for generated documents
|
||||
*/
|
||||
export function AttributionFooter({
|
||||
sources,
|
||||
generatedAt
|
||||
}: {
|
||||
sources: SourceAttributionProps['sources']
|
||||
generatedAt?: Date
|
||||
}) {
|
||||
if (!sources || sources.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Group by license
|
||||
const byLicense = sources.reduce((acc, source) => {
|
||||
const key = source.licenseCode
|
||||
if (!acc[key]) acc[key] = []
|
||||
acc[key].push(source)
|
||||
return acc
|
||||
}, {} as Record<string, typeof sources>)
|
||||
|
||||
return (
|
||||
<footer className="mt-8 pt-4 border-t border-slate-200 text-xs text-slate-500">
|
||||
<h5 className="font-medium text-slate-600 mb-2">Quellennachweis</h5>
|
||||
<ul className="space-y-1">
|
||||
{Object.entries(byLicense).map(([licenseCode, licenseSources]) => (
|
||||
<li key={licenseCode}>
|
||||
<span className="font-medium">{DSFA_LICENSE_LABELS[licenseCode as DSFALicenseCode]}:</span>{' '}
|
||||
{licenseSources.map(s => s.sourceName).join(', ')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{generatedAt && (
|
||||
<p className="mt-2 text-slate-400">
|
||||
Generiert am {generatedAt.toLocaleDateString('de-DE')} um {generatedAt.toLocaleTimeString('de-DE')}
|
||||
</p>
|
||||
)}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default SourceAttribution
|
||||
@@ -201,6 +201,62 @@ export function ThresholdAnalysisSection({ dsfa, onUpdate, isSubmitting }: Thres
|
||||
{wp248Result.reason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Annex-Trigger: Empfehlung bei >= 2 WP248 Kriterien */}
|
||||
{wp248Selected.length >= 2 && (
|
||||
<div className="mt-4 p-4 rounded-xl border bg-indigo-50 border-indigo-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<svg className="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-indigo-800 text-sm">Annex mit separater Risikobewertung empfohlen</p>
|
||||
<p className="text-sm text-indigo-700 mt-1">
|
||||
Bei {wp248Selected.length} erfuellten WP248-Kriterien wird ein Annex mit detaillierter Risikobewertung empfohlen.
|
||||
</p>
|
||||
<div className="mt-2">
|
||||
<p className="text-xs font-medium text-indigo-700 mb-1">Vorgeschlagene Annex-Scopes basierend auf Ihren Kriterien:</p>
|
||||
<ul className="text-xs text-indigo-600 space-y-1">
|
||||
{wp248Selected.includes('scoring_profiling') && (
|
||||
<li>- Annex: Profiling & Scoring — Detailanalyse der Bewertungslogik</li>
|
||||
)}
|
||||
{wp248Selected.includes('automated_decision') && (
|
||||
<li>- Annex: Automatisierte Einzelentscheidung — Art. 22 Pruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('systematic_monitoring') && (
|
||||
<li>- Annex: Systematische Ueberwachung — Verhaeltnismaessigkeitspruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('sensitive_data') && (
|
||||
<li>- Annex: Besondere Datenkategorien — Schutzbedarfsanalyse Art. 9</li>
|
||||
)}
|
||||
{wp248Selected.includes('large_scale') && (
|
||||
<li>- Annex: Umfangsanalyse — Quantitative Bewertung der Verarbeitung</li>
|
||||
)}
|
||||
{wp248Selected.includes('matching_combining') && (
|
||||
<li>- Annex: Datenzusammenfuehrung — Zweckbindungspruefung</li>
|
||||
)}
|
||||
{wp248Selected.includes('vulnerable_subjects') && (
|
||||
<li>- Annex: Schutzbeduerftige Betroffene — Verstaerkte Schutzmassnahmen</li>
|
||||
)}
|
||||
{wp248Selected.includes('innovative_technology') && (
|
||||
<li>- Annex: Innovative Technologie — Technikfolgenabschaetzung</li>
|
||||
)}
|
||||
{wp248Selected.includes('preventing_rights') && (
|
||||
<li>- Annex: Rechteausuebung — Barrierefreiheit der Betroffenenrechte</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
{aiTriggersSelected.length > 0 && (
|
||||
<p className="text-xs text-indigo-500 mt-2">
|
||||
+ KI-Trigger aktiv: Zusaetzlicher Annex fuer KI-Risikobewertung empfohlen (AI Act Konformitaet).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step 2: Art. 35 Abs. 3 Cases */}
|
||||
|
||||
@@ -11,6 +11,7 @@ export { DSFASidebar } from './DSFASidebar'
|
||||
export { StakeholderConsultationSection } from './StakeholderConsultationSection'
|
||||
export { Art36Warning } from './Art36Warning'
|
||||
export { ReviewScheduleSection } from './ReviewScheduleSection'
|
||||
export { SourceAttribution, InlineSourceRef, AttributionFooter } from './SourceAttribution'
|
||||
|
||||
// =============================================================================
|
||||
// DSFA Card Component
|
||||
|
||||
Reference in New Issue
Block a user