Files

375 lines
12 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 { 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>
);
}