417 lines
14 KiB
JavaScript
417 lines
14 KiB
JavaScript
'use client';
|
||
|
||
import { useState, useEffect } from 'react';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { Container, Typography, Box, Paper, Tabs, Tab, CircularProgress, Divider, LinearProgress } from '@mui/material';
|
||
|
||
import QuestionListView from '@/components/questions/QuestionListView';
|
||
import QuestionTreeView from '@/components/questions/QuestionTreeView';
|
||
import TabPanel from '@/components/text-split/components/TabPanel';
|
||
import useTaskSettings from '@/hooks/useTaskSettings';
|
||
import QuestionEditDialog from './components/QuestionEditDialog';
|
||
import QuestionsPageHeader from './components/QuestionsPageHeader';
|
||
import ConfirmDialog from './components/ConfirmDialog';
|
||
import TemplateListView from './components/TemplateListView';
|
||
import TemplateFormDialog from './components/template/TemplateFormDialog';
|
||
import ExportQuestionsDialog from './components/ExportQuestionsDialog';
|
||
import { useQuestionTemplates } from './hooks/useQuestionTemplates';
|
||
import { useQuestionEdit } from './hooks/useQuestionEdit';
|
||
import { useQuestionDelete } from './hooks/useQuestionDelete';
|
||
import { useQuestionsFilter } from './hooks/useQuestionsFilter';
|
||
import QuestionsFilter from './components/QuestionsFilter';
|
||
import { useQuestionGeneration } from './hooks/useQuestionGeneration';
|
||
import useQuestionExport from './hooks/useQuestionExport';
|
||
import axios from 'axios';
|
||
import { toast } from 'sonner';
|
||
import { useAtomValue } from 'jotai/index';
|
||
import { selectedModelInfoAtom } from '@/lib/store';
|
||
|
||
export default function QuestionsPage({ params }) {
|
||
const { t } = useTranslation();
|
||
const { projectId } = params;
|
||
const [loading, setLoading] = useState(true);
|
||
const [questions, setQuestions] = useState({});
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(10);
|
||
const [tags, setTags] = useState([]);
|
||
const model = useAtomValue(selectedModelInfoAtom);
|
||
const [activeTab, setActiveTab] = useState(0);
|
||
|
||
// 模板管理
|
||
const {
|
||
templates,
|
||
loading: templatesLoading,
|
||
createTemplate,
|
||
updateTemplate,
|
||
deleteTemplate
|
||
} = useQuestionTemplates(projectId, null); // null 表示获取所有类型的模板
|
||
|
||
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||
const [exportDialogOpen, setExportDialogOpen] = useState(false);
|
||
|
||
// 使用新的过滤和搜索 Hook
|
||
const {
|
||
answerFilter,
|
||
searchTerm,
|
||
debouncedSearchTerm,
|
||
searchMatchMode,
|
||
chunkNameFilter,
|
||
debouncedChunkNameFilter,
|
||
sourceTypeFilter,
|
||
selectedQuestions,
|
||
setSelectedQuestions,
|
||
handleSelectQuestion,
|
||
handleSelectAll,
|
||
handleSearchChange,
|
||
handleFilterChange,
|
||
handleChunkNameFilterChange,
|
||
handleSourceTypeFilterChange,
|
||
handleSearchMatchModeChange
|
||
} = useQuestionsFilter(projectId);
|
||
|
||
const getQuestionList = async () => {
|
||
try {
|
||
// 获取问题列表
|
||
const questionsResponse = await axios.get(
|
||
`/api/projects/${projectId}/questions?page=${currentPage}&size=10&status=${answerFilter}&input=${searchTerm}&searchMatchMode=${searchMatchMode}&chunkName=${encodeURIComponent(debouncedChunkNameFilter)}&sourceType=${sourceTypeFilter}`
|
||
);
|
||
if (questionsResponse.status !== 200) {
|
||
throw new Error(t('common.fetchError'));
|
||
}
|
||
setQuestions(questionsResponse.data || {});
|
||
|
||
// 获取标签树
|
||
const tagsResponse = await axios.get(`/api/projects/${projectId}/tags`);
|
||
if (tagsResponse.status !== 200) {
|
||
throw new Error(t('common.fetchError'));
|
||
}
|
||
setTags(tagsResponse.data.tags || []);
|
||
|
||
setLoading(false);
|
||
} catch (error) {
|
||
console.error(t('common.fetchError'), error);
|
||
toast.error(error.message);
|
||
}
|
||
};
|
||
|
||
// 当筛选条件改变时,重置页码到第1页
|
||
useEffect(() => {
|
||
setCurrentPage(1);
|
||
}, [answerFilter, debouncedSearchTerm, debouncedChunkNameFilter, sourceTypeFilter, searchMatchMode]);
|
||
|
||
useEffect(() => {
|
||
getQuestionList();
|
||
}, [currentPage, answerFilter, debouncedSearchTerm, debouncedChunkNameFilter, sourceTypeFilter, searchMatchMode]);
|
||
|
||
const { taskSettings } = useTaskSettings(projectId);
|
||
|
||
// 使用新的问题生成 Hook
|
||
const {
|
||
processing,
|
||
progress,
|
||
handleBatchGenerateAnswers,
|
||
handleAutoGenerateDatasets,
|
||
handleAutoGenerateMultiTurnDatasets,
|
||
handleAutoGenerateImageDatasets
|
||
} = useQuestionGeneration(projectId, model, taskSettings, getQuestionList);
|
||
|
||
const {
|
||
editDialogOpen,
|
||
editMode,
|
||
editingQuestion,
|
||
handleOpenCreateDialog,
|
||
handleOpenEditDialog,
|
||
handleCloseDialog,
|
||
handleSubmitQuestion
|
||
} = useQuestionEdit(projectId, updatedQuestion => {
|
||
getQuestionList();
|
||
toast.success(t('questions.operationSuccess'));
|
||
});
|
||
|
||
const { confirmDialog, handleDeleteQuestion, handleBatchDeleteQuestions, closeConfirmDialog, handleConfirmAction } =
|
||
useQuestionDelete(projectId, () => {
|
||
getQuestionList();
|
||
});
|
||
|
||
const { exportQuestions } = useQuestionExport(projectId);
|
||
|
||
// 获取所有数据
|
||
useEffect(() => {
|
||
getQuestionList();
|
||
}, [projectId]);
|
||
|
||
// 处理标签页切换
|
||
const handleTabChange = (event, newValue) => {
|
||
setActiveTab(newValue);
|
||
};
|
||
|
||
// 模板管理函数
|
||
const handleOpenCreateTemplateDialog = () => {
|
||
setEditingTemplate(null);
|
||
setTemplateDialogOpen(true);
|
||
};
|
||
|
||
const handleEditTemplate = template => {
|
||
setEditingTemplate(template);
|
||
setTemplateDialogOpen(true);
|
||
};
|
||
|
||
const handleCloseTemplateDialog = () => {
|
||
setTemplateDialogOpen(false);
|
||
setEditingTemplate(null);
|
||
};
|
||
|
||
const handleSubmitTemplate = async data => {
|
||
try {
|
||
if (editingTemplate) {
|
||
await updateTemplate(editingTemplate.id, data);
|
||
} else {
|
||
await createTemplate(data);
|
||
}
|
||
getQuestionList();
|
||
handleCloseTemplateDialog();
|
||
} catch (error) {
|
||
console.error('Failed to save template:', error);
|
||
}
|
||
};
|
||
|
||
const handleDeleteTemplate = async templateId => {
|
||
const confirmed = window.confirm(t('questions.template.deleteConfirm'));
|
||
if (confirmed) {
|
||
try {
|
||
await deleteTemplate(templateId);
|
||
} catch (error) {
|
||
console.error('Failed to delete template:', error);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleOpenExportDialog = () => {
|
||
setExportDialogOpen(true);
|
||
};
|
||
|
||
const handleCloseExportDialog = () => {
|
||
setExportDialogOpen(false);
|
||
};
|
||
|
||
const handleExportQuestions = async exportOptions => {
|
||
const options = {
|
||
...exportOptions,
|
||
selectedIds: selectedQuestions,
|
||
filters: {
|
||
searchTerm: debouncedSearchTerm,
|
||
chunkName: debouncedChunkNameFilter,
|
||
sourceType: sourceTypeFilter
|
||
}
|
||
};
|
||
await exportQuestions(options);
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<Container maxWidth="lg" sx={{ mt: 4 }}>
|
||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '70vh' }}>
|
||
<CircularProgress />
|
||
</Box>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
|
||
{/* 处理中的进度显示 - 全局蒙版样式 */}
|
||
{processing && (
|
||
<Box
|
||
sx={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
width: '100vw',
|
||
height: '100vh',
|
||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||
zIndex: 9999,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
alignItems: 'center'
|
||
}}
|
||
>
|
||
<Paper
|
||
elevation={6}
|
||
sx={{
|
||
width: '90%',
|
||
maxWidth: 500,
|
||
p: 3,
|
||
borderRadius: 2,
|
||
textAlign: 'center'
|
||
}}
|
||
>
|
||
<Typography variant="h6" sx={{ mb: 2, color: 'primary.main', fontWeight: 'bold' }}>
|
||
{t('datasets.generatingDataset')}
|
||
</Typography>
|
||
|
||
<Box sx={{ mb: 3 }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||
<Typography variant="body1" sx={{ mr: 1 }}>
|
||
{progress.percentage}%
|
||
</Typography>
|
||
<Box sx={{ width: '100%' }}>
|
||
<LinearProgress
|
||
variant="determinate"
|
||
value={progress.percentage}
|
||
sx={{ height: 8, borderRadius: 4 }}
|
||
color="primary"
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
|
||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
|
||
<Typography variant="body2">
|
||
{t('questions.generatingProgress', {
|
||
completed: progress.completed,
|
||
total: progress.total
|
||
})}
|
||
</Typography>
|
||
<Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
|
||
{t('questions.generatedCount', { count: progress.datasetCount })}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
|
||
<CircularProgress size={60} thickness={4} sx={{ mb: 2 }} />
|
||
|
||
<Typography variant="body2" color="text.secondary">
|
||
{t('questions.pleaseWait')}
|
||
</Typography>
|
||
</Paper>
|
||
</Box>
|
||
)}
|
||
|
||
<QuestionsPageHeader
|
||
questionsTotal={questions.total}
|
||
selectedQuestionsCount={selectedQuestions.length}
|
||
onBatchDeleteQuestions={() => handleBatchDeleteQuestions(selectedQuestions, setSelectedQuestions)}
|
||
onOpenCreateDialog={handleOpenCreateDialog}
|
||
onOpenCreateTemplateDialog={handleOpenCreateTemplateDialog}
|
||
onBatchGenerateAnswers={() => handleBatchGenerateAnswers(selectedQuestions)}
|
||
onAutoGenerateDatasets={handleAutoGenerateDatasets}
|
||
onAutoGenerateMultiTurnDatasets={handleAutoGenerateMultiTurnDatasets}
|
||
onAutoGenerateImageDatasets={handleAutoGenerateImageDatasets}
|
||
onExportQuestions={handleOpenExportDialog}
|
||
/>
|
||
|
||
<Paper sx={{ mb: 4 }}>
|
||
<Tabs
|
||
value={activeTab}
|
||
onChange={handleTabChange}
|
||
variant="fullWidth"
|
||
indicatorColor="primary"
|
||
sx={{
|
||
borderBottom: 1,
|
||
borderColor: 'divider'
|
||
}}
|
||
>
|
||
<Tab label={t('questions.listView')} />
|
||
<Tab label={t('questions.template.management')} />
|
||
<Tab label={t('questions.treeView')} />
|
||
</Tabs>
|
||
|
||
<QuestionsFilter
|
||
selectedQuestionsCount={selectedQuestions.length}
|
||
totalQuestions={questions?.total || 0}
|
||
isAllSelected={selectedQuestions.length > 0 && selectedQuestions.length === questions?.total}
|
||
isIndeterminate={selectedQuestions.length > 0 && selectedQuestions.length < questions?.total}
|
||
onSelectAll={handleSelectAll}
|
||
searchTerm={searchTerm}
|
||
onSearchChange={handleSearchChange}
|
||
searchMatchMode={searchMatchMode}
|
||
onSearchMatchModeChange={handleSearchMatchModeChange}
|
||
answerFilter={answerFilter}
|
||
onFilterChange={handleFilterChange}
|
||
chunkNameFilter={chunkNameFilter}
|
||
onChunkNameFilterChange={handleChunkNameFilterChange}
|
||
sourceTypeFilter={sourceTypeFilter}
|
||
onSourceTypeFilterChange={handleSourceTypeFilterChange}
|
||
activeTab={activeTab}
|
||
/>
|
||
|
||
<Divider />
|
||
|
||
<TabPanel value={activeTab} index={0}>
|
||
<QuestionListView
|
||
questions={questions.data}
|
||
currentPage={currentPage}
|
||
totalQuestions={Math.ceil(questions.total / pageSize)}
|
||
handlePageChange={(_, newPage) => setCurrentPage(newPage)}
|
||
selectedQuestions={selectedQuestions}
|
||
onSelectQuestion={handleSelectQuestion}
|
||
onDeleteQuestion={questionId => handleDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions)}
|
||
onEditQuestion={handleOpenEditDialog}
|
||
refreshQuestions={getQuestionList}
|
||
projectId={projectId}
|
||
/>
|
||
</TabPanel>
|
||
|
||
<TabPanel value={activeTab} index={1}>
|
||
<TemplateListView
|
||
templates={templates}
|
||
onEditTemplate={handleEditTemplate}
|
||
onDeleteTemplate={handleDeleteTemplate}
|
||
loading={templatesLoading}
|
||
/>
|
||
</TabPanel>
|
||
|
||
<TabPanel value={activeTab} index={2}>
|
||
<QuestionTreeView
|
||
questions={questions.data}
|
||
tags={tags}
|
||
selectedQuestions={selectedQuestions}
|
||
onSelectQuestion={handleSelectQuestion}
|
||
onDeleteQuestion={questionId => handleDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions)}
|
||
onEditQuestion={handleOpenEditDialog}
|
||
projectId={projectId}
|
||
searchTerm={searchTerm}
|
||
/>
|
||
</TabPanel>
|
||
</Paper>
|
||
|
||
{/* 确认对话框 */}
|
||
<ConfirmDialog
|
||
open={confirmDialog.open}
|
||
onClose={closeConfirmDialog}
|
||
onConfirm={handleConfirmAction}
|
||
title={confirmDialog.title}
|
||
content={confirmDialog.content}
|
||
/>
|
||
|
||
<QuestionEditDialog
|
||
open={editDialogOpen}
|
||
onClose={handleCloseDialog}
|
||
onSubmit={handleSubmitQuestion}
|
||
initialData={editingQuestion}
|
||
tags={tags}
|
||
mode={editMode}
|
||
projectId={projectId}
|
||
/>
|
||
|
||
<TemplateFormDialog
|
||
open={templateDialogOpen}
|
||
onClose={handleCloseTemplateDialog}
|
||
onSubmit={handleSubmitTemplate}
|
||
template={editingTemplate}
|
||
/>
|
||
|
||
<ExportQuestionsDialog
|
||
open={exportDialogOpen}
|
||
onClose={handleCloseExportDialog}
|
||
onExport={handleExportQuestions}
|
||
selectedCount={selectedQuestions.length}
|
||
totalCount={questions.total || 0}
|
||
/>
|
||
</Container>
|
||
);
|
||
}
|