250 lines
7.9 KiB
JavaScript
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>
|
|
);
|
|
}
|