269 lines
8.3 KiB
JavaScript
269 lines
8.3 KiB
JavaScript
|
|
'use client';
|
|||
|
|
|
|||
|
|
import {
|
|||
|
|
Dialog,
|
|||
|
|
DialogTitle,
|
|||
|
|
DialogContent,
|
|||
|
|
DialogActions,
|
|||
|
|
Button,
|
|||
|
|
Box,
|
|||
|
|
Typography,
|
|||
|
|
Chip,
|
|||
|
|
CircularProgress
|
|||
|
|
} from '@mui/material';
|
|||
|
|
import { useTranslation } from 'react-i18next';
|
|||
|
|
import Image from 'next/image';
|
|||
|
|
import QuestionSelector from './QuestionSelector';
|
|||
|
|
import AnswerInput from './AnswerInput';
|
|||
|
|
|
|||
|
|
export default function AnnotationDialog({
|
|||
|
|
open,
|
|||
|
|
onClose,
|
|||
|
|
image,
|
|||
|
|
templates,
|
|||
|
|
selectedTemplate,
|
|||
|
|
onTemplateChange,
|
|||
|
|
answer,
|
|||
|
|
onAnswerChange,
|
|||
|
|
onSave,
|
|||
|
|
onSaveAndContinue,
|
|||
|
|
saving,
|
|||
|
|
loading,
|
|||
|
|
onOpenCreateQuestion,
|
|||
|
|
onOpenCreateTemplate
|
|||
|
|
}) {
|
|||
|
|
const { t } = useTranslation();
|
|||
|
|
|
|||
|
|
if (!image) return null;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Dialog
|
|||
|
|
open={open}
|
|||
|
|
onClose={onClose}
|
|||
|
|
maxWidth="xl"
|
|||
|
|
fullWidth
|
|||
|
|
PaperProps={{
|
|||
|
|
sx: {
|
|||
|
|
borderRadius: 2,
|
|||
|
|
maxHeight: '90vh'
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<DialogTitle sx={{ pb: 2 }}>
|
|||
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|||
|
|
<Typography variant="h5" fontWeight="600">
|
|||
|
|
{t('images.annotateImage', { defaultValue: '标注图片' })}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|||
|
|
{image && (
|
|||
|
|
<Chip
|
|||
|
|
label={`${image.answeredQuestions?.length || 0} / ${(image.answeredQuestions?.length || 0) + (image.unansweredQuestions?.length || 0)} 已完成`}
|
|||
|
|
color="primary"
|
|||
|
|
variant="outlined"
|
|||
|
|
size="small"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
</DialogTitle>
|
|||
|
|
|
|||
|
|
<DialogContent sx={{ p: 3 }}>
|
|||
|
|
{/* 图片预览区域 */}
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
display: 'flex',
|
|||
|
|
gap: 4,
|
|||
|
|
mb: 4,
|
|||
|
|
minHeight: 450
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* 图片预览 */}
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
flex: '0 0 450px',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
gap: 2
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{image && (
|
|||
|
|
<>
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
position: 'relative',
|
|||
|
|
width: '100%',
|
|||
|
|
height: 400,
|
|||
|
|
border: '2px solid',
|
|||
|
|
borderColor: 'divider',
|
|||
|
|
borderRadius: 2,
|
|||
|
|
overflow: 'hidden',
|
|||
|
|
bgcolor: 'grey.50'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{image.base64 ? (
|
|||
|
|
<Image src={image.base64} alt={image.imageName} fill style={{ objectFit: 'contain' }} priority />
|
|||
|
|
) : (
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
width: '100%',
|
|||
|
|
height: '100%',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
color: 'text.secondary'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Typography variant="body2">
|
|||
|
|
{t('images.imageLoadError', { defaultValue: '图片加载失败' })}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 图片信息卡片 */}
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
p: 2,
|
|||
|
|
bgcolor: 'grey.50',
|
|||
|
|
borderRadius: 2,
|
|||
|
|
border: '1px solid',
|
|||
|
|
borderColor: 'divider'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<Typography variant="subtitle1" fontWeight="600" gutterBottom>
|
|||
|
|
{image.imageName}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 1 }}>
|
|||
|
|
{image.width && image.height && (
|
|||
|
|
<Chip label={`${image.width} × ${image.height}`} size="small" variant="outlined" />
|
|||
|
|
)}
|
|||
|
|
{image.size && (
|
|||
|
|
<Chip label={`${(image.size / 1024).toFixed(2)} KB`} size="small" variant="outlined" />
|
|||
|
|
)}
|
|||
|
|
{image.format && <Chip label={image.format?.toUpperCase()} size="small" variant="outlined" />}
|
|||
|
|
</Box>
|
|||
|
|
<Typography variant="body2" color="text.secondary">
|
|||
|
|
<strong>{t('images.annotatedCount', { defaultValue: '已标注' })}:</strong> {image.datasetCount || 0}{' '}
|
|||
|
|
{t('images.questions', { defaultValue: '个问题' })}
|
|||
|
|
</Typography>
|
|||
|
|
</Box>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 标注区域 */}
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
flex: 1,
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
gap: 3,
|
|||
|
|
minWidth: 0
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* 问题选择器 */}
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
p: 3,
|
|||
|
|
bgcolor: 'background.paper',
|
|||
|
|
borderRadius: 2,
|
|||
|
|
border: '1px solid',
|
|||
|
|
borderColor: 'divider'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{loading ? (
|
|||
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|||
|
|
<CircularProgress />
|
|||
|
|
</Box>
|
|||
|
|
) : (
|
|||
|
|
<QuestionSelector
|
|||
|
|
templates={templates}
|
|||
|
|
selectedTemplate={selectedTemplate}
|
|||
|
|
onTemplateChange={onTemplateChange}
|
|||
|
|
answeredQuestions={image?.answeredQuestions || []}
|
|||
|
|
unansweredQuestions={image?.unansweredQuestions || []}
|
|||
|
|
onOpenCreateQuestion={onOpenCreateQuestion}
|
|||
|
|
onOpenCreateTemplate={onOpenCreateTemplate}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 答案输入区域 */}
|
|||
|
|
{selectedTemplate && (
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
p: 3,
|
|||
|
|
bgcolor: 'background.paper',
|
|||
|
|
borderRadius: 2,
|
|||
|
|
border: '1px solid',
|
|||
|
|
borderColor: 'divider'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<AnswerInput
|
|||
|
|
answerType={selectedTemplate.answerType}
|
|||
|
|
answer={answer}
|
|||
|
|
onAnswerChange={onAnswerChange}
|
|||
|
|
labels={selectedTemplate.labels}
|
|||
|
|
customFormat={selectedTemplate.customFormat}
|
|||
|
|
projectId={image?.projectId}
|
|||
|
|
imageName={image?.imageName}
|
|||
|
|
question={selectedTemplate}
|
|||
|
|
/>
|
|||
|
|
</Box>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
</DialogContent>
|
|||
|
|
|
|||
|
|
<DialogActions
|
|||
|
|
sx={{
|
|||
|
|
p: 3,
|
|||
|
|
pt: 0,
|
|||
|
|
gap: 1,
|
|||
|
|
justifyContent: 'space-between',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexWrap: 'wrap'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{/* 左侧:创建按钮 */}
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|||
|
|
<Button
|
|||
|
|
onClick={onOpenCreateQuestion}
|
|||
|
|
variant="outlined"
|
|||
|
|
size="small"
|
|||
|
|
sx={{ borderRadius: 2, textTransform: 'none' }}
|
|||
|
|
>
|
|||
|
|
{t('images.createQuestion', { defaultValue: '创建问题' })}
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
onClick={onOpenCreateTemplate}
|
|||
|
|
variant="outlined"
|
|||
|
|
size="small"
|
|||
|
|
sx={{ borderRadius: 2, textTransform: 'none' }}
|
|||
|
|
>
|
|||
|
|
{t('images.createTemplate', { defaultValue: '创建问题模板' })}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
|
|||
|
|
{/* 右侧:操作按钮 */}
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|||
|
|
<Button onClick={onClose} disabled={saving} variant="outlined" sx={{ borderRadius: 2 }}>
|
|||
|
|
{t('common.cancel')}
|
|||
|
|
</Button>
|
|||
|
|
<Button
|
|||
|
|
onClick={onSaveAndContinue}
|
|||
|
|
disabled={saving || !selectedTemplate}
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{ borderRadius: 2 }}
|
|||
|
|
>
|
|||
|
|
{saving ? <CircularProgress size={20} /> : t('images.saveAndContinue', { defaultValue: '保存并继续' })}
|
|||
|
|
</Button>
|
|||
|
|
<Button onClick={onSave} disabled={saving || !selectedTemplate} variant="contained" sx={{ borderRadius: 2 }}>
|
|||
|
|
{saving ? <CircularProgress size={20} /> : t('common.save')}
|
|||
|
|
</Button>
|
|||
|
|
</Box>
|
|||
|
|
</DialogActions>
|
|||
|
|
</Dialog>
|
|||
|
|
);
|
|||
|
|
}
|