Files
YG-Datasets/easy-dataset-main/app/projects/[projectId]/blind-test-tasks/components/BlindTestTaskCard.js

317 lines
11 KiB
JavaScript
Raw Normal View History

2026-03-17 14:36:31 +08:00
'use client';
import {
Box,
Card,
CardContent,
Typography,
Chip,
IconButton,
Menu,
MenuItem,
LinearProgress,
Avatar,
Grid,
Tooltip,
Divider
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import DeleteIcon from '@mui/icons-material/Delete';
import StopIcon from '@mui/icons-material/Stop';
import VisibilityIcon from '@mui/icons-material/Visibility';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { alpha, useTheme } from '@mui/material/styles';
const STATUS_MAP = {
0: { label: 'blindTest.statusProcessing', color: 'primary', bgColor: 'primary.main' },
1: { label: 'blindTest.statusCompleted', color: 'success', bgColor: 'success.main' },
2: { label: 'blindTest.statusFailed', color: 'error', bgColor: 'error.main' },
3: { label: 'blindTest.statusInterrupted', color: 'warning', bgColor: 'warning.main' }
};
export default function BlindTestTaskCard({ task, onView, onDelete, onInterrupt, onContinue }) {
const { t } = useTranslation();
const theme = useTheme();
const [anchorEl, setAnchorEl] = useState(null);
const handleMenuOpen = e => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
const handleView = e => {
e?.stopPropagation?.();
handleMenuClose();
onView?.(task);
};
const handleDelete = e => {
e?.stopPropagation?.();
handleMenuClose();
onDelete?.(task);
};
const handleInterrupt = e => {
e?.stopPropagation?.();
handleMenuClose();
onInterrupt?.(task);
};
const handleContinue = e => {
e?.stopPropagation?.();
handleMenuClose();
onContinue?.(task);
};
const statusConfig = STATUS_MAP[task.status] || STATUS_MAP[0];
const progress = task.totalCount > 0 ? (task.completedCount / task.totalCount) * 100 : 0;
const isProcessing = task.status === 0;
const isCompleted = task.status === 1;
// 计算模型得分
const results = task.detail?.results || [];
const modelAScore = results.reduce((sum, r) => sum + (r.modelAScore || 0), 0);
const modelBScore = results.reduce((sum, r) => sum + (r.modelBScore || 0), 0);
const totalScore = modelAScore + modelBScore;
// Calculate win percentages for visual bar
const modelAPercent = totalScore > 0 ? (modelAScore / totalScore) * 100 : 50;
const modelBPercent = totalScore > 0 ? (modelBScore / totalScore) * 100 : 50;
const winner = isCompleted ? (modelAScore > modelBScore ? 'A' : modelBScore > modelAScore ? 'B' : 'Tie') : null;
return (
<Card
sx={{
width: '100%',
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
border: '1px solid',
borderColor: 'divider',
borderRadius: 3,
overflow: 'visible',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: `0 8px 24px -4px ${alpha(theme.palette.primary.main, 0.1)}`,
borderColor: 'primary.main'
}
}}
onClick={e => handleView(e)}
>
<CardContent sx={{ p: '20px !important' }}>
<Grid container alignItems="center" spacing={3}>
{/* Status & Time */}
<Grid item xs={12} md={2} lg={1.5}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Chip
label={t(statusConfig.label)}
size="small"
sx={{
bgcolor: alpha(theme.palette[statusConfig.color].main, 0.1),
color: `${statusConfig.color}.main`,
fontWeight: 600,
border: '1px solid',
borderColor: alpha(theme.palette[statusConfig.color].main, 0.2),
width: 'fit-content'
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, color: 'text.secondary' }}>
<AccessTimeIcon sx={{ fontSize: 14 }} />
<Typography variant="caption" noWrap>
{new Date(task.createAt).toLocaleDateString()}
</Typography>
</Box>
</Box>
</Grid>
{/* Model Comparison Area */}
<Grid item xs={12} md={9} lg={9.5}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
{/* Model A */}
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 2, minWidth: 0 }}>
<Avatar
sx={{
width: 40,
height: 40,
bgcolor: 'primary.main',
fontSize: '1rem',
boxShadow:
winner === 'A'
? `0 0 0 2px ${theme.palette.background.paper}, 0 0 0 4px ${theme.palette.primary.main}`
: 'none'
}}
>
A
</Avatar>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Tooltip title={task.modelInfo?.modelA?.modelName}>
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 600 }}>
{task.modelInfo?.modelA?.modelName || 'Model A'}
</Typography>
</Tooltip>
<Typography variant="caption" color="text.secondary" noWrap display="block">
{task.modelInfo?.modelA?.providerName}
</Typography>
</Box>
{isCompleted && winner === 'A' && <EmojiEventsIcon color="primary" />}
</Box>
{/* Center Status/Score */}
<Box
sx={{
width: 140,
textAlign: 'center',
px: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center'
}}
>
{isCompleted ? (
<Box sx={{ width: '100%' }}>
<Typography variant="h6" sx={{ fontWeight: 800, letterSpacing: 1, lineHeight: 1 }}>
<span style={{ color: theme.palette.primary.main }}>{modelAScore.toFixed(1)}</span>
<span style={{ color: theme.palette.text.disabled, margin: '0 4px', fontSize: '0.8em' }}>:</span>
<span style={{ color: theme.palette.secondary.main }}>{modelBScore.toFixed(1)}</span>
</Typography>
<Box
sx={{
display: 'flex',
height: 4,
borderRadius: 2,
overflow: 'hidden',
mt: 1,
width: '100%',
bgcolor: 'grey.100'
}}
>
<Box sx={{ width: `${modelAPercent}%`, bgcolor: 'primary.main' }} />
<Box sx={{ width: `${modelBPercent}%`, bgcolor: 'secondary.main' }} />
</Box>
</Box>
) : (
<Box sx={{ width: '100%' }}>
<Typography
variant="caption"
color="text.secondary"
fontWeight="bold"
sx={{ mb: 0.5, display: 'block' }}
>
VS
</Typography>
<LinearProgress
variant="determinate"
value={progress}
sx={{ height: 6, borderRadius: 3, bgcolor: 'action.hover' }}
/>
<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 0.5, display: 'block', fontSize: '0.7rem' }}
>
{Math.round(progress)}%
</Typography>
</Box>
)}
</Box>
{/* Model B */}
<Box
sx={{
flex: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
minWidth: 0,
flexDirection: 'row-reverse'
}}
>
<Avatar
sx={{
width: 40,
height: 40,
bgcolor: 'secondary.main',
fontSize: '1rem',
boxShadow:
winner === 'B'
? `0 0 0 2px ${theme.palette.background.paper}, 0 0 0 4px ${theme.palette.secondary.main}`
: 'none'
}}
>
B
</Avatar>
<Box sx={{ flex: 1, minWidth: 0, textAlign: 'right' }}>
<Tooltip title={task.modelInfo?.modelB?.modelName}>
<Typography variant="subtitle2" noWrap sx={{ fontWeight: 600 }}>
{task.modelInfo?.modelB?.modelName || 'Model B'}
</Typography>
</Tooltip>
<Typography variant="caption" color="text.secondary" noWrap display="block">
{task.modelInfo?.modelB?.providerName}
</Typography>
</Box>
{isCompleted && winner === 'B' && <EmojiEventsIcon color="secondary" />}
</Box>
</Box>
</Grid>
{/* Menu */}
<Grid item xs={12} md={1} lg={1} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<IconButton size="small" onClick={handleMenuOpen} sx={{ color: 'text.secondary' }}>
<MoreVertIcon />
</IconButton>
</Grid>
</Grid>
</CardContent>
{/* 菜单 */}
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
elevation: 2,
sx: {
mt: 1,
minWidth: 160,
borderRadius: 2,
border: '1px solid',
borderColor: 'divider'
}
}}
>
<MenuItem onClick={handleView} sx={{ gap: 1.5, py: 1 }}>
<VisibilityIcon fontSize="small" color="action" />
<Typography variant="body2">{t('blindTest.viewDetails', '查看详情')}</Typography>
</MenuItem>
{isProcessing && (
<MenuItem onClick={handleContinue} sx={{ gap: 1.5, py: 1 }}>
<PlayArrowIcon fontSize="small" color="primary" />
<Typography variant="body2">{t('blindTest.continue', '继续盲测')}</Typography>
</MenuItem>
)}
{isProcessing && (
<MenuItem onClick={handleInterrupt} sx={{ gap: 1.5, py: 1 }}>
<StopIcon fontSize="small" color="warning" />
<Typography variant="body2">{t('blindTest.interrupt', '中断任务')}</Typography>
</MenuItem>
)}
<Divider sx={{ my: 1 }} />
<MenuItem onClick={handleDelete} sx={{ gap: 1.5, py: 1, color: 'error.main' }}>
<DeleteIcon fontSize="small" />
<Typography variant="body2">{t('common.delete', '删除')}</Typography>
</MenuItem>
</Menu>
</Card>
);
}