Files

336 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}