[split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
150
website/app/admin/training/_components/ChartComponents.tsx
Normal file
150
website/app/admin/training/_components/ChartComponents.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import type { TrainingJob } from './types'
|
||||
|
||||
// Progress Ring Component
|
||||
export function ProgressRing({ progress, size = 120, strokeWidth = 8, color = '#10B981' }: {
|
||||
progress: number
|
||||
size?: number
|
||||
strokeWidth?: number
|
||||
color?: string
|
||||
}) {
|
||||
const radius = (size - strokeWidth) / 2
|
||||
const circumference = radius * 2 * Math.PI
|
||||
const offset = circumference - (progress / 100) * circumference
|
||||
|
||||
return (
|
||||
<div className="relative" style={{ width: size, height: size }}>
|
||||
<svg className="transform -rotate-90" width={size} height={size}>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
className="text-gray-200 dark:text-gray-700"
|
||||
/>
|
||||
<circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={offset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-500"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{Math.round(progress)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Mini Line Chart Component
|
||||
export function MiniChart({ data, color = '#10B981', height = 60 }: {
|
||||
data: number[]
|
||||
color?: string
|
||||
height?: number
|
||||
}) {
|
||||
if (!data.length) return null
|
||||
|
||||
const max = Math.max(...data)
|
||||
const min = Math.min(...data)
|
||||
const range = max - min || 1
|
||||
const width = 200
|
||||
const padding = 4
|
||||
|
||||
const points = data.map((value, i) => {
|
||||
const x = padding + (i / (data.length - 1)) * (width - 2 * padding)
|
||||
const y = padding + (1 - (value - min) / range) * (height - 2 * padding)
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
|
||||
return (
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<polyline
|
||||
points={points}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
{data.length > 0 && (
|
||||
<circle
|
||||
cx={padding + ((data.length - 1) / (data.length - 1)) * (width - 2 * padding)}
|
||||
cy={padding + (1 - (data[data.length - 1] - min) / range) * (height - 2 * padding)}
|
||||
r={4}
|
||||
fill={color}
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Status Badge
|
||||
export function StatusBadge({ status }: { status: TrainingJob['status'] }) {
|
||||
const styles = {
|
||||
queued: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
||||
preparing: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
training: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
validating: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
paused: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
}
|
||||
|
||||
const labels = {
|
||||
queued: 'In Warteschlange',
|
||||
preparing: 'Vorbereitung',
|
||||
training: 'Training läuft',
|
||||
validating: 'Validierung',
|
||||
completed: 'Abgeschlossen',
|
||||
failed: 'Fehlgeschlagen',
|
||||
paused: 'Pausiert',
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${styles[status]}`}>
|
||||
{status === 'training' && (
|
||||
<span className="w-2 h-2 mr-1.5 bg-blue-500 rounded-full animate-pulse" />
|
||||
)}
|
||||
{labels[status]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
// Metric Card
|
||||
export function MetricCard({ label, value, unit, trend, color }: {
|
||||
label: string
|
||||
value: number | string
|
||||
unit?: string
|
||||
trend?: 'up' | 'down' | 'neutral'
|
||||
color?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">{label}</p>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-2xl font-bold" style={{ color: color || 'inherit' }}>
|
||||
{typeof value === 'number' ? value.toFixed(3) : value}
|
||||
</span>
|
||||
{unit && <span className="text-sm text-gray-400">{unit}</span>}
|
||||
{trend && (
|
||||
<span className={`ml-2 text-sm ${
|
||||
trend === 'up' ? 'text-green-500' : trend === 'down' ? 'text-red-500' : 'text-gray-400'
|
||||
}`}>
|
||||
{trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user