first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,328 @@
'use client';
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Box,
Typography,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
Divider,
CircularProgress,
FormHelperText
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import ModelSelector from './ModelSelector';
import QuestionFilter from './QuestionFilter';
import ScoreAnchorsForm from './ScoreAnchorsForm';
import { useEvalTaskForm } from '../hooks/useEvalTaskForm';
import { useEffect } from 'react';
export default function CreateEvalTaskDialog({ open, onClose, projectId, onSuccess }) {
const { t, i18n } = useTranslation();
const [submitting, setSubmitting] = useState(false);
const {
models,
selectedModels,
setSelectedModels,
judgeModel,
setJudgeModel,
evalDatasets,
availableTags,
questionTypes,
setQuestionTypes,
selectedTags,
setSelectedTags,
searchKeyword,
setSearchKeyword,
questionCount,
setQuestionCount,
filteredTotal,
sampledIds,
hasSubjectiveQuestions,
hasShortAnswer,
hasOpenEnded,
shortAnswerScoreAnchors,
setShortAnswerScoreAnchors,
openEndedScoreAnchors,
setOpenEndedScoreAnchors,
initScoreAnchors,
loading,
error,
setError,
setSampledIds,
resetFilters,
resetForm
} = useEvalTaskForm(projectId, open);
// 当有主观题时,初始化评分规则
useEffect(() => {
if (hasSubjectiveQuestions && open) {
initScoreAnchors(i18n.language === 'zh-CN' ? 'zh-CN' : 'en');
}
}, [hasSubjectiveQuestions, open, i18n.language]);
// 统计各题型数量
const typeStats = {};
evalDatasets.forEach(d => {
typeStats[d.questionType] = (typeStats[d.questionType] || 0) + 1;
});
const getModelKey = model => `${model.providerId}::${model.modelId}`;
const handleModelSelectionChange = newSelection => {
setSelectedModels(newSelection);
setError('');
};
const handleSubmit = async () => {
// 先清除之前的错误
setError('');
// 验证
if (selectedModels.length === 0) {
setError(t('evalTasks.errorNoModels'));
return;
}
if (filteredTotal === 0) {
setError(t('evalTasks.errorNoQuestions'));
return;
}
if (hasSubjectiveQuestions && !judgeModel) {
setError(t('evalTasks.errorNoJudgeModel'));
return;
}
// 验证教师模型不在测试模型中
if (judgeModel && selectedModels.includes(judgeModel)) {
setError(t('evalTasks.errorJudgeSameAsTest'));
return;
}
try {
setSubmitting(true);
setError('');
// 解析选中的模型
const models = selectedModels.map(m => {
const [providerId, modelId] = m.split('::');
return { modelId, providerId }; // 注意顺序modelId 在前
});
// 解析教师模型
let judgeModelId = null;
let judgeProviderId = null;
if (judgeModel) {
const [pId, mId] = judgeModel.split('::');
judgeProviderId = pId;
judgeModelId = mId;
}
// 调用后端采样接口获取题目 ID
const sampleBody = {
questionTypes: questionTypes,
tags: selectedTags,
keyword: searchKeyword.trim() || '',
limit: questionCount > 0 ? questionCount : undefined
};
const sampleResponse = await fetch(`/api/projects/${projectId}/eval-datasets/sample`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sampleBody)
});
const sampleResult = await sampleResponse.json();
if (!sampleResponse.ok || sampleResult.code !== 0) {
setError(sampleResult.error || t('evalTasks.errorCreateFailed'));
return;
}
const ids = sampleResult?.data?.ids || [];
if (ids.length === 0) {
setError(t('evalTasks.errorNoQuestions'));
return;
}
setSampledIds(ids);
// 构建自定义评分规则对象
const customScoreAnchors = {};
if (hasShortAnswer && shortAnswerScoreAnchors.length > 0) {
customScoreAnchors.short_answer = shortAnswerScoreAnchors;
}
if (hasOpenEnded && openEndedScoreAnchors.length > 0) {
customScoreAnchors.open_ended = openEndedScoreAnchors;
}
// 创建任务
const response = await fetch(`/api/projects/${projectId}/eval-tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
models, // 后端期望的字段名
judgeModelId, // 分开传递
judgeProviderId, // 分开传递
evalDatasetIds: ids,
language: i18n.language === 'zh-CN' ? 'zh-CN' : 'en',
customScoreAnchors: Object.keys(customScoreAnchors).length > 0 ? customScoreAnchors : undefined
})
});
const result = await response.json();
if (result.code === 0) {
onSuccess && onSuccess(result.data);
handleClose();
} else {
setError(result.error || t('evalTasks.errorCreateFailed'));
}
} catch (err) {
console.error('创建评估任务失败:', err);
setError(t('evalTasks.errorCreateFailed'));
} finally {
setSubmitting(false);
}
};
const handleClose = () => {
resetForm();
onClose();
};
const handleJudgeModelChange = event => {
setJudgeModel(event.target.value);
setError('');
};
return (
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>{t('evalTasks.createTitle')}</DialogTitle>
<DialogContent>
<Box sx={{ mt: 1 }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
{error}
</Alert>
)}
{/* 选择测试模型 */}
<ModelSelector
models={models}
selectedModels={selectedModels}
onSelectionChange={handleModelSelectionChange}
error={selectedModels.length === 0 && error}
/>
<Divider sx={{ my: 2 }} />
{/* 题目筛选 */}
<QuestionFilter
questionTypes={questionTypes}
selectedTags={selectedTags}
searchKeyword={searchKeyword}
questionCount={questionCount}
availableTags={availableTags}
typeStats={typeStats}
filteredCount={filteredTotal}
onQuestionTypesChange={setQuestionTypes}
onTagsChange={setSelectedTags}
onSearchChange={setSearchKeyword}
onQuestionCountChange={setQuestionCount}
onReset={resetFilters}
/>
{/* 最终题目统计 */}
<Box sx={{ mb: 3, p: 2, bgcolor: 'action.hover', borderRadius: 1 }}>
<Typography variant="body2" color="text.secondary">
{t('evalTasks.finalSelection')}
<strong>{sampledIds.length || (questionCount > 0 ? questionCount : filteredTotal)}</strong>{' '}
{t('evalTasks.questionsSuffix')}
</Typography>
{hasSubjectiveQuestions && (
<Typography variant="body2" color="warning.main" sx={{ mt: 1 }}>
{t('evalTasks.hasSubjectiveHint')}
</Typography>
)}
</Box>
{/* 选择教师模型(仅当有主观题时显示) */}
{hasSubjectiveQuestions && (
<>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>{t('evalTasks.selectJudgeModel')} *</InputLabel>
<Select
value={judgeModel}
onChange={handleJudgeModelChange}
label={`${t('evalTasks.selectJudgeModel')} *`}
>
<MenuItem value="">
<em>{t('evalTasks.selectJudgeModelPlaceholder')}</em>
</MenuItem>
{models
.filter(m => {
const key = `${m.providerId}::${m.modelId}`;
return !selectedModels.includes(key);
})
.map(model => {
const key = `${model.providerId}::${model.modelId}`;
return (
<MenuItem key={key} value={key}>
{model.providerName} / {model.modelName}
</MenuItem>
);
})}
</Select>
<FormHelperText>{t('evalTasks.selectJudgeModelHint')}</FormHelperText>
</FormControl>
{/* 简答题评分规则 */}
{hasShortAnswer && (
<ScoreAnchorsForm
questionType="short_answer"
scoreAnchors={shortAnswerScoreAnchors}
onChange={setShortAnswerScoreAnchors}
language={i18n.language}
/>
)}
{/* 开放题评分规则 */}
{hasOpenEnded && (
<ScoreAnchorsForm
questionType="open_ended"
scoreAnchors={openEndedScoreAnchors}
onChange={setOpenEndedScoreAnchors}
language={i18n.language}
/>
)}
</>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={submitting}>
{t('common.cancel')}
</Button>
<Button
onClick={handleSubmit}
variant="contained"
disabled={submitting || loading}
startIcon={submitting && <CircularProgress size={16} />}
>
{submitting ? t('common.creating') : t('evalTasks.startEval')}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -0,0 +1,183 @@
'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>
);
}

View File

@@ -0,0 +1,87 @@
'use client';
import {
Box,
Typography,
Checkbox,
FormHelperText,
FormControl,
InputLabel,
Select,
MenuItem,
ListItemText,
OutlinedInput,
Chip
} from '@mui/material';
import { useTranslation } from 'react-i18next';
export default function ModelSelector({ models, selectedModels, onSelectionChange, error }) {
const { t } = useTranslation();
const getModelKey = model => `${model.providerId}::${model.modelId}`;
const handleChange = event => {
const {
target: { value }
} = event;
// On autofill we get a stringified value.
onSelectionChange(typeof value === 'string' ? value.split(',') : value);
};
const getModelLabel = modelKey => {
const model = models.find(m => getModelKey(m) === modelKey);
if (!model) return modelKey;
return `${model.providerName || model.providerId} / ${model.modelName || model.modelId}`;
};
return (
<Box sx={{ mb: 3 }}>
<FormControl fullWidth error={!!error} size="small">
<InputLabel id="model-selector-label">{t('evalTasks.selectModels')} *</InputLabel>
<Select
labelId="model-selector-label"
multiple
value={selectedModels}
onChange={handleChange}
input={<OutlinedInput label={`${t('evalTasks.selectModels')} *`} />}
renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map(value => (
<Chip key={value} label={getModelLabel(value)} size="small" />
))}
</Box>
)}
MenuProps={{
PaperProps: {
style: {
maxHeight: 300,
width: 250
}
}
}}
>
{models.length === 0 ? (
<MenuItem disabled value="">
<Typography variant="body2" color="text.secondary">
{t('evalTasks.noModelsAvailable')}
</Typography>
</MenuItem>
) : (
models.map(model => {
const modelKey = getModelKey(model);
return (
<MenuItem key={modelKey} value={modelKey}>
<Checkbox checked={selectedModels.includes(modelKey)} />
<ListItemText
primary={`${model.providerName || model.providerId} / ${model.modelName || model.modelId}`}
/>
</MenuItem>
);
})
)}
</Select>
<FormHelperText>{error || t('evalTasks.selectModelsHint')}</FormHelperText>
</FormControl>
</Box>
);
}

View File

@@ -0,0 +1,160 @@
'use client';
import {
Box,
Typography,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
Chip,
OutlinedInput,
Checkbox,
ListItemText,
Slider,
Button
} from '@mui/material';
import FilterAltIcon from '@mui/icons-material/FilterAlt';
import ClearIcon from '@mui/icons-material/Clear';
import { useTranslation } from 'react-i18next';
const QUESTION_TYPES = [
{ value: 'true_false', labelKey: 'eval.questionTypes.true_false' },
{ value: 'single_choice', labelKey: 'eval.questionTypes.single_choice' },
{ value: 'multiple_choice', labelKey: 'eval.questionTypes.multiple_choice' },
{ value: 'short_answer', labelKey: 'eval.questionTypes.short_answer' },
{ value: 'open_ended', labelKey: 'eval.questionTypes.open_ended' }
];
export default function QuestionFilter({
questionTypes,
selectedTags,
searchKeyword,
questionCount,
availableTags,
filteredCount,
onQuestionTypesChange,
onTagsChange,
onSearchChange,
onQuestionCountChange,
onReset
}) {
const { t } = useTranslation();
const hasFilters = questionTypes.length > 0 || selectedTags.length > 0 || searchKeyword || questionCount > 0;
return (
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<FilterAltIcon sx={{ mr: 1, color: 'primary.main' }} />
<Typography variant="subtitle2" sx={{ fontWeight: 600, flex: 1 }}>
{t('evalTasks.filterTitle')}
</Typography>
{hasFilters && (
<Button size="small" startIcon={<ClearIcon />} onClick={onReset}>
{t('evalTasks.clearFilter')}
</Button>
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* 关键字搜索 */}
<TextField
fullWidth
size="small"
label={t('evalTasks.searchKeyword')}
placeholder={t('evalTasks.searchPlaceholder')}
value={searchKeyword}
onChange={e => onSearchChange(e.target.value)}
/>
{/* 题型和标签筛选 - 并排显示 */}
<Box sx={{ display: 'flex', gap: 2 }}>
{/* 题型筛选 */}
<FormControl fullWidth size="small">
<InputLabel>{t('evalTasks.filterByTypeLabel')}</InputLabel>
<Select
multiple
value={questionTypes}
onChange={e => onQuestionTypesChange(e.target.value)}
input={<OutlinedInput label={t('evalTasks.filterByTypeLabel')} />}
renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map(value => (
<Chip key={value} label={t(`eval.questionTypes.${value}`)} size="small" />
))}
</Box>
)}
>
{QUESTION_TYPES.map(type => (
<MenuItem key={type.value} value={type.value}>
<Checkbox checked={questionTypes.includes(type.value)} />
<ListItemText primary={`${t(type.labelKey)} `} />
</MenuItem>
))}
</Select>
</FormControl>
{/* 标签筛选 */}
{availableTags.length > 0 && (
<FormControl fullWidth size="small">
<InputLabel>{t('evalTasks.filterByTagLabel')}</InputLabel>
<Select
multiple
value={selectedTags}
onChange={e => onTagsChange(e.target.value)}
input={<OutlinedInput label={t('evalTasks.filterByTagLabel')} />}
renderValue={selected => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map(value => (
<Chip key={value} label={value} size="small" />
))}
</Box>
)}
>
{availableTags.map(tag => (
<MenuItem key={tag} value={tag}>
<Checkbox checked={selectedTags.includes(tag)} />
<ListItemText primary={tag} />
</MenuItem>
))}
</Select>
</FormControl>
)}
</Box>
{/* 题目数量选择 - 紧凑布局 */}
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ flex: 1 }}>
{t('evalTasks.questionCountLabel')}
{questionCount === 0 ? t('common.all') : questionCount} / {filteredCount}
</Typography>
<TextField
size="small"
type="number"
value={questionCount}
onChange={e => onQuestionCountChange(parseInt(e.target.value) || 0)}
inputProps={{ min: 0, max: filteredCount }}
sx={{ width: 100 }}
/>
</Box>
<Slider
value={questionCount}
onChange={(e, value) => onQuestionCountChange(value)}
min={0}
max={filteredCount}
step={1}
valueLabelDisplay="auto"
/>
<Typography variant="caption" color="text.secondary">
{questionCount === 0
? t('evalTasks.useAllQuestions')
: t('evalTasks.randomSampleHint', { filteredCount, questionCount })}
</Typography>
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,143 @@
'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
TextField,
Accordion,
AccordionSummary,
AccordionDetails,
Chip,
IconButton,
Tooltip
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import RestoreIcon from '@mui/icons-material/Restore';
import { useTranslation } from 'react-i18next';
import { getDefaultScoreAnchors } from '@/lib/llm/prompts/llmJudge';
/**
* 评分规则表单组件
* 用于自定义简答题和开放题的评分规则
*/
export default function ScoreAnchorsForm({
questionType, // 'short_answer' 或 'open_ended'
scoreAnchors,
onChange,
language = 'zh-CN'
}) {
const { t, i18n } = useTranslation();
const [expanded, setExpanded] = useState(false);
// 获取当前语言
const currentLanguage = i18n.language === 'zh-CN' ? 'zh-CN' : 'en';
// 初始化评分规则(如果为空)
useEffect(() => {
if (!scoreAnchors || scoreAnchors.length === 0) {
onChange(getDefaultScoreAnchors(questionType, currentLanguage));
}
}, [questionType, currentLanguage]);
// 处理单个规则的描述更改
const handleDescriptionChange = (index, newDescription) => {
const newAnchors = [...scoreAnchors];
newAnchors[index] = { ...newAnchors[index], description: newDescription };
onChange(newAnchors);
};
// 恢复默认值
const handleRestore = () => {
onChange(getDefaultScoreAnchors(questionType, currentLanguage));
};
// 获取题型显示名称
const getQuestionTypeName = () => {
if (questionType === 'short_answer') {
return t('evalTasks.shortAnswer', '简答题');
}
return t('evalTasks.openEnded', '开放题');
};
// 获取分数区间的颜色
const getScoreColor = range => {
if (range === '1.0') return 'success';
if (range.includes('0.8') || range.includes('0.9')) return 'info';
if (range.includes('0.6') || range.includes('0.7')) return 'warning';
return 'error';
};
if (!scoreAnchors || scoreAnchors.length === 0) {
return null;
}
return (
<Accordion
expanded={expanded}
onChange={(e, isExpanded) => setExpanded(isExpanded)}
sx={{
mb: 2,
'&:before': { display: 'none' },
boxShadow: 1
}}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
sx={{
bgcolor: 'action.hover',
'&:hover': { bgcolor: 'action.selected' }
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%' }}>
<Typography variant="subtitle2" fontWeight={600}>
{t('evalTasks.scoreAnchorsTitle', '{{type}}评分规则', { type: getQuestionTypeName() })}
</Typography>
<Chip label={t('evalTasks.customizable', '可自定义')} size="small" color="primary" variant="outlined" />
</Box>
</AccordionSummary>
<AccordionDetails>
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="body2" color="text.secondary">
{t('evalTasks.scoreAnchorsHint', '自定义评分标准用于指导LLM评估模型的回答质量')}
</Typography>
<Tooltip title={t('evalTasks.restoreDefault', '恢复默认')}>
<IconButton size="small" onClick={handleRestore} color="primary">
<RestoreIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
{scoreAnchors.map((anchor, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Chip
label={anchor.range}
size="small"
color={getScoreColor(anchor.range)}
sx={{ minWidth: 70, fontWeight: 600 }}
/>
<Typography variant="caption" color="text.secondary">
{t('evalTasks.scoreRange', '分数区间')}
</Typography>
</Box>
<TextField
fullWidth
size="small"
multiline
rows={2}
value={anchor.description}
onChange={e => handleDescriptionChange(index, e.target.value)}
placeholder={t('evalTasks.scoreDescriptionPlaceholder', '请输入该分数区间的评分标准描述...')}
sx={{
'& .MuiOutlinedInput-root': {
fontSize: '0.875rem'
}
}}
/>
</Box>
))}
</AccordionDetails>
</Accordion>
);
}