271 lines
12 KiB
TypeScript
271 lines
12 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Box, Paper, Typography, Chip, IconButton, Tooltip, LinearProgress,
|
|
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TablePagination,
|
|
} from '@mui/material';
|
|
import {
|
|
History as HistoryIcon,
|
|
Delete as DeleteIcon,
|
|
Visibility as VisibilityIcon,
|
|
ContentCopy as CopyIcon,
|
|
Facebook as FacebookIcon,
|
|
Cancel as CancelIcon,
|
|
Replay as ResumeIcon,
|
|
} from '@mui/icons-material';
|
|
import type { AnalysisSession, PaginationInfo } from './history-types';
|
|
import { getRiskIcon, getRiskCategoryColor, formatDuration, formatDate } from './history-helpers';
|
|
|
|
// Sessions still in flight can be canceled (agent-v3 /:id/cancel).
|
|
const CANCELABLE = new Set(['pending', 'running', 'processing']);
|
|
// Failed/canceled sessions can be resumed from checkpoint (/:id/resume).
|
|
const RESUMABLE = new Set(['failed', 'canceled']);
|
|
|
|
interface Props {
|
|
loading: boolean;
|
|
analyses: AnalysisSession[];
|
|
pagination: PaginationInfo;
|
|
copiedId: string | null;
|
|
onPageChange: (event: unknown, newPage: number) => void;
|
|
onRowsPerPageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
onViewDetails: (sessionId: string) => void;
|
|
onDeleteClick: (session: AnalysisSession) => void;
|
|
onCopyId: (e: React.MouseEvent, id: string) => void;
|
|
onSocialPostClick?: (session: AnalysisSession) => void;
|
|
onCancelClick?: (session: AnalysisSession) => void;
|
|
onResumeClick?: (session: AnalysisSession) => void;
|
|
}
|
|
|
|
export const HistoryTable: React.FC<Props> = ({
|
|
loading,
|
|
analyses,
|
|
pagination,
|
|
copiedId,
|
|
onPageChange,
|
|
onRowsPerPageChange,
|
|
onViewDetails,
|
|
onDeleteClick,
|
|
onCopyId,
|
|
onSocialPostClick,
|
|
onCancelClick,
|
|
onResumeClick,
|
|
}) => (
|
|
<Paper>
|
|
{loading && <LinearProgress />}
|
|
<TableContainer>
|
|
<Table size="small">
|
|
<TableHead>
|
|
<TableRow sx={{ bgcolor: 'action.hover' }}>
|
|
<TableCell sx={{ fontWeight: 600 }}>Session ID</TableCell>
|
|
<TableCell sx={{ fontWeight: 600 }}>Date</TableCell>
|
|
<TableCell sx={{ fontWeight: 600 }}>Email</TableCell>
|
|
<TableCell sx={{ fontWeight: 600 }}>Source</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Risk</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Techniques</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>AI %</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Claims</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Duration</TableCell>
|
|
<TableCell align="center" sx={{ fontWeight: 600 }}>Actions</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{!loading && analyses.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={10} align="center" sx={{ py: 4 }}>
|
|
<HistoryIcon sx={{ fontSize: 48, color: 'text.disabled', mb: 1 }} />
|
|
<Typography color="text.secondary">No analyses found</Typography>
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
analyses.map((analysis) => (
|
|
<TableRow
|
|
key={analysis.session_id}
|
|
hover
|
|
sx={{ cursor: 'pointer' }}
|
|
onClick={() => onViewDetails(analysis.session_id)}
|
|
>
|
|
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
<Box display="flex" alignItems="center" gap={0.5}>
|
|
<Tooltip title={analysis.session_id}>
|
|
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
|
|
{analysis.session_id.substring(0, 8)}...
|
|
</Typography>
|
|
</Tooltip>
|
|
<Tooltip title={copiedId === analysis.session_id ? 'Copied!' : 'Copy'}>
|
|
<IconButton size="small" onClick={(e) => onCopyId(e, analysis.session_id)}>
|
|
<CopyIcon sx={{ fontSize: 14, color: copiedId === analysis.session_id ? 'success.main' : 'action.active' }} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
|
{formatDate(analysis.started_at)}
|
|
</Typography>
|
|
<Typography variant="caption" color="text.secondary">
|
|
{analysis.input_type}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Tooltip title={`User ID: ${analysis.user_id}`}>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
maxWidth: 180,
|
|
}}
|
|
>
|
|
{analysis.user_email || <span style={{ color: '#999' }}>No email</span>}
|
|
</Typography>
|
|
</Tooltip>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<Chip
|
|
label={analysis.input_type}
|
|
size="small"
|
|
variant="outlined"
|
|
color={analysis.input_type === 'url' ? 'primary' : analysis.input_type === 'text' ? 'default' : 'secondary'}
|
|
sx={{ textTransform: 'uppercase', fontSize: 11, height: 22 }}
|
|
/>
|
|
<Chip label={analysis.source_app} size="small" variant="outlined" sx={{ fontSize: 11, height: 22 }} />
|
|
</Box>
|
|
{(analysis.source_verdict || analysis.domain) && (
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.3 }}>
|
|
{analysis.source_publication || analysis.domain || ''}
|
|
{(analysis.source_verdict || analysis.domain_verdict) && (
|
|
<Chip
|
|
label={analysis.source_verdict || analysis.domain_verdict}
|
|
size="small"
|
|
sx={{
|
|
ml: 0.5, height: 18, fontSize: 10,
|
|
bgcolor: (analysis.source_verdict || analysis.domain_verdict) === 'TRUSTED' ? '#22c55e'
|
|
: (analysis.source_verdict || analysis.domain_verdict) === 'SUSPICIOUS' ? '#f97316'
|
|
: (analysis.source_verdict || analysis.domain_verdict) === 'UNTRUSTED' ? '#ef4444' : undefined,
|
|
color: (analysis.source_verdict || analysis.domain_verdict) !== 'NEUTRAL' ? '#fff' : undefined,
|
|
}}
|
|
/>
|
|
)}
|
|
</Typography>
|
|
)}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{analysis.risk_score !== null ? (
|
|
<Tooltip title={`${analysis.risk_category} - ${analysis.risk_level}`}>
|
|
<Chip
|
|
icon={getRiskIcon(analysis.risk_level)}
|
|
label={`${analysis.risk_score} ${analysis.risk_category || ''}`}
|
|
size="small"
|
|
sx={{
|
|
bgcolor: getRiskCategoryColor(analysis.risk_category),
|
|
color: '#fff',
|
|
fontWeight: 600,
|
|
'& .MuiChip-icon': { color: '#fff' },
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
) : (
|
|
<Chip label="-" size="small" variant="outlined" />
|
|
)}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{analysis.techniques_count !== null ? (
|
|
<Chip
|
|
label={analysis.techniques_count}
|
|
size="small"
|
|
color={Number(analysis.techniques_count) > 0 ? 'warning' : 'default'}
|
|
variant="outlined"
|
|
/>
|
|
) : '-'}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{analysis.ai_probability !== null ? (
|
|
<Tooltip title={analysis.ai_verdict || ''}>
|
|
<Chip
|
|
label={`${Math.round(Number(analysis.ai_probability))}%`}
|
|
size="small"
|
|
color={Number(analysis.ai_probability) > 50 ? 'warning' : 'default'}
|
|
variant="outlined"
|
|
/>
|
|
</Tooltip>
|
|
) : '-'}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
{analysis.total_claims !== null ? (
|
|
<Tooltip title={`True: ${analysis.verified_true}, False: ${analysis.verified_false}`}>
|
|
<Typography variant="body2">
|
|
{analysis.total_claims}
|
|
</Typography>
|
|
</Tooltip>
|
|
) : '-'}
|
|
</TableCell>
|
|
<TableCell align="center">
|
|
<Typography variant="body2" color="text.secondary">
|
|
{formatDuration(analysis.total_duration_ms)}
|
|
</Typography>
|
|
</TableCell>
|
|
<TableCell align="center" onClick={(e) => e.stopPropagation()}>
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => onViewDetails(analysis.session_id)}
|
|
title="View details"
|
|
>
|
|
<VisibilityIcon fontSize="small" />
|
|
</IconButton>
|
|
{onSocialPostClick && (
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => onSocialPostClick(analysis)}
|
|
title="Post to Facebook"
|
|
sx={{ color: '#1877F2' }}
|
|
>
|
|
<FacebookIcon fontSize="small" />
|
|
</IconButton>
|
|
)}
|
|
{onCancelClick && CANCELABLE.has(analysis.status) && (
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => onCancelClick(analysis)}
|
|
title="Cancel running analysis"
|
|
color="warning"
|
|
>
|
|
<CancelIcon fontSize="small" />
|
|
</IconButton>
|
|
)}
|
|
{onResumeClick && RESUMABLE.has(analysis.status) && (
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => onResumeClick(analysis)}
|
|
title="Resume from checkpoint (re-runs only unfinished components)"
|
|
color="primary"
|
|
>
|
|
<ResumeIcon fontSize="small" />
|
|
</IconButton>
|
|
)}
|
|
<IconButton
|
|
size="small"
|
|
onClick={() => onDeleteClick(analysis)}
|
|
title="Delete"
|
|
color="error"
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</IconButton>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
<TablePagination
|
|
component="div"
|
|
count={pagination.total}
|
|
page={pagination.page - 1}
|
|
onPageChange={onPageChange}
|
|
rowsPerPage={pagination.limit}
|
|
onRowsPerPageChange={onRowsPerPageChange}
|
|
rowsPerPageOptions={[10, 20, 50, 100]}
|
|
/>
|
|
</Paper>
|
|
);
|