Files
property-match/.claude/worktrees/agent-a82a3716/src/components/ai-monitoring/AIOutputDetailPanel.tsx
T
Benjamin Sutter d15a13e485 feat: remove Administration workspace — keep only Verwaltung + Suche
- Delete all ops page components (ReviewQueue, AIMonitoring, Governance,
  SourceMonitoring, ActivityTimeline, SignalPipeline)
- Remove OPERATIONS workspace from AppShell config, nav order, path detection
- Remove all /ops/* routes from App.tsx
- Remove WorkspaceType.OPERATIONS from allowedWorkspaces in authService,
  sessionStore, permissions
- Keep MarketIntelligence page (already moved to /supply/market-intelligence)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:32:41 +02:00

192 lines
7.1 KiB
TypeScript

import { Alert, Box, Divider, IconButton, Typography } from '@mui/material'
import { X } from 'lucide-react'
import { AIOutputStatusBadge } from './AIOutputStatusBadge'
import { PromptVersionBadge } from './PromptVersionBadge'
import { AIErrorBadge } from './AIErrorBadge'
import { AIReviewActionToolbar } from './AIReviewActionToolbar'
import type { AIOutput, AIOutputType } from '../../domain/aiOutput'
import type { ReviewStatus } from '../../domain/enums'
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 ERROR_TYPE_LABELS: Record<string, string> = {
SCHEMA_VALIDATION: 'Schema-Validierungsfehler',
PROVIDER_TIMEOUT: 'Provider-Timeout',
INVALID_JSON: 'Ungültiges JSON',
EMPTY_RESPONSE: 'Leere Antwort',
RATE_LIMIT: 'Rate-Limit erreicht',
}
const MODEL_LABELS: Record<string, string> = {
'claude-3-5-sonnet-20241022': 'Claude 3.5 Sonnet',
'claude-3-haiku-20240307': 'Claude 3 Haiku',
'claude-3-opus-20240229': 'Claude 3 Opus',
}
const ENTITY_TYPE_LABELS: Record<string, string> = {
NEED: 'Gesuch', MATCH: 'Match', PROPERTY: 'Objekt', SIGNAL: 'Signal',
}
interface Props {
output: AIOutput
onClose: () => void
onUpdateStatus: (status: ReviewStatus) => void
isSubmitting?: boolean
}
function MetaRow({ label, children }: { label: string; children: React.ReactNode }) {
return (
<Box sx={{ display: 'flex', gap: 1, mb: 0.625, alignItems: 'flex-start' }}>
<Typography variant="caption" color="text.secondary" sx={{ minWidth: 116, flexShrink: 0, pt: 0.125 }}>
{label}
</Typography>
<Box sx={{ flex: 1, minWidth: 0 }}>{children}</Box>
</Box>
)
}
export function AIOutputDetailPanel({ output, onClose, onUpdateStatus, isSubmitting }: Props) {
const handleCopyJson = () => {
navigator.clipboard.writeText(output.outputPreview).catch(() => {})
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
{/* Header */}
<Box sx={{ px: 2, py: 1.5, borderBottom: '1px solid #e2e8f0', flexShrink: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 0.5, alignItems: 'center' }}>
<AIOutputStatusBadge status={output.reviewStatus} />
{output.error && <AIErrorBadge error={output.error} />}
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.875rem' }}>
{TYPE_LABELS[output.type] ?? output.type}
</Typography>
</Box>
<IconButton size="small" onClick={onClose} sx={{ flexShrink: 0, mt: -0.25 }}>
<X size={16} />
</IconButton>
</Box>
</Box>
{/* Scrollable body */}
<Box sx={{ flex: 1, overflowY: 'auto', px: 2, py: 1.5 }}>
{/* Metadata */}
<MetaRow label="Output-ID">
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#475569' }}>
{output.id}
</Typography>
</MetaRow>
<MetaRow label="Erstellt">
<Typography variant="caption" sx={{ color: '#334155' }}>
{new Date(output.createdAt).toLocaleString('de-CH', { dateStyle: 'medium', timeStyle: 'short' })}
</Typography>
</MetaRow>
<MetaRow label="Modell">
<Typography variant="caption" sx={{ fontWeight: 600, color: '#334155' }}>
{MODEL_LABELS[output.model] ?? output.model}
</Typography>
</MetaRow>
<MetaRow label="Provider">
<Typography variant="caption" sx={{ color: '#334155', textTransform: 'capitalize' }}>
{output.provider}
</Typography>
</MetaRow>
<MetaRow label="Prompt-Version">
<PromptVersionBadge promptVersion={output.promptVersion} schemaVersion={output.schemaVersion} />
</MetaRow>
<MetaRow label="Input-Hash">
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#94a3b8' }}>
{output.inputHash}
</Typography>
</MetaRow>
<MetaRow label="Bezug">
<Typography variant="caption" sx={{ color: '#334155' }}>
{ENTITY_TYPE_LABELS[output.relatedEntityType] ?? output.relatedEntityType}{' '}
<span style={{ fontFamily: 'monospace', fontSize: '0.65rem', color: '#94a3b8' }}>
{output.relatedEntityId}
</span>
</Typography>
</MetaRow>
{output.latencyMs != null && (
<MetaRow label="Latenz">
<Typography variant="caption" sx={{ color: output.latencyMs > 5000 ? '#c0392b' : '#334155', fontWeight: output.latencyMs > 5000 ? 700 : 400 }}>
{(output.latencyMs / 1000).toFixed(2)}s
</Typography>
</MetaRow>
)}
{output.costEstimate != null && (
<MetaRow label="Kostenschätzung">
<Typography variant="caption" sx={{ color: '#334155' }}>
${output.costEstimate.toFixed(4)}
</Typography>
</MetaRow>
)}
<Divider sx={{ my: 1.5 }} />
{/* Error details */}
{output.error && (
<Box sx={{ mb: 1.5 }}>
<Alert
severity="error"
sx={{ '& .MuiAlert-message': { fontSize: '0.8rem' }, mb: 1 }}
>
<strong>{ERROR_TYPE_LABELS[output.error.type] ?? output.error.type}</strong>
<br />
{output.error.message}
{output.error.recoverable && (
<Typography variant="caption" sx={{ display: 'block', mt: 0.5, color: '#92400e' }}>
Wiederholbar kann erneut ausgelöst werden.
</Typography>
)}
</Alert>
</Box>
)}
{/* Output preview */}
<Box sx={{ mb: 1.5 }}>
<Typography variant="caption" sx={{ fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#64748b', display: 'block', mb: 0.5 }}>
Output-Vorschau
</Typography>
<Box
sx={{
p: 1.25,
bgcolor: '#f8fafc',
borderRadius: 1,
border: '1px solid #e2e8f0',
fontFamily: 'monospace',
fontSize: '0.75rem',
color: '#334155',
lineHeight: 1.6,
overflowX: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-all',
}}
>
{output.outputPreview || '(kein Output)'}
</Box>
</Box>
<Divider sx={{ mb: 1.5 }} />
{/* Actions */}
<AIReviewActionToolbar
output={output}
onUpdateStatus={onUpdateStatus}
onCopyJson={handleCopyJson}
isSubmitting={isSubmitting}
/>
</Box>
</Box>
)
}