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>
This commit is contained in:
+191
@@ -0,0 +1,191 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user