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