first-update
This commit is contained in:
486
easy-dataset-main/app/projects/[projectId]/images/page.js
Normal file
486
easy-dataset-main/app/projects/[projectId]/images/page.js
Normal file
@@ -0,0 +1,486 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Container,
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { imageStyles } from './styles/imageStyles';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
|
||||
import ImageFilters from './components/ImageFilters';
|
||||
import ImageGrid from './components/ImageGrid';
|
||||
import ImageList from './components/ImageList';
|
||||
import ImportDialog from './components/ImportDialog';
|
||||
import QuestionDialog from './components/QuestionDialog';
|
||||
import DatasetDialog from './components/DatasetDialog';
|
||||
import AnnotationDialog from './components/annotation/AnnotationDialog';
|
||||
import { useQuestionTemplates } from '../questions/hooks/useQuestionTemplates';
|
||||
import { useAnnotation } from './hooks/useAnnotation';
|
||||
import { useQuestionEdit } from '../questions/hooks/useQuestionEdit';
|
||||
import QuestionEditDialog from '../questions/components/QuestionEditDialog';
|
||||
import TemplateFormDialog from '../questions/components/template/TemplateFormDialog';
|
||||
|
||||
export default function ImagesPage() {
|
||||
const { projectId } = useParams();
|
||||
const router = useRouter();
|
||||
const { t, i18n } = useTranslation();
|
||||
const selectedModel = useAtomValue(selectedModelInfoAtom);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [images, setImages] = useState([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(8);
|
||||
|
||||
// 筛选条件
|
||||
const [imageName, setImageName] = useState('');
|
||||
const [hasQuestions, setHasQuestions] = useState('all');
|
||||
const [hasDatasets, setHasDatasets] = useState('all');
|
||||
|
||||
// 视图模式
|
||||
const [viewMode, setViewMode] = useState('grid');
|
||||
|
||||
// 选中状态(仅列表视图使用)
|
||||
const [selectedIds, setSelectedIds] = useState([]);
|
||||
|
||||
// 对话框状态
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [questionDialogOpen, setQuestionDialogOpen] = useState(false);
|
||||
const [datasetDialogOpen, setDatasetDialogOpen] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState(null);
|
||||
const [autoGenerateDialogOpen, setAutoGenerateDialogOpen] = useState(false);
|
||||
const [questionCount, setQuestionCount] = useState(3);
|
||||
|
||||
// 问题模板和标注功能 (只获取图像类型的模板)
|
||||
const { templates, createTemplate } = useQuestionTemplates(projectId, 'image');
|
||||
|
||||
// 问题编辑 Hook
|
||||
const { editDialogOpen, editMode, editingQuestion, handleOpenCreateDialog, handleCloseDialog, handleSubmitQuestion } =
|
||||
useQuestionEdit(projectId, async () => {
|
||||
fetchImages();
|
||||
if (annotationOpen && currentImage) {
|
||||
await refreshCurrentImage();
|
||||
}
|
||||
toast.success(t('questions.operationSuccess'));
|
||||
});
|
||||
|
||||
// 模板管理状态
|
||||
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
||||
|
||||
// 获取图片列表
|
||||
const fetchImages = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
pageSize: pageSize.toString()
|
||||
});
|
||||
|
||||
if (imageName) params.append('imageName', imageName);
|
||||
if (hasQuestions !== 'all') params.append('hasQuestions', hasQuestions);
|
||||
if (hasDatasets !== 'all') params.append('hasDatasets', hasDatasets);
|
||||
|
||||
const response = await axios.get(`/api/projects/${projectId}/images?${params.toString()}`);
|
||||
setImages(response.data.data);
|
||||
setTotal(response.data.total);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch images:', error);
|
||||
toast.error(t('common.fetchError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 查找下一个有未标注问题的图片
|
||||
const handleFindNextImage = async () => {
|
||||
try {
|
||||
const response = await axios.get(`/api/projects/${projectId}/images/next-unanswered`);
|
||||
return response.data.data || null;
|
||||
} catch (error) {
|
||||
console.error('查找下一个图片失败:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
open: annotationOpen,
|
||||
saving: annotationSaving,
|
||||
loading: annotationLoading,
|
||||
currentImage,
|
||||
selectedTemplate,
|
||||
answer,
|
||||
setAnswer,
|
||||
handleTemplateChange,
|
||||
openAnnotation,
|
||||
closeAnnotation,
|
||||
saveAnnotation,
|
||||
refreshCurrentImage
|
||||
} = useAnnotation(projectId, fetchImages, handleFindNextImage);
|
||||
|
||||
useEffect(() => {
|
||||
fetchImages();
|
||||
}, [projectId, page, imageName, hasQuestions, hasDatasets]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIds([]);
|
||||
}, [viewMode]);
|
||||
|
||||
// 处理导入成功
|
||||
const handleImportSuccess = () => {
|
||||
setImportDialogOpen(false);
|
||||
setPage(1);
|
||||
fetchImages();
|
||||
};
|
||||
|
||||
// 处理生成问题
|
||||
const handleGenerateQuestions = image => {
|
||||
setSelectedImage(image);
|
||||
setQuestionDialogOpen(true);
|
||||
};
|
||||
|
||||
// 处理生成数据集
|
||||
const handleGenerateDataset = image => {
|
||||
setSelectedImage(image);
|
||||
setDatasetDialogOpen(true);
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleDeleteImage = async imageId => {
|
||||
if (!confirm(t('images.deleteConfirm', { defaultValue: '确定要删除这张图片吗?' }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/projects/${projectId}/images?imageId=${imageId}`);
|
||||
toast.success(t('images.deleteSuccess', { defaultValue: '删除成功' }));
|
||||
fetchImages();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete image:', error);
|
||||
toast.error(t('images.deleteFailed', { defaultValue: '删除失败' }));
|
||||
}
|
||||
};
|
||||
|
||||
// 批量删除图片
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toast.error(t('images.selectImagesToDelete', { defaultValue: '请选择要删除的图片' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
t('images.batchDeleteConfirm', {
|
||||
defaultValue: `确定要删除选中的 ${selectedIds.length} 张图片吗?`,
|
||||
count: selectedIds.length
|
||||
})
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 逐个调用删除接口
|
||||
for (const imageId of selectedIds) {
|
||||
try {
|
||||
await axios.delete(`/api/projects/${projectId}/images?imageId=${imageId}`);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete image ${imageId}:`, error);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
if (failCount === 0) {
|
||||
toast.success(
|
||||
t('images.batchDeleteSuccess', {
|
||||
defaultValue: `成功删除 ${successCount} 张图片`,
|
||||
count: successCount
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.warning(
|
||||
t('images.batchDeletePartialSuccess', {
|
||||
defaultValue: `成功删除 ${successCount} 张,失败 ${failCount} 张`,
|
||||
success: successCount,
|
||||
fail: failCount
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 清空选中状态并刷新列表
|
||||
setSelectedIds([]);
|
||||
fetchImages();
|
||||
} catch (error) {
|
||||
console.error('Batch delete failed:', error);
|
||||
toast.error(t('images.batchDeleteFailed', { defaultValue: '批量删除失败' }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理自动提取问题
|
||||
const handleAutoGenerateQuestions = () => {
|
||||
if (!selectedModel) {
|
||||
toast.error(t('images.selectModelFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedModel.type !== 'vision') {
|
||||
toast.error(t('images.visionModelRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
setAutoGenerateDialogOpen(true);
|
||||
};
|
||||
|
||||
// 确认创建自动提取任务
|
||||
const handleConfirmAutoGenerate = async () => {
|
||||
// 验证问题数量
|
||||
if (questionCount < 1 || questionCount > 10) {
|
||||
toast.error(t('images.countRange'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setAutoGenerateDialogOpen(false);
|
||||
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'image-question-generation',
|
||||
modelInfo: selectedModel,
|
||||
language: i18n.language,
|
||||
note: { questionCount }
|
||||
});
|
||||
|
||||
if (response.data.code === 0) {
|
||||
toast.success(t('images.taskCreated'));
|
||||
// 跳转到任务管理页面
|
||||
router.push(`/projects/${projectId}/tasks`);
|
||||
} else {
|
||||
toast.error(response.data.error || t('images.taskCreateFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create auto-generate task:', error);
|
||||
toast.error(t('images.taskCreateFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
// 模板管理函数
|
||||
const handleOpenCreateTemplateDialog = () => {
|
||||
setTemplateDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseTemplateDialog = () => {
|
||||
setTemplateDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmitTemplate = async data => {
|
||||
try {
|
||||
await createTemplate(data);
|
||||
handleCloseTemplateDialog();
|
||||
fetchImages();
|
||||
if (annotationOpen && currentImage) {
|
||||
await refreshCurrentImage();
|
||||
}
|
||||
toast.success(t('questions.operationSuccess'));
|
||||
} catch (error) {
|
||||
console.error('Failed to save template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={imageStyles.pageContainer}>
|
||||
{/* 页面头部 */}
|
||||
<Box sx={imageStyles.header}>
|
||||
<Box sx={imageStyles.headerTitle}>
|
||||
<Typography variant="h4" component="h1" sx={imageStyles.title}>
|
||||
{t('images.title', { defaultValue: '图片管理' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={imageStyles.headerActions}>
|
||||
{viewMode === 'list' && selectedIds.length > 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={handleBatchDelete}
|
||||
sx={imageStyles.actionButton}
|
||||
>
|
||||
{t('images.batchDelete', { defaultValue: '批量删除' })} ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
onClick={handleAutoGenerateQuestions}
|
||||
sx={imageStyles.actionButton}
|
||||
>
|
||||
{t('images.autoGenerateQuestions', { defaultValue: 'AI 批量生成问题' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddPhotoAlternateIcon />}
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
sx={imageStyles.actionButton}
|
||||
>
|
||||
{t('images.importImages', { defaultValue: '导入图片' })}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<ImageFilters
|
||||
imageName={imageName}
|
||||
onImageNameChange={setImageName}
|
||||
hasQuestions={hasQuestions}
|
||||
onHasQuestionsChange={setHasQuestions}
|
||||
hasDatasets={hasDatasets}
|
||||
onHasDatasetsChange={setHasDatasets}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
|
||||
{/* 图片列表 */}
|
||||
{loading ? (
|
||||
<Box sx={imageStyles.loadingContainer}>
|
||||
<CircularProgress size={48} />
|
||||
</Box>
|
||||
) : viewMode === 'grid' ? (
|
||||
<ImageGrid
|
||||
images={images}
|
||||
total={total}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setPage}
|
||||
onGenerateQuestions={handleGenerateQuestions}
|
||||
onGenerateDataset={handleGenerateDataset}
|
||||
onDelete={handleDeleteImage}
|
||||
onAnnotate={openAnnotation}
|
||||
/>
|
||||
) : (
|
||||
<ImageList
|
||||
images={images}
|
||||
total={total}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onPageChange={setPage}
|
||||
onGenerateQuestions={handleGenerateQuestions}
|
||||
onGenerateDataset={handleGenerateDataset}
|
||||
onDelete={handleDeleteImage}
|
||||
onAnnotate={openAnnotation}
|
||||
selectedIds={selectedIds}
|
||||
onSelectionChange={setSelectedIds}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ImportDialog
|
||||
open={importDialogOpen}
|
||||
projectId={projectId}
|
||||
onClose={() => setImportDialogOpen(false)}
|
||||
onSuccess={handleImportSuccess}
|
||||
/>
|
||||
|
||||
<QuestionDialog
|
||||
open={questionDialogOpen}
|
||||
projectId={projectId}
|
||||
image={selectedImage}
|
||||
onClose={() => setQuestionDialogOpen(false)}
|
||||
onSuccess={fetchImages}
|
||||
/>
|
||||
|
||||
<DatasetDialog
|
||||
open={datasetDialogOpen}
|
||||
projectId={projectId}
|
||||
image={selectedImage}
|
||||
onClose={() => setDatasetDialogOpen(false)}
|
||||
onSuccess={fetchImages}
|
||||
/>
|
||||
|
||||
<Dialog open={autoGenerateDialogOpen} onClose={() => setAutoGenerateDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('images.autoGenerateQuestions')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography sx={{ mb: 3 }}>{t('images.autoGenerateConfirm')}</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label={t('images.questionCount')}
|
||||
value={questionCount}
|
||||
onChange={e => setQuestionCount(parseInt(e.target.value) || 1)}
|
||||
inputProps={{ min: 1, max: 10 }}
|
||||
helperText={t('images.questionCountHelp')}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('images.currentModel')}: {selectedModel?.modelName || t('common.none')}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setAutoGenerateDialogOpen(false)}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleConfirmAutoGenerate} variant="contained">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<AnnotationDialog
|
||||
open={annotationOpen}
|
||||
onClose={closeAnnotation}
|
||||
image={currentImage}
|
||||
templates={templates}
|
||||
selectedTemplate={selectedTemplate}
|
||||
onTemplateChange={handleTemplateChange}
|
||||
answer={answer}
|
||||
onAnswerChange={setAnswer}
|
||||
onSave={() => saveAnnotation(false)}
|
||||
onSaveAndContinue={() => saveAnnotation(true)}
|
||||
saving={annotationSaving}
|
||||
loading={annotationLoading}
|
||||
onOpenCreateQuestion={handleOpenCreateDialog}
|
||||
onOpenCreateTemplate={handleOpenCreateTemplateDialog}
|
||||
/>
|
||||
|
||||
{/* 问题编辑对话框 */}
|
||||
<QuestionEditDialog
|
||||
open={editDialogOpen}
|
||||
mode={editMode}
|
||||
question={editingQuestion}
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleSubmitQuestion}
|
||||
projectId={projectId}
|
||||
initialData={{ sourceType: 'image', imageId: currentImage?.id }}
|
||||
/>
|
||||
|
||||
{/* 问题模板对话框 */}
|
||||
<TemplateFormDialog
|
||||
open={templateDialogOpen}
|
||||
onClose={handleCloseTemplateDialog}
|
||||
onSubmit={handleSubmitTemplate}
|
||||
projectId={projectId}
|
||||
template={{ sourceType: 'image' }}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user