336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { Card, CardContent, Box, Typography, Chip, Checkbox, IconButton, Tooltip, Divider } from '@mui/material';
|
|||
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|||
|
|
import EditIcon from '@mui/icons-material/Edit';
|
|||
|
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|||
|
|
import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
|
|||
|
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
|||
|
|
import ShortTextIcon from '@mui/icons-material/ShortText';
|
|||
|
|
import NotesIcon from '@mui/icons-material/Notes';
|
|||
|
|
import CheckIcon from '@mui/icons-material/Check';
|
|||
|
|
import CloseIcon from '@mui/icons-material/Close';
|
|||
|
|
import { useTheme, alpha } from '@mui/material/styles';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import { useRouter } from 'next/navigation';
|
|||
|
|
|
|||
|
|
// 题型图标和颜色映射
|
|||
|
|
const QUESTION_TYPE_CONFIG = {
|
|||
|
|
true_false: {
|
|||
|
|
icon: CheckCircleIcon,
|
|||
|
|
color: 'success',
|
|||
|
|
bgColor: 'success.light'
|
|||
|
|
},
|
|||
|
|
single_choice: {
|
|||
|
|
icon: RadioButtonCheckedIcon,
|
|||
|
|
color: 'primary',
|
|||
|
|
bgColor: 'primary.light'
|
|||
|
|
},
|
|||
|
|
multiple_choice: {
|
|||
|
|
icon: CheckBoxIcon,
|
|||
|
|
color: 'secondary',
|
|||
|
|
bgColor: 'secondary.light'
|
|||
|
|
},
|
|||
|
|
short_answer: {
|
|||
|
|
icon: ShortTextIcon,
|
|||
|
|
color: 'warning',
|
|||
|
|
bgColor: 'warning.light'
|
|||
|
|
},
|
|||
|
|
open_ended: {
|
|||
|
|
icon: NotesIcon,
|
|||
|
|
color: 'info',
|
|||
|
|
bgColor: 'info.light'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default function EvalDatasetCard({ item, selected, onSelect, onEdit, onDelete, projectId }) {
|
|||
|
|
const theme = useTheme();
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
const router = useRouter();
|
|||
|
|
|
|||
|
|
const typeConfig = QUESTION_TYPE_CONFIG[item.questionType] || QUESTION_TYPE_CONFIG.short_answer;
|
|||
|
|
const TypeIcon = typeConfig.icon;
|
|||
|
|
|
|||
|
|
// 解析选项
|
|||
|
|
const options = item.options
|
|||
|
|
? typeof item.options === 'string'
|
|||
|
|
? JSON.parse(item.options || '[]')
|
|||
|
|
: item.options
|
|||
|
|
: [];
|
|||
|
|
|
|||
|
|
// 解析答案
|
|||
|
|
const correctAnswer = item.correctAnswer;
|
|||
|
|
|
|||
|
|
const handleCardClick = e => {
|
|||
|
|
// 如果点击的是复选框或按钮,不跳转
|
|||
|
|
if (e.target.closest('.MuiCheckbox-root') || e.target.closest('.MuiIconButton-root')) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
router.push(`/projects/${projectId}/eval-datasets/${item.id}`);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Card
|
|||
|
|
variant="outlined"
|
|||
|
|
onClick={handleCardClick}
|
|||
|
|
sx={{
|
|||
|
|
height: 'fit-content',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
transition: 'all 0.2s ease-in-out',
|
|||
|
|
borderColor: selected ? theme.palette.primary.main : theme.palette.divider,
|
|||
|
|
bgcolor: selected ? alpha(theme.palette.primary.main, 0.04) : 'background.paper',
|
|||
|
|
borderRadius: 2,
|
|||
|
|
position: 'relative',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
'&:hover': {
|
|||
|
|
borderColor: theme.palette.primary.main,
|
|||
|
|
transform: 'translateY(-4px)',
|
|||
|
|
boxShadow: `0 8px 24px ${alpha(theme.palette.common.black, 0.08)}`
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<CardContent sx={{ flex: 1, display: 'flex', flexDirection: 'column', p: 2.5 }}>
|
|||
|
|
{/* 头部:题型标签和操作 */}
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 2 }}>
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
|||
|
|
<Checkbox
|
|||
|
|
size="small"
|
|||
|
|
checked={selected}
|
|||
|
|
onChange={e => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
onSelect(item.id);
|
|||
|
|
}}
|
|||
|
|
sx={{ p: 0.5, ml: -0.5 }}
|
|||
|
|
/>
|
|||
|
|
<Chip
|
|||
|
|
icon={<TypeIcon sx={{ fontSize: '16px !important' }} />}
|
|||
|
|
label={t(`eval.questionTypes.${item.questionType}`)}
|
|||
|
|
size="small"
|
|||
|
|
color={typeConfig.color}
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{
|
|||
|
|
fontWeight: 600,
|
|||
|
|
borderWidth: '1.5px',
|
|||
|
|
bgcolor: alpha(theme.palette[typeConfig.color].main, 0.05)
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
|||
|
|
<Tooltip title={t('common.edit')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
onClick={e => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
onEdit(item);
|
|||
|
|
}}
|
|||
|
|
sx={{
|
|||
|
|
color: 'text.secondary',
|
|||
|
|
'&:hover': { color: 'primary.main', bgcolor: alpha(theme.palette.primary.main, 0.1) }
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<EditIcon fontSize="small" />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
<Tooltip title={t('common.delete')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
onClick={e => {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
onDelete(item.id);
|
|||
|
|
}}
|
|||
|
|
sx={{
|
|||
|
|
color: 'text.secondary',
|
|||
|
|
'&:hover': { color: 'error.main', bgcolor: alpha(theme.palette.error.main, 0.1) }
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<DeleteIcon fontSize="small" />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 问题内容 */}
|
|||
|
|
<Box sx={{ mb: 2 }}>
|
|||
|
|
<Typography
|
|||
|
|
variant="body1"
|
|||
|
|
sx={{
|
|||
|
|
fontWeight: 600,
|
|||
|
|
lineHeight: 1.6,
|
|||
|
|
color:
|
|||
|
|
item.questionType === 'true_false'
|
|||
|
|
? correctAnswer === '✅'
|
|||
|
|
? 'success.main'
|
|||
|
|
: 'error.main'
|
|||
|
|
: 'text.primary',
|
|||
|
|
display: 'inline'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{item.questionType === 'true_false' && correctAnswer} {item.question}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 选项列表(仅单选/多选显示) */}
|
|||
|
|
{(item.questionType === 'single_choice' || item.questionType === 'multiple_choice') && options.length > 0 && (
|
|||
|
|
<Box sx={{ mb: 2, flex: 1 }}>
|
|||
|
|
{(item.questionType === 'multiple_choice' ? options : options.slice(0, 4)).map((option, index) => {
|
|||
|
|
const optionLabel = String.fromCharCode(65 + index); // A, B, C, D
|
|||
|
|
// 解析多选题答案,支持多种格式:数组、JSON字符串、逗号分隔字符串
|
|||
|
|
const parseMultipleAnswers = answer => {
|
|||
|
|
if (Array.isArray(answer)) return answer;
|
|||
|
|
if (!answer) return [];
|
|||
|
|
// 尝试解析 JSON 数组
|
|||
|
|
if (answer.startsWith('[')) {
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(answer);
|
|||
|
|
} catch (e) {
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 逗号分隔字符串格式,如 "A,B,D"
|
|||
|
|
return answer.split(',').map(s => s.trim());
|
|||
|
|
};
|
|||
|
|
const isCorrect =
|
|||
|
|
item.questionType === 'multiple_choice'
|
|||
|
|
? parseMultipleAnswers(correctAnswer).includes(optionLabel)
|
|||
|
|
: correctAnswer === optionLabel;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Box
|
|||
|
|
key={index}
|
|||
|
|
sx={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'baseline',
|
|||
|
|
gap: 1,
|
|||
|
|
mb: 0.5,
|
|||
|
|
p: '4px 8px',
|
|||
|
|
borderRadius: 1,
|
|||
|
|
bgcolor: isCorrect ? alpha(theme.palette.success.main, 0.08) : 'transparent',
|
|||
|
|
border: '1px solid',
|
|||
|
|
borderColor: isCorrect ? alpha(theme.palette.success.main, 0.3) : 'transparent'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Typography
|
|||
|
|
variant="caption"
|
|||
|
|
sx={{
|
|||
|
|
fontWeight: 700,
|
|||
|
|
color: isCorrect ? 'success.main' : 'text.secondary',
|
|||
|
|
minWidth: 16
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{optionLabel}.
|
|||
|
|
</Typography>
|
|||
|
|
<Typography
|
|||
|
|
variant="body2"
|
|||
|
|
sx={{
|
|||
|
|
color: isCorrect ? 'success.dark' : 'text.secondary',
|
|||
|
|
fontSize: '0.875rem',
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
textOverflow: 'ellipsis',
|
|||
|
|
whiteSpace: 'nowrap'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{option}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
{item.questionType === 'single_choice' && options.length > 4 && (
|
|||
|
|
<Typography variant="caption" color="text.disabled" sx={{ pl: 1, mt: 0.5, display: 'block' }}>
|
|||
|
|
... +{options.length - 4} {t('eval.moreOptions')}
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 非选择题且非判断题答案 */}
|
|||
|
|
{item.questionType !== 'single_choice' &&
|
|||
|
|
item.questionType !== 'multiple_choice' &&
|
|||
|
|
item.questionType !== 'true_false' &&
|
|||
|
|
correctAnswer && (
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
p: 1.5,
|
|||
|
|
borderRadius: 2,
|
|||
|
|
bgcolor: 'background.paper',
|
|||
|
|
border: '1px dashed',
|
|||
|
|
borderColor: 'divider',
|
|||
|
|
mb: 2,
|
|||
|
|
flex: 1
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
|||
|
|
{t('eval.answer')}:
|
|||
|
|
</Typography>
|
|||
|
|
<Typography
|
|||
|
|
variant="body2"
|
|||
|
|
sx={{
|
|||
|
|
color: 'text.secondary',
|
|||
|
|
display: '-webkit-box',
|
|||
|
|
WebkitLineClamp: 4,
|
|||
|
|
WebkitBoxOrient: 'vertical',
|
|||
|
|
overflow: 'hidden'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{correctAnswer}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Divider sx={{ my: 1.5, opacity: 0.6 }} />
|
|||
|
|
|
|||
|
|
{/* 底部元信息 */}
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
|||
|
|
{item.chunks ? (
|
|||
|
|
<Tooltip title={item.chunks.name || item.chunks.fileName}>
|
|||
|
|
<Chip
|
|||
|
|
label={item.chunks.name || item.chunks.fileName}
|
|||
|
|
size="small"
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{
|
|||
|
|
fontSize: 11,
|
|||
|
|
height: 22,
|
|||
|
|
maxWidth: 140,
|
|||
|
|
borderColor: 'divider',
|
|||
|
|
color: 'text.secondary'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
) : (
|
|||
|
|
<Box />
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{item.tags && (
|
|||
|
|
<Tooltip title={item.tags}>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 0.5, overflow: 'hidden', maxWidth: 120 }}>
|
|||
|
|
{item.tags
|
|||
|
|
.split(/[,,]/)
|
|||
|
|
.slice(0, 2)
|
|||
|
|
.map((tag, index) => (
|
|||
|
|
<Chip
|
|||
|
|
key={index}
|
|||
|
|
label={tag}
|
|||
|
|
size="small"
|
|||
|
|
sx={{
|
|||
|
|
fontSize: 11,
|
|||
|
|
height: 22,
|
|||
|
|
bgcolor: alpha(theme.palette.info.main, 0.08),
|
|||
|
|
color: 'info.dark',
|
|||
|
|
maxWidth: 80
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
))}
|
|||
|
|
{item.tags.split(/[,,]/).length > 2 && (
|
|||
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: 11, alignSelf: 'center' }}>
|
|||
|
|
+{item.tags.split(/[,,]/).length - 2}
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
</Tooltip>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
);
|
|||
|
|
}
|