184 lines
6.4 KiB
JavaScript
184 lines
6.4 KiB
JavaScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
Box,
|
||
|
|
Typography,
|
||
|
|
Chip,
|
||
|
|
IconButton,
|
||
|
|
LinearProgress,
|
||
|
|
Menu,
|
||
|
|
MenuItem,
|
||
|
|
ListItemIcon,
|
||
|
|
Avatar,
|
||
|
|
useTheme
|
||
|
|
} from '@mui/material';
|
||
|
|
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||
|
|
import StopIcon from '@mui/icons-material/Stop';
|
||
|
|
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||
|
|
import ErrorIcon from '@mui/icons-material/Error';
|
||
|
|
import PauseCircleIcon from '@mui/icons-material/PauseCircle';
|
||
|
|
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||
|
|
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
||
|
|
import QuizIcon from '@mui/icons-material/Quiz';
|
||
|
|
import { useTranslation } from 'react-i18next';
|
||
|
|
import { getModelIcon } from '@/lib/util/modelIcon';
|
||
|
|
import styles from '../styles';
|
||
|
|
|
||
|
|
const STATUS_CONFIG = {
|
||
|
|
0: { label: 'evalTasks.statusProcessing', color: 'info', icon: HourglassEmptyIcon },
|
||
|
|
1: { label: 'evalTasks.statusCompleted', color: 'success', icon: CheckCircleIcon },
|
||
|
|
2: { label: 'evalTasks.statusFailed', color: 'error', icon: ErrorIcon },
|
||
|
|
3: { label: 'evalTasks.statusInterrupted', color: 'warning', icon: PauseCircleIcon }
|
||
|
|
};
|
||
|
|
|
||
|
|
export default function EvalTaskCard({ task, onView, onDelete, onInterrupt }) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const theme = useTheme();
|
||
|
|
const [anchorEl, setAnchorEl] = useState(null);
|
||
|
|
const open = Boolean(anchorEl);
|
||
|
|
|
||
|
|
const { modelInfo, detail, status, completedCount, totalCount, createAt } = task;
|
||
|
|
const statusConfig = STATUS_CONFIG[status] || STATUS_CONFIG[0];
|
||
|
|
const StatusIcon = statusConfig.icon;
|
||
|
|
const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0;
|
||
|
|
const finalScore = detail?.finalScore;
|
||
|
|
|
||
|
|
const handleMenuClick = e => {
|
||
|
|
e.stopPropagation();
|
||
|
|
setAnchorEl(e.currentTarget);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleMenuClose = () => setAnchorEl(null);
|
||
|
|
|
||
|
|
const handleAction = action => () => {
|
||
|
|
handleMenuClose();
|
||
|
|
action?.(task);
|
||
|
|
};
|
||
|
|
|
||
|
|
const getScoreColor = score => {
|
||
|
|
if (score >= 80) return 'success';
|
||
|
|
if (score >= 60) return 'info';
|
||
|
|
if (score >= 40) return 'warning';
|
||
|
|
return 'error';
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card sx={styles.taskCard(theme)} onClick={handleAction(onView)}>
|
||
|
|
<CardContent sx={styles.taskCardContent}>
|
||
|
|
{/* 头部 */}
|
||
|
|
<Box sx={styles.taskCardHeader}>
|
||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 1, overflow: 'hidden' }}>
|
||
|
|
<Avatar sx={{ bgcolor: 'transparent', width: 40, height: 40, border: '1px solid', borderColor: 'divider' }}>
|
||
|
|
<img
|
||
|
|
src={getModelIcon(modelInfo?.modelName || modelInfo?.modelId)}
|
||
|
|
alt={modelInfo?.modelId || 'model'}
|
||
|
|
style={{ width: 28, height: 28, objectFit: 'contain' }}
|
||
|
|
/>
|
||
|
|
</Avatar>
|
||
|
|
<Box sx={styles.taskCardModel}>
|
||
|
|
<Typography sx={styles.taskCardModelName} noWrap>
|
||
|
|
{modelInfo?.modelName || modelInfo?.modelId}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="caption" color="text.secondary" noWrap>
|
||
|
|
{modelInfo?.providerName || modelInfo?.providerId}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
</Box>
|
||
|
|
<IconButton size="small" onClick={handleMenuClick}>
|
||
|
|
<MoreVertIcon fontSize="small" />
|
||
|
|
</IconButton>
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{/* 状态和得分 */}
|
||
|
|
<Box sx={styles.taskCardStatus}>
|
||
|
|
<Chip
|
||
|
|
icon={<StatusIcon sx={{ fontSize: 14 }} />}
|
||
|
|
label={t(statusConfig.label)}
|
||
|
|
color={statusConfig.color}
|
||
|
|
size="small"
|
||
|
|
variant="outlined"
|
||
|
|
sx={{ height: 24, '& .MuiChip-label': { px: 1, fontSize: '0.7rem' } }}
|
||
|
|
/>
|
||
|
|
{finalScore !== undefined && status === 1 && (
|
||
|
|
<Chip
|
||
|
|
label={`${finalScore.toFixed(1)}%`}
|
||
|
|
color={getScoreColor(finalScore)}
|
||
|
|
size="small"
|
||
|
|
sx={{ height: 24, fontWeight: 600, '& .MuiChip-label': { px: 1 } }}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{/* 进度条 */}
|
||
|
|
{status === 0 && (
|
||
|
|
<Box sx={styles.taskCardProgress}>
|
||
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
|
||
|
|
<Typography variant="caption" color="text.secondary">
|
||
|
|
{t('evalTasks.progress')}
|
||
|
|
</Typography>
|
||
|
|
<Typography variant="caption" color="primary" fontWeight={600}>
|
||
|
|
{completedCount}/{totalCount}
|
||
|
|
</Typography>
|
||
|
|
</Box>
|
||
|
|
<LinearProgress variant="determinate" value={progress} sx={styles.progressBar} />
|
||
|
|
</Box>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 统计信息 */}
|
||
|
|
<Box sx={styles.taskCardStats}>
|
||
|
|
<Chip
|
||
|
|
icon={<QuizIcon sx={{ fontSize: 14 }} />}
|
||
|
|
label={`${totalCount} ${t('evalTasks.questions')}`}
|
||
|
|
size="small"
|
||
|
|
variant="outlined"
|
||
|
|
sx={{ height: 22, '& .MuiChip-label': { px: 0.75, fontSize: '0.7rem' } }}
|
||
|
|
/>
|
||
|
|
{detail?.hasSubjectiveQuestions && (
|
||
|
|
<Chip
|
||
|
|
label={t('evalTasks.hasSubjective')}
|
||
|
|
size="small"
|
||
|
|
color="info"
|
||
|
|
variant="outlined"
|
||
|
|
sx={{ height: 22, '& .MuiChip-label': { px: 0.75, fontSize: '0.7rem' } }}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</Box>
|
||
|
|
|
||
|
|
{/* 时间 */}
|
||
|
|
<Typography sx={{ ...styles.taskCardTime, mt: 1.5 }} color="text.disabled">
|
||
|
|
{new Date(createAt).toLocaleString()}
|
||
|
|
</Typography>
|
||
|
|
</CardContent>
|
||
|
|
|
||
|
|
{/* 菜单 */}
|
||
|
|
<Menu anchorEl={anchorEl} open={open} onClose={handleMenuClose} onClick={e => e.stopPropagation()}>
|
||
|
|
<MenuItem onClick={handleAction(onView)}>
|
||
|
|
<ListItemIcon>
|
||
|
|
<VisibilityIcon fontSize="small" />
|
||
|
|
</ListItemIcon>
|
||
|
|
{t('datasets.viewDetails')}
|
||
|
|
</MenuItem>
|
||
|
|
{status === 0 && (
|
||
|
|
<MenuItem onClick={handleAction(onInterrupt)}>
|
||
|
|
<ListItemIcon>
|
||
|
|
<StopIcon fontSize="small" color="warning" />
|
||
|
|
</ListItemIcon>
|
||
|
|
{t('evalTasks.interrupt')}
|
||
|
|
</MenuItem>
|
||
|
|
)}
|
||
|
|
<MenuItem onClick={handleAction(onDelete)} sx={{ color: 'error.main' }}>
|
||
|
|
<ListItemIcon>
|
||
|
|
<DeleteIcon fontSize="small" color="error" />
|
||
|
|
</ListItemIcon>
|
||
|
|
{t('common.delete')}
|
||
|
|
</MenuItem>
|
||
|
|
</Menu>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|