Files
YG-Datasets/easy-dataset-main/app/projects/[projectId]/questions/page.js

417 lines
14 KiB
JavaScript
Raw Normal View History

2026-03-17 14:36:31 +08:00
'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>
);
}