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

417 lines
14 KiB
JavaScript
Raw 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, 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>
);
}