da50f3b5ea
- Design tokens (ds.ts, theme.ts, scoreTheme.ts): new warm palette (#152642 navy, #f9f8f6 warm white, #e8e7e4 borders, #b8975a gold accent), flat score tier badges replacing CSS gradients, Inter + DM Serif Display typography - Card components: white-background cards, DM Serif score numbers, max-2 badge chips with +N overflow tooltip, editorial score badge positioning - Layout shell: gold left-accent nav active state, 64px top bar, outlined workspace chip, DM Serif page titles - Shared atoms: GenericBadge (outlined/solid variants), ResultFilterBar (simplified chip styles), DecisionContextPanel (dot metrics, no left accent) - Global replacement (118 files): #1e3a5f→#152642, #e2e8f0→#e8e7e4, #f4f6f9→#f9f8f6 — all handled via Node.js for proper UTF-8 safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.1 KiB
TypeScript
138 lines
5.1 KiB
TypeScript
import {
|
||
Box,
|
||
Table,
|
||
TableBody,
|
||
TableCell,
|
||
TableHead,
|
||
TableRow,
|
||
Typography,
|
||
} from '@mui/material'
|
||
import { AIOutputStatusBadge } from './AIOutputStatusBadge'
|
||
import { PromptVersionBadge } from './PromptVersionBadge'
|
||
import { AIErrorBadge } from './AIErrorBadge'
|
||
import { AIMonitoringEmptyState } from './AIMonitoringEmptyState'
|
||
import type { AIOutput, AIOutputType } from '../../domain/aiOutput'
|
||
|
||
const TYPE_LABELS: Record<AIOutputType, string> = {
|
||
NEED_PARSE: 'Bedarf-Parsing',
|
||
FOLLOW_UP_QUESTIONS: 'Rückfragen',
|
||
MATCH_EXPLANATION: 'Match-Begründung',
|
||
COMPARE_SUMMARY: 'Vergleich',
|
||
DECISION_BRIEF: 'Entscheidungs-Brief',
|
||
DATA_QUALITY_SUMMARY: 'Datenqualität',
|
||
}
|
||
|
||
const TYPE_COLORS: Record<AIOutputType, string> = {
|
||
NEED_PARSE: '#152642',
|
||
FOLLOW_UP_QUESTIONS: '#0891b2',
|
||
MATCH_EXPLANATION: '#4f46e5',
|
||
COMPARE_SUMMARY: '#1a7a4a',
|
||
DECISION_BRIEF: '#7c3aed',
|
||
DATA_QUALITY_SUMMARY: '#d97706',
|
||
}
|
||
|
||
const MODEL_SHORT: Record<string, string> = {
|
||
'claude-3-5-sonnet-20241022': 'Sonnet 3.5',
|
||
'claude-3-haiku-20240307': 'Haiku 3',
|
||
'claude-3-opus-20240229': 'Opus 3',
|
||
}
|
||
|
||
function shortTime(iso: string) {
|
||
return new Date(iso).toLocaleString('de-CH', { dateStyle: 'short', timeStyle: 'short' })
|
||
}
|
||
|
||
interface Props {
|
||
outputs: AIOutput[]
|
||
selectedId: string | null
|
||
onSelect: (output: AIOutput) => void
|
||
isEmpty: boolean
|
||
}
|
||
|
||
export function AIOutputTable({ outputs, selectedId, onSelect, isEmpty }: Props) {
|
||
if (isEmpty && outputs.length === 0) {
|
||
return <AIMonitoringEmptyState context="no-outputs" />
|
||
}
|
||
if (outputs.length === 0) {
|
||
return <AIMonitoringEmptyState context="filtered-empty" />
|
||
}
|
||
|
||
return (
|
||
<Table size="small" stickyHeader>
|
||
<TableHead>
|
||
<TableRow sx={{ '& th': { bgcolor: '#f8fafc', fontSize: '0.7rem', fontWeight: 700, color: '#64748b', py: 0.75, textTransform: 'uppercase', letterSpacing: 0.4 } }}>
|
||
<TableCell sx={{ minWidth: 110 }}>Zeitpunkt</TableCell>
|
||
<TableCell sx={{ minWidth: 130 }}>Typ</TableCell>
|
||
<TableCell sx={{ minWidth: 100 }}>Modell</TableCell>
|
||
<TableCell sx={{ minWidth: 140 }}>Version</TableCell>
|
||
<TableCell sx={{ minWidth: 95 }}>Status</TableCell>
|
||
<TableCell sx={{ minWidth: 65 }}>Latenz</TableCell>
|
||
<TableCell sx={{ minWidth: 80 }}>Fehler</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{outputs.map(output => {
|
||
const isSelected = selectedId === output.id
|
||
const color = TYPE_COLORS[output.type] ?? '#64748b'
|
||
return (
|
||
<TableRow
|
||
key={output.id}
|
||
hover
|
||
onClick={() => onSelect(output)}
|
||
sx={{
|
||
cursor: 'pointer',
|
||
bgcolor: isSelected ? '#eff6ff' : undefined,
|
||
borderLeft: isSelected ? '3px solid #1e3a5f' : '3px solid transparent',
|
||
'&:hover': { bgcolor: isSelected ? '#eff6ff' : '#f8fafc' },
|
||
'& td': { py: 0.75, borderBottom: '1px solid #f1f5f9' },
|
||
}}
|
||
>
|
||
<TableCell>
|
||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
|
||
{shortTime(output.createdAt)}
|
||
</Typography>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
px: 0.75,
|
||
py: 0.2,
|
||
borderRadius: 1,
|
||
bgcolor: `${color}12`,
|
||
}}
|
||
>
|
||
<Typography variant="caption" sx={{ color, fontWeight: 700, fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
|
||
{TYPE_LABELS[output.type] ?? output.type}
|
||
</Typography>
|
||
</Box>
|
||
</TableCell>
|
||
<TableCell>
|
||
<Typography variant="caption" sx={{ fontWeight: 500, fontSize: '0.7rem', color: '#334155' }}>
|
||
{MODEL_SHORT[output.model] ?? output.model}
|
||
</Typography>
|
||
</TableCell>
|
||
<TableCell>
|
||
<PromptVersionBadge promptVersion={output.promptVersion} schemaVersion={output.schemaVersion} />
|
||
</TableCell>
|
||
<TableCell>
|
||
<AIOutputStatusBadge status={output.reviewStatus} />
|
||
</TableCell>
|
||
<TableCell>
|
||
<Typography variant="caption" sx={{ fontSize: '0.7rem', color: output.latencyMs && output.latencyMs > 5000 ? '#c0392b' : '#64748b' }}>
|
||
{output.latencyMs != null ? `${(output.latencyMs / 1000).toFixed(1)}s` : '–'}
|
||
</Typography>
|
||
</TableCell>
|
||
<TableCell>
|
||
{output.error ? <AIErrorBadge error={output.error} /> : (
|
||
<Typography variant="caption" color="text.disabled" sx={{ fontSize: '0.7rem' }}>–</Typography>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
)
|
||
})}
|
||
</TableBody>
|
||
</Table>
|
||
)
|
||
}
|