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>
|
||
);
|
||
}
|