d15a13e485
- 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>
192 lines
7.1 KiB
TypeScript
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>
|
|
)
|
|
}
|