375 lines
12 KiB
JavaScript
375 lines
12 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import { useState } from 'react';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import {
|
|||
|
|
Box,
|
|||
|
|
Typography,
|
|||
|
|
Checkbox,
|
|||
|
|
IconButton,
|
|||
|
|
Chip,
|
|||
|
|
Tooltip,
|
|||
|
|
Pagination,
|
|||
|
|
Divider,
|
|||
|
|
Paper,
|
|||
|
|
CircularProgress,
|
|||
|
|
TextField
|
|||
|
|
} from '@mui/material';
|
|||
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|||
|
|
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
|||
|
|
import EditIcon from '@mui/icons-material/Edit';
|
|||
|
|
import ChatIcon from '@mui/icons-material/Chat';
|
|||
|
|
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
|
|||
|
|
import { toast } from 'sonner';
|
|||
|
|
import { useAtomValue } from 'jotai';
|
|||
|
|
import { selectedModelInfoAtom } from '@/lib/store';
|
|||
|
|
|
|||
|
|
export default function QuestionListView({
|
|||
|
|
questions = [],
|
|||
|
|
currentPage,
|
|||
|
|
totalQuestions = 0,
|
|||
|
|
handlePageChange,
|
|||
|
|
selectedQuestions = [],
|
|||
|
|
onSelectQuestion,
|
|||
|
|
onDeleteQuestion,
|
|||
|
|
projectId,
|
|||
|
|
onEditQuestion,
|
|||
|
|
refreshQuestions
|
|||
|
|
}) {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
// 处理状态
|
|||
|
|
const [processingQuestions, setProcessingQuestions] = useState({});
|
|||
|
|
const { generateSingleDataset } = useGenerateDataset();
|
|||
|
|
// 获取当前选中的模型
|
|||
|
|
const selectedModelInfo = useAtomValue(selectedModelInfoAtom);
|
|||
|
|
|
|||
|
|
// 获取文本块的标题
|
|||
|
|
const getChunkTitle = content => {
|
|||
|
|
const firstLine = content ? content.split('\n')[0].trim() : '';
|
|||
|
|
if (firstLine.startsWith('# ')) {
|
|||
|
|
return firstLine.substring(2);
|
|||
|
|
} else if (firstLine.length > 0) {
|
|||
|
|
return firstLine.length > 200 ? firstLine.substring(0, 200) + '...' : firstLine;
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 检查问题是否被选中
|
|||
|
|
const isQuestionSelected = questionId => {
|
|||
|
|
return selectedQuestions.includes(questionId);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理生成数据集
|
|||
|
|
const handleGenerateDataset = async (questionId, questionInfo, imageId, imageName) => {
|
|||
|
|
// 设置处理状态
|
|||
|
|
setProcessingQuestions(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
[questionId]: true
|
|||
|
|
}));
|
|||
|
|
await generateSingleDataset({
|
|||
|
|
projectId,
|
|||
|
|
questionId,
|
|||
|
|
questionInfo,
|
|||
|
|
imageId,
|
|||
|
|
imageName
|
|||
|
|
});
|
|||
|
|
// 重置处理状态
|
|||
|
|
setProcessingQuestions(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
[questionId]: false
|
|||
|
|
}));
|
|||
|
|
refreshQuestions();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 处理生成多轮对话数据集
|
|||
|
|
const handleGenerateMultiTurnDataset = async (questionId, questionInfo) => {
|
|||
|
|
try {
|
|||
|
|
// 设置处理状态
|
|||
|
|
setProcessingQuestions(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
[`${questionId}_multi`]: true
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// 首先检查项目是否配置了多轮对话设置
|
|||
|
|
const configResponse = await fetch(`/api/projects/${projectId}/tasks`);
|
|||
|
|
if (!configResponse.ok) {
|
|||
|
|
throw new Error('获取项目配置失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const config = await configResponse.json();
|
|||
|
|
const multiTurnConfig = {
|
|||
|
|
systemPrompt: config.multiTurnSystemPrompt,
|
|||
|
|
scenario: config.multiTurnScenario,
|
|||
|
|
rounds: config.multiTurnRounds,
|
|||
|
|
roleA: config.multiTurnRoleA,
|
|||
|
|
roleB: config.multiTurnRoleB
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
console.log('multiTurnConfig:', multiTurnConfig);
|
|||
|
|
|
|||
|
|
// 检查是否已配置必要的多轮对话设置
|
|||
|
|
// 系统提示词是可选的,但场景、角色A、角色B和轮数是必需的
|
|||
|
|
if (
|
|||
|
|
!multiTurnConfig.scenario ||
|
|||
|
|
!multiTurnConfig.roleA ||
|
|||
|
|
!multiTurnConfig.roleB ||
|
|||
|
|
!multiTurnConfig.rounds ||
|
|||
|
|
multiTurnConfig.rounds < 1
|
|||
|
|
) {
|
|||
|
|
toast.error(t('questions.multiTurnNotConfigured', '请先在项目设置中配置多轮对话相关参数'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查是否选中了模型
|
|||
|
|
if (!selectedModelInfo) {
|
|||
|
|
toast.error(t('datasets.selectModelFirst', '请先选择模型'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用多轮对话生成API
|
|||
|
|
const response = await fetch(`/api/projects/${projectId}/dataset-conversations`, {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify({
|
|||
|
|
questionId,
|
|||
|
|
...multiTurnConfig,
|
|||
|
|
model: selectedModelInfo
|
|||
|
|
})
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!response.ok) {
|
|||
|
|
const errorData = await response.json();
|
|||
|
|
throw new Error(errorData.message || '生成多轮对话数据集失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await response.json();
|
|||
|
|
toast.success(t('questions.multiTurnGenerated', '多轮对话数据集生成成功!'));
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('生成多轮对话数据集失败:', error);
|
|||
|
|
toast.error(error.message || '生成多轮对话数据集失败');
|
|||
|
|
} finally {
|
|||
|
|
// 重置处理状态
|
|||
|
|
setProcessingQuestions(prev => ({
|
|||
|
|
...prev,
|
|||
|
|
[`${questionId}_multi`]: false
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Box style={{ padding: '20px' }}>
|
|||
|
|
{/* 问题列表 */}
|
|||
|
|
<Paper
|
|||
|
|
elevation={0}
|
|||
|
|
sx={{
|
|||
|
|
borderRadius: 2,
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
boxShadow: '0 2px 4px rgba(0,0,0,0.05)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Box sx={{ px: 2, py: 1, display: 'flex', alignItems: 'center', bgcolor: 'background.paper' }}>
|
|||
|
|
<Typography variant="body2" sx={{ fontWeight: 500, ml: 1 }}>
|
|||
|
|
{t('datasets.question')}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center' }}>
|
|||
|
|
<Typography variant="body2" sx={{ fontWeight: 500, mr: 2, display: { xs: 'none', sm: 'block' } }}>
|
|||
|
|
{t('common.label')}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography
|
|||
|
|
variant="body2"
|
|||
|
|
sx={{ fontWeight: 500, width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}
|
|||
|
|
>
|
|||
|
|
{t('common.dataSource')}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography variant="body2" sx={{ fontWeight: 500, width: 100, textAlign: 'center' }}>
|
|||
|
|
{t('common.actions')}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Divider />
|
|||
|
|
|
|||
|
|
{questions.map((question, index) => {
|
|||
|
|
const isSelected = isQuestionSelected(question.id);
|
|||
|
|
const questionKey = question.id;
|
|||
|
|
return (
|
|||
|
|
<Box key={questionKey}>
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
px: 2,
|
|||
|
|
py: 1.5,
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
bgcolor: isSelected ? 'action.selected' : 'background.paper',
|
|||
|
|
'&:hover': {
|
|||
|
|
bgcolor: 'action.hover'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Checkbox
|
|||
|
|
checked={isSelected}
|
|||
|
|
onChange={() => {
|
|||
|
|
onSelectQuestion(questionKey);
|
|||
|
|
}}
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Box sx={{ ml: 1, flex: 1, mr: 2 }}>
|
|||
|
|
<Typography variant="body2">
|
|||
|
|
{question.question}
|
|||
|
|
{question.datasetCount > 0 ? (
|
|||
|
|
<Chip
|
|||
|
|
label={t('datasets.answerCount', { count: question.datasetCount })}
|
|||
|
|
size="small"
|
|||
|
|
color="primary"
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{ fontSize: '0.75rem', maxWidth: 150 }}
|
|||
|
|
/>
|
|||
|
|
) : null}
|
|||
|
|
</Typography>
|
|||
|
|
<Typography variant="caption" color="text.secondary" sx={{ display: { xs: 'block', sm: 'none' } }}>
|
|||
|
|
{question.label || t('datasets.noTag')} • ID: {(question.question || '').substring(0, 8)}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ display: { xs: 'none', sm: 'block' }, mr: 2 }}>
|
|||
|
|
{question.label ? (
|
|||
|
|
<Chip
|
|||
|
|
label={question.label}
|
|||
|
|
size="small"
|
|||
|
|
color="primary"
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{ fontSize: '0.75rem', maxWidth: 150 }}
|
|||
|
|
/>
|
|||
|
|
) : (
|
|||
|
|
<Typography variant="caption" color="text.disabled">
|
|||
|
|
{t('datasets.noTag')}
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ width: 150, mr: 2, display: { xs: 'none', md: 'block' } }}>
|
|||
|
|
<Tooltip title={getChunkTitle(question.chunk?.content)}>
|
|||
|
|
<Chip
|
|||
|
|
label={
|
|||
|
|
question.imageId
|
|||
|
|
? `Image: ${question.imageName}`
|
|||
|
|
: `${t('chunks.title')}: ${question.chunk?.name}`
|
|||
|
|
}
|
|||
|
|
size="small"
|
|||
|
|
variant="outlined"
|
|||
|
|
color="info"
|
|||
|
|
sx={{
|
|||
|
|
fontSize: '0.75rem',
|
|||
|
|
maxWidth: '100%',
|
|||
|
|
textOverflow: 'ellipsis'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
<Box sx={{ width: 160, display: 'flex', justifyContent: 'center' }}>
|
|||
|
|
<Tooltip title={t('common.edit')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
color="primary"
|
|||
|
|
onClick={() => onEditQuestion(question)}
|
|||
|
|
disabled={processingQuestions[questionKey]}
|
|||
|
|
>
|
|||
|
|
<EditIcon fontSize="small" />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
<Tooltip title={t('datasets.generateDataset')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
color="primary"
|
|||
|
|
onClick={() =>
|
|||
|
|
handleGenerateDataset(question.id, question.question, question.imageId, question.imageName)
|
|||
|
|
}
|
|||
|
|
disabled={processingQuestions[questionKey]}
|
|||
|
|
>
|
|||
|
|
{processingQuestions[questionKey] ? (
|
|||
|
|
<CircularProgress size={16} />
|
|||
|
|
) : (
|
|||
|
|
<AutoFixHighIcon fontSize="small" />
|
|||
|
|
)}
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
|
|||
|
|
{!question.imageId && (
|
|||
|
|
<Tooltip title={t('questions.generateMultiTurn', '生成多轮对话')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
color="secondary"
|
|||
|
|
onClick={() => handleGenerateMultiTurnDataset(question.id, question.question)}
|
|||
|
|
disabled={processingQuestions[`${questionKey}_multi`]}
|
|||
|
|
>
|
|||
|
|
{processingQuestions[`${questionKey}_multi`] ? (
|
|||
|
|
<CircularProgress size={16} />
|
|||
|
|
) : (
|
|||
|
|
<ChatIcon fontSize="small" />
|
|||
|
|
)}
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Tooltip title={t('common.delete')}>
|
|||
|
|
<IconButton
|
|||
|
|
size="small"
|
|||
|
|
color="error"
|
|||
|
|
onClick={() => onDeleteQuestion(question.id)}
|
|||
|
|
disabled={processingQuestions[questionKey]}
|
|||
|
|
>
|
|||
|
|
<DeleteIcon fontSize="small" />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
{index < questions.length - 1 && <Divider />}
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</Paper>
|
|||
|
|
|
|||
|
|
{/* 分页 */}
|
|||
|
|
{totalQuestions > 1 && (
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', mt: 3, mb: 2 }}>
|
|||
|
|
<Pagination
|
|||
|
|
count={totalQuestions}
|
|||
|
|
page={currentPage}
|
|||
|
|
onChange={handlePageChange}
|
|||
|
|
color="primary"
|
|||
|
|
showFirstButton
|
|||
|
|
showLastButton
|
|||
|
|
shape="rounded"
|
|||
|
|
size="medium"
|
|||
|
|
/>
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|||
|
|
<Typography variant="body2">{t('common.jumpTo')}:</Typography>
|
|||
|
|
<TextField
|
|||
|
|
size="small"
|
|||
|
|
type="number"
|
|||
|
|
inputProps={{
|
|||
|
|
min: 1,
|
|||
|
|
max: totalQuestions,
|
|||
|
|
style: { padding: '4px 8px', width: '50px' }
|
|||
|
|
}}
|
|||
|
|
onKeyPress={e => {
|
|||
|
|
if (e.key === 'Enter') {
|
|||
|
|
const pageNum = parseInt(e.target.value, 10);
|
|||
|
|
if (pageNum >= 1 && pageNum <= totalQuestions) {
|
|||
|
|
handlePageChange(null, pageNum);
|
|||
|
|
e.target.value = '';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
}
|