Extract components and hooks from oversized page files (563/561/520 LOC) into colocated _components/ and _hooks/ subdirectories. All three page.tsx files are now thin orchestrators under 300 LOC each (dsfa: 216, audit-llm: 121, quality: 163). Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
'use client'
|
|
|
|
export interface QualityMetric {
|
|
id: string
|
|
name: string
|
|
category: 'accuracy' | 'fairness' | 'robustness' | 'explainability' | 'performance'
|
|
score: number
|
|
threshold: number
|
|
trend: 'up' | 'down' | 'stable'
|
|
last_measured: string
|
|
ai_system: string | null
|
|
}
|
|
|
|
export function MetricCard({ metric, onEdit }: { metric: QualityMetric; onEdit: (m: QualityMetric) => void }) {
|
|
const isAboveThreshold = metric.score >= metric.threshold
|
|
const categoryColors = {
|
|
accuracy: 'bg-blue-100 text-blue-700',
|
|
fairness: 'bg-purple-100 text-purple-700',
|
|
robustness: 'bg-green-100 text-green-700',
|
|
explainability: 'bg-yellow-100 text-yellow-700',
|
|
performance: 'bg-orange-100 text-orange-700',
|
|
}
|
|
|
|
const categoryLabels = {
|
|
accuracy: 'Genauigkeit',
|
|
fairness: 'Fairness',
|
|
robustness: 'Robustheit',
|
|
explainability: 'Erklaerbarkeit',
|
|
performance: 'Performance',
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border-2 p-6 ${isAboveThreshold ? 'border-gray-200' : 'border-red-200'}`}>
|
|
<div className="flex items-start justify-between mb-4">
|
|
<div>
|
|
<span className={`px-2 py-1 text-xs rounded-full ${categoryColors[metric.category]}`}>
|
|
{categoryLabels[metric.category]}
|
|
</span>
|
|
<h4 className="font-semibold text-gray-900 mt-2">{metric.name}</h4>
|
|
{metric.ai_system && <p className="text-xs text-gray-500">{metric.ai_system}</p>}
|
|
</div>
|
|
<div className={`flex items-center gap-1 text-sm ${
|
|
metric.trend === 'up' ? 'text-green-600' :
|
|
metric.trend === 'down' ? 'text-red-600' : 'text-gray-500'
|
|
}`}>
|
|
{metric.trend === 'up' && <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" /></svg>}
|
|
{metric.trend === 'down' && <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" /></svg>}
|
|
{metric.trend === 'stable' && <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14" /></svg>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-end justify-between">
|
|
<div>
|
|
<div className={`text-3xl font-bold ${isAboveThreshold ? 'text-gray-900' : 'text-red-600'}`}>
|
|
{metric.score}%
|
|
</div>
|
|
<div className="text-sm text-gray-500">Schwellenwert: {metric.threshold}%</div>
|
|
</div>
|
|
<div className="w-24 h-2 bg-gray-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${isAboveThreshold ? 'bg-green-500' : 'bg-red-500'}`}
|
|
style={{ width: `${Math.min(metric.score, 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mt-3 flex justify-end">
|
|
<button onClick={() => onEdit(metric)} className="text-xs text-purple-600 hover:text-purple-700 hover:bg-purple-50 px-2 py-1 rounded">
|
|
Score aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|