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 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
278 lines
9.4 KiB
TypeScript
278 lines
9.4 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
|
|
interface Classification {
|
|
id: string
|
|
regulation: string
|
|
regulation_label: string
|
|
classification_result: string
|
|
risk_level: string
|
|
confidence: number
|
|
reasoning: string
|
|
classified_at: string | null
|
|
}
|
|
|
|
const REGULATIONS = [
|
|
{
|
|
key: 'ai_act',
|
|
label: 'AI Act',
|
|
description: 'EU-Verordnung ueber kuenstliche Intelligenz (2024/1689)',
|
|
icon: '🤖',
|
|
},
|
|
{
|
|
key: 'machinery_regulation',
|
|
label: 'Maschinenverordnung',
|
|
description: 'EU-Maschinenverordnung (2023/1230)',
|
|
icon: '⚙️',
|
|
},
|
|
{
|
|
key: 'cra',
|
|
label: 'Cyber Resilience Act',
|
|
description: 'EU-Verordnung ueber Cyberresilienz',
|
|
icon: '🔒',
|
|
},
|
|
{
|
|
key: 'nis2',
|
|
label: 'NIS2',
|
|
description: 'Richtlinie ueber Netz- und Informationssicherheit',
|
|
icon: '🌐',
|
|
},
|
|
]
|
|
|
|
function RiskLevelBadge({ level }: { level: string }) {
|
|
const colors: Record<string, string> = {
|
|
unacceptable: 'bg-black text-white',
|
|
high: 'bg-red-100 text-red-700',
|
|
limited: 'bg-yellow-100 text-yellow-700',
|
|
minimal: 'bg-green-100 text-green-700',
|
|
not_applicable: 'bg-gray-100 text-gray-500',
|
|
critical: 'bg-red-100 text-red-700',
|
|
important: 'bg-orange-100 text-orange-700',
|
|
default_category: 'bg-blue-100 text-blue-700',
|
|
essential: 'bg-orange-100 text-orange-700',
|
|
non_essential: 'bg-green-100 text-green-700',
|
|
}
|
|
const labels: Record<string, string> = {
|
|
unacceptable: 'Unakzeptabel',
|
|
high: 'Hochrisiko',
|
|
limited: 'Begrenztes Risiko',
|
|
minimal: 'Minimales Risiko',
|
|
not_applicable: 'Nicht anwendbar',
|
|
critical: 'Kritisch',
|
|
important: 'Wichtig',
|
|
default_category: 'Standard',
|
|
essential: 'Wesentlich',
|
|
non_essential: 'Nicht wesentlich',
|
|
}
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${colors[level] || 'bg-gray-100 text-gray-700'}`}>
|
|
{labels[level] || level}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function ConfidenceBar({ value }: { value: number }) {
|
|
const pct = Math.round(value * 100)
|
|
const color = pct >= 80 ? 'bg-green-500' : pct >= 60 ? 'bg-yellow-500' : 'bg-orange-500'
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex-1 bg-gray-200 rounded-full h-1.5">
|
|
<div className={`${color} h-1.5 rounded-full`} style={{ width: `${pct}%` }} />
|
|
</div>
|
|
<span className="text-xs text-gray-500">{pct}%</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ClassificationCard({
|
|
regulation,
|
|
classification,
|
|
onReclassify,
|
|
classifying,
|
|
}: {
|
|
regulation: (typeof REGULATIONS)[number]
|
|
classification: Classification | null
|
|
onReclassify: (regKey: string) => void
|
|
classifying: boolean
|
|
}) {
|
|
return (
|
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-2xl">{regulation.icon}</span>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{regulation.label}</h3>
|
|
<p className="text-xs text-gray-500">{regulation.description}</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => onReclassify(regulation.key)}
|
|
disabled={classifying}
|
|
className="text-xs px-3 py-1.5 border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{classifying ? 'Laeuft...' : classification ? 'Neu klassifizieren' : 'Klassifizieren'}
|
|
</button>
|
|
</div>
|
|
|
|
{classification ? (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">Ergebnis</span>
|
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{classification.classification_result}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-gray-500">Risikostufe</span>
|
|
<RiskLevelBadge level={classification.risk_level} />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-gray-500">Konfidenz</span>
|
|
<ConfidenceBar value={classification.confidence} />
|
|
</div>
|
|
<div className="pt-3 border-t border-gray-100 dark:border-gray-700">
|
|
<span className="text-xs text-gray-500 block mb-1">Begruendung</span>
|
|
<p className="text-sm text-gray-700 dark:text-gray-300">{classification.reasoning}</p>
|
|
</div>
|
|
{classification.classified_at && (
|
|
<div className="text-xs text-gray-400">
|
|
Klassifiziert am: {new Date(classification.classified_at).toLocaleDateString('de-DE')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="py-8 text-center">
|
|
<div className="w-12 h-12 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-3">
|
|
<svg className="w-6 h-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-sm text-gray-500">Noch nicht klassifiziert</p>
|
|
<p className="text-xs text-gray-400 mt-1">Klicken Sie "Klassifizieren" um die Analyse zu starten</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function ClassificationPage() {
|
|
const params = useParams()
|
|
const projectId = params.projectId as string
|
|
const [classifications, setClassifications] = useState<Classification[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [classifyingAll, setClassifyingAll] = useState(false)
|
|
const [classifyingReg, setClassifyingReg] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
fetchClassifications()
|
|
}, [projectId])
|
|
|
|
async function fetchClassifications() {
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/classifications`)
|
|
if (res.ok) {
|
|
const json = await res.json()
|
|
setClassifications(json.classifications || json || [])
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch classifications:', err)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function handleClassifyAll() {
|
|
setClassifyingAll(true)
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/classifications/classify-all`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
if (res.ok) {
|
|
await fetchClassifications()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to classify all:', err)
|
|
} finally {
|
|
setClassifyingAll(false)
|
|
}
|
|
}
|
|
|
|
async function handleReclassify(regKey: string) {
|
|
setClassifyingReg(regKey)
|
|
try {
|
|
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/classifications/${regKey}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
})
|
|
if (res.ok) {
|
|
await fetchClassifications()
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to reclassify:', err)
|
|
} finally {
|
|
setClassifyingReg(null)
|
|
}
|
|
}
|
|
|
|
function getClassificationForReg(regKey: string): Classification | null {
|
|
return classifications.find((c) => c.regulation === regKey) || null
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Regulatorische Klassifikation</h1>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
Automatische Einordnung Ihrer Maschine/Anlage in die relevanten EU-Regulierungsrahmen.
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={handleClassifyAll}
|
|
disabled={classifyingAll}
|
|
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{classifyingAll ? (
|
|
<>
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
|
|
Wird klassifiziert...
|
|
</>
|
|
) : (
|
|
<>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
Alle klassifizieren
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Classification Cards */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{REGULATIONS.map((reg) => (
|
|
<ClassificationCard
|
|
key={reg.key}
|
|
regulation={reg}
|
|
classification={getClassificationForReg(reg.key)}
|
|
onReclassify={handleReclassify}
|
|
classifying={classifyingReg === reg.key || classifyingAll}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|