Files
dashfront/src/components/manifest/ManifestPdfDialog.jsx
2026-09-20 07:18:43 +02:00

250 lines
7.9 KiB
JavaScript

import { useEffect, useRef, useState } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import Collapse from '@mui/material/Collapse';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Alert from '@mui/material/Alert';
import CloseIcon from '@mui/icons-material/Close';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
/** Chip colour by SSE event type */
const EVENT_CHIP_COLOR = {
status: 'default',
llm_call: 'primary',
tool_call: 'secondary',
usage: 'info',
};
function EventLog({ messages, streaming }) {
const bottomRef = useRef(null);
const [expanded, setExpanded] = useState(true);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, [messages]);
if (!messages?.length && !streaming) return null;
return (
<Box>
<Box
sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'pointer', userSelect: 'none', mb: 0.5 }}
onClick={() => setExpanded((v) => !v)}
>
<Typography variant="caption" color="text.secondary" fontWeight={600}>
LLM-Protokoll
</Typography>
{streaming ? (
<CircularProgress size={12} sx={{ ml: 'auto' }} />
) : (
<CheckCircleIcon sx={{ ml: 'auto', fontSize: 16, color: 'success.main' }} />
)}
<IconButton size="small" sx={{ p: 0 }}>
{expanded ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
</IconButton>
</Box>
<Collapse in={expanded}>
<Box
sx={{
maxHeight: 280,
overflowY: 'auto',
border: 1,
borderColor: 'divider',
borderRadius: 1,
bgcolor: (t) => (t.palette.mode === 'dark' ? 'grey.900' : 'grey.50'),
p: 1,
}}
>
{messages.map((msg, i) => (
<Box key={i} sx={{ display: 'flex', alignItems: 'baseline', gap: 1, mb: 0.25 }}>
<Chip
label={msg.type}
size="small"
color={EVENT_CHIP_COLOR[msg.type] ?? 'default'}
sx={{ fontFamily: 'monospace', fontSize: '0.65rem', height: 18, flexShrink: 0 }}
/>
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontSize: '0.75rem', wordBreak: 'break-all' }}>
{msg.text}
</Typography>
</Box>
))}
{streaming && !messages.length && (
<Typography variant="caption" color="text.secondary">
Verbinde…
</Typography>
)}
<div ref={bottomRef} />
</Box>
</Collapse>
</Box>
);
}
export default function ManifestPdfDialog({
open,
filename,
data,
unmatchedSupplierName,
onCreateSupplier,
onClose,
onAbort,
}) {
if (!open) return null;
const { streaming = false, messages = [], result = null, error = null, aborted = false } = data ?? {};
const hasResult = Boolean(result);
const isTextDoc =
filename?.toLowerCase().endsWith('.txt') || filename?.toLowerCase().endsWith('.csv');
const handleAbortOrClose = streaming ? (onAbort || onClose) : onClose;
return (
<Dialog
open={open}
onClose={handleAbortOrClose}
maxWidth="sm"
fullWidth
aria-labelledby="pdf-dialog-title"
>
<DialogTitle
id="pdf-dialog-title"
sx={{
m: 0,
p: 2,
display: 'flex',
alignItems: 'center',
gap: 1.5,
borderBottom: 1,
borderColor: 'divider',
}}
>
{aborted ? (
<ErrorOutlinedIcon color="warning" />
) : error ? (
<ErrorOutlinedIcon color="error" />
) : streaming ? (
<CircularProgress size={22} />
) : isTextDoc ? (
<TextSnippetOutlinedIcon color="action" />
) : (
<PictureAsPdfIcon color="action" />
)}
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle1" component="div" fontWeight={600}>
{streaming
? (isTextDoc ? 'Dokument wird analysiert…' : 'PDF wird analysiert…')
: aborted
? 'Analyse abgebrochen'
: error
? 'Analyse fehlgeschlagen'
: (isTextDoc ? 'Dokument-Analyse abgeschlossen' : 'PDF-Analyse abgeschlossen')}
</Typography>
{filename ? (
<Typography variant="caption" color="text.secondary">
{filename}
</Typography>
) : null}
</Box>
<IconButton
aria-label="close"
onClick={handleAbortOrClose}
size="small"
sx={{ color: (theme) => theme.palette.grey[500] }}
>
<CloseIcon fontSize="small" />
</IconButton>
</DialogTitle>
<DialogContent dividers sx={{ p: 2 }}>
{/* Error or Aborted Alert */}
{error && (
<Alert severity={aborted ? 'warning' : 'error'} icon={aborted ? undefined : <ErrorOutlinedIcon />} sx={{ mb: 2 }}>
<Typography variant="body2" fontWeight={600}>{error}</Typography>
</Alert>
)}
{/* Live log */}
<EventLog messages={messages} streaming={streaming} />
{/* Success notification */}
{hasResult && !streaming && !error && !aborted && (
<Alert severity="success" sx={{ mt: 2 }}>
Daten erfolgreich in das Formular übernommen.
</Alert>
)}
{/* Unmatched supplier alert */}
{hasResult && !streaming && !error && !aborted && unmatchedSupplierName && (
<Alert
severity="warning"
sx={{ mt: 1.5, alignItems: 'center' }}
action={
onCreateSupplier ? (
<Button
color="inherit"
size="small"
variant="outlined"
onClick={() => onCreateSupplier(unmatchedSupplierName)}
>
Lieferant anlegen
</Button>
) : null
}
>
Lieferant „{unmatchedSupplierName}“ wurde im Dokument erkannt, existiert aber noch nicht in den Stammdaten.
</Alert>
)}
</DialogContent>
<DialogActions sx={{ p: 1.5, px: 2 }}>
{streaming ? (
<Button
onClick={onAbort || onClose}
variant="outlined"
color="error"
size="small"
startIcon={<CloseIcon />}
>
Abbrechen
</Button>
) : (
<>
{unmatchedSupplierName && onCreateSupplier ? (
<Button
onClick={() => onCreateSupplier(unmatchedSupplierName)}
variant="contained"
color="primary"
size="small"
>
Lieferant „{unmatchedSupplierName}“ anlegen
</Button>
) : null}
<Button
onClick={onClose}
variant={unmatchedSupplierName && onCreateSupplier ? 'outlined' : 'contained'}
size="small"
>
Schließen
</Button>
</>
)}
</DialogActions>
</Dialog>
);
}