first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,97 @@
'use client';
import { Button, CircularProgress } from '@mui/material';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import axios from 'axios';
import { toast } from 'sonner';
import { useAtomValue } from 'jotai/index';
import { selectedModelInfoAtom } from '@/lib/store';
/**
* AI 生成答案按钮组件
* @param {string} projectId - 项目ID
* @param {string} imageName - 图片名称
* @param {string} question - 问题内容
* @param {function} onSuccess - 生成成功的回调,接收生成的答案
* @param {boolean} previewOnly - 是否只预览(不保存数据集),默认 true
* @param {object} sx - 自定义样式
*/
export default function AIGenerateButton({
projectId,
imageName,
question,
onSuccess,
previewOnly = true,
sx = {},
answerType
}) {
const { t, i18n } = useTranslation();
const [loading, setLoading] = useState(false);
const model = useAtomValue(selectedModelInfoAtom);
const handleGenerate = async () => {
if (!projectId || !imageName || !question) {
toast.error(t('images.missingParameters', { defaultValue: '缺少必要参数' }));
return;
}
if (model.type !== 'vision') {
toast.error(t('images.visionModelRequired', { defaultValue: '请选择支持视觉的模型' }));
return;
}
setLoading(true);
try {
const response = await axios.post(`/api/projects/${projectId}/images/datasets`, {
imageName,
question,
model,
language: i18n.language,
previewOnly
});
if (response.data.success && response.data.answer) {
let data = response.data.answer;
if (answerType === 'label') {
try {
data = JSON.parse(response.data.answer);
} catch {}
}
onSuccess(data);
toast.success(t('images.aiGenerateSuccess', { defaultValue: 'AI 生成成功' }));
}
} catch (error) {
console.error('AI 生成失败:', error);
const errorMsg = error.response?.data?.error || t('images.aiGenerateFailed', { defaultValue: 'AI 生成失败' });
toast.error(errorMsg);
} finally {
setLoading(false);
}
};
return (
<Button
startIcon={loading ? <CircularProgress size={20} /> : <AutoAwesomeIcon />}
onClick={handleGenerate}
disabled={loading}
variant="outlined"
size="small"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 500,
borderWidth: 2,
'&:hover': {
borderWidth: 2
},
...sx
}}
>
{loading
? t('common.generating', { defaultValue: '生成中...' })
: t('images.aiGenerate', { defaultValue: 'AI 识别' })}
</Button>
);
}

View File

@@ -0,0 +1,268 @@
'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>
);
}

View File

@@ -0,0 +1,437 @@
'use client';
import { Box, Typography, TextField, Chip, Button, Paper } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import AddIcon from '@mui/icons-material/Add';
import AIGenerateButton from './AIGenerateButton';
export default function AnswerInput({
answerType,
answer,
onAnswerChange,
labels,
customFormat,
projectId,
imageName,
question
}) {
const { t, i18n } = useTranslation();
const [newLabel, setNewLabel] = useState('');
const [jsonError, setJsonError] = useState('');
// 文字类型输入
if (answerType === 'text') {
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="600" sx={{ color: 'text.primary' }}>
{t('images.answer', { defaultValue: '文本答案' })} *
</Typography>
<AIGenerateButton
projectId={projectId}
imageName={imageName}
question={question}
onSuccess={onAnswerChange}
/>
</Box>
<TextField
fullWidth
multiline
rows={6}
value={answer}
onChange={e => onAnswerChange(e.target.value)}
placeholder={t('images.answerPlaceholder', { defaultValue: '请输入答案...' })}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
backgroundColor: 'background.paper',
'& fieldset': {
borderWidth: 2,
borderColor: 'divider'
},
'&:hover fieldset': {
borderColor: 'primary.main'
},
'&.Mui-focused fieldset': {
borderColor: 'primary.main',
borderWidth: 2
}
},
'& textarea': {
fontSize: '14px',
lineHeight: 1.6
}
}}
/>
</Box>
);
}
// 标签类型输入 - 提前解析 labels避免条件中的 hooks 问题
if (answerType === 'label') {
const selectedLabels = Array.isArray(answer) ? answer : [];
// 解析 labels可能是 JSON 字符串或数组)
let labelOptions = [];
if (typeof labels === 'string' && labels) {
try {
labelOptions = JSON.parse(labels);
} catch (e) {
labelOptions = [];
}
} else if (Array.isArray(labels)) {
labelOptions = labels;
}
if (!labelOptions.includes('其他') && !labelOptions.includes('other')) {
labelOptions.push(i18n.language === 'en' ? 'other' : '其他');
}
const handleToggleLabel = label => {
if (selectedLabels.includes(label)) {
onAnswerChange(selectedLabels.filter(l => l !== label));
} else {
let newLabels = [...selectedLabels, label];
onAnswerChange(newLabels);
}
};
const handleAddNewLabel = () => {
if (newLabel.trim() && !labelOptions.includes(newLabel.trim())) {
handleToggleLabel(newLabel.trim());
setNewLabel('');
}
};
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h6" fontWeight="600" sx={{ color: 'text.primary' }}>
{t('images.selectLabels', { defaultValue: '标签选择' })} *
</Typography>
<AIGenerateButton
projectId={projectId}
imageName={imageName}
question={question}
onSuccess={onAnswerChange}
answerType={answerType}
/>
</Box>
{/* 可选标签 */}
<Paper
variant="outlined"
sx={{
p: 3,
mb: 3,
borderRadius: 3,
backgroundColor: 'grey.50',
border: '2px solid',
borderColor: 'divider',
'&:hover': {
borderColor: 'primary.light'
}
}}
>
<Typography variant="subtitle2" color="text.secondary" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
{t('images.availableLabels', { defaultValue: '可选标签' })}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5 }}>
{labelOptions && labelOptions.length > 0 ? (
labelOptions.map(label => (
<Chip
key={label}
label={label}
onClick={() => handleToggleLabel(label)}
color={selectedLabels.includes(label) ? 'primary' : 'default'}
variant={selectedLabels.includes(label) ? 'filled' : 'outlined'}
sx={{
borderRadius: 2,
fontWeight: 500,
fontSize: '0.875rem',
height: 36,
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
transform: 'translateY(-1px)',
boxShadow: 2
}
}}
/>
))
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
{t('images.noLabelsAvailable', { defaultValue: '暂无可选标签' })}
</Typography>
)}
</Box>
</Paper>
{/* 添加新标签 */}
{/* <Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<TextField
size="small"
value={newLabel}
onChange={e => setNewLabel(e.target.value)}
placeholder={t('images.addNewLabel', { defaultValue: '添加新标签...' })}
onKeyPress={e => {
if (e.key === 'Enter') {
handleAddNewLabel();
}
}}
sx={{
flex: 1,
'& .MuiOutlinedInput-root': {
borderRadius: 2,
backgroundColor: 'background.paper',
'& fieldset': {
borderWidth: 2
},
'&:hover fieldset': {
borderColor: 'primary.main'
}
}
}}
/>
<Button
startIcon={<AddIcon />}
onClick={handleAddNewLabel}
disabled={!newLabel.trim()}
variant="contained"
sx={{
borderRadius: 2,
px: 3,
fontWeight: 600,
textTransform: 'none',
boxShadow: 2,
'&:hover': {
boxShadow: 4
}
}}
>
{t('common.add', { defaultValue: '添加' })}
</Button>
</Box> */}
{/* 已选择标签 */}
{/* {selectedLabels.length > 0 && (
<Box>
<Typography variant="subtitle2" color="text.secondary" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
{t('images.selectedLabels', { defaultValue: '已选择' })} ({selectedLabels.length})
</Typography>
<Paper
sx={{
p: 2.5,
borderRadius: 3,
backgroundColor: 'primary.50',
border: '2px solid',
borderColor: 'primary.200'
}}
>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5 }}>
{selectedLabels.map(label => (
<Chip
key={label}
label={label}
onDelete={() => handleToggleLabel(label)}
color="primary"
sx={{
borderRadius: 2,
fontWeight: 500,
fontSize: '0.875rem',
height: 36,
'& .MuiChip-deleteIcon': {
fontSize: '18px',
'&:hover': {
color: 'error.main'
}
}
}}
/>
))}
</Box>
</Paper>
</Box>
)} */}
</Box>
);
}
// 自定义格式输入
if (answerType === 'custom_format') {
const handleJsonChange = value => {
onAnswerChange(value);
// 验证 JSON 格式
if (value.trim()) {
try {
JSON.parse(value);
setJsonError('');
} catch (e) {
setJsonError(t('images.invalidJsonFormat', { defaultValue: 'JSON 格式不正确' }));
}
} else {
setJsonError('');
}
};
const handleUseTemplate = () => {
if (customFormat) {
try {
let templateJson;
if (typeof customFormat === 'string') {
templateJson = JSON.parse(customFormat);
} else {
templateJson = customFormat;
}
const formatted = JSON.stringify(templateJson, null, 2);
onAnswerChange(formatted);
setJsonError('');
} catch (e) {
onAnswerChange('{}');
}
}
};
if (answer && typeof answer === 'object') {
answer = JSON.stringify(answer, null, 2);
}
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h6" fontWeight="600" sx={{ color: 'text.primary' }}>
{t('images.customFormatAnswer', { defaultValue: '自定义格式答案' })} *
</Typography>
<Box sx={{ display: 'flex', gap: 1.5 }}>
<AIGenerateButton
projectId={projectId}
imageName={imageName}
question={question}
onSuccess={onAnswerChange}
/>
{customFormat && (
<Button
size="small"
onClick={handleUseTemplate}
variant="outlined"
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 500,
borderWidth: 2,
'&:hover': {
borderWidth: 2
}
}}
>
{t('images.useTemplate', { defaultValue: '使用模板' })}
</Button>
)}
{/* <Button
size="small"
onClick={handleFormatJson}
variant="outlined"
disabled={!answer.trim()}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 500,
borderWidth: 2,
'&:hover': {
borderWidth: 2
}
}}
>
{t('images.formatJson', { defaultValue: '格式化' })}
</Button> */}
</Box>
</Box>
{/* 显示格式要求 */}
{customFormat && (
<Paper
variant="outlined"
sx={{
p: 3,
mb: 3,
bgcolor: 'grey.50',
borderRadius: 3,
border: '2px solid',
borderColor: 'divider'
}}
>
<Typography variant="subtitle2" color="text.secondary" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
{t('images.formatRequirement', { defaultValue: '格式要求' })}
</Typography>
<Box
sx={{
backgroundColor: 'background.paper',
borderRadius: 2,
p: 2,
border: '1px solid',
borderColor: 'divider'
}}
>
<pre
style={{
margin: 0,
fontSize: '13px',
overflow: 'auto',
maxHeight: '150px',
fontFamily: 'Monaco, Consolas, "Courier New", monospace',
lineHeight: 1.5,
color: '#2d3748'
}}
>
{typeof customFormat === 'string' ? customFormat : JSON.stringify(customFormat, null, 2)}
</pre>
</Box>
</Paper>
)}
{/* JSON 输入框 */}
<TextField
fullWidth
multiline
rows={10}
value={answer}
onChange={e => handleJsonChange(e.target.value)}
placeholder={t('images.customFormatPlaceholder', { defaultValue: '请输入符合格式的 JSON...' })}
error={!!jsonError}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
backgroundColor: 'background.paper',
'& fieldset': {
borderWidth: 2
},
'&:hover fieldset': {
borderColor: 'primary.main'
},
'&.Mui-focused fieldset': {
borderColor: 'primary.main',
borderWidth: 2
},
'&.Mui-error fieldset': {
borderColor: 'error.main',
borderWidth: 2
}
},
'& textarea': {
fontFamily: 'Monaco, Consolas, "Courier New", monospace',
fontSize: '13px',
lineHeight: 1.5
},
'& .MuiFormHelperText-root': {
fontSize: '0.875rem',
fontWeight: 500
}
}}
/>
</Box>
);
}
return null;
}

View File

@@ -0,0 +1,200 @@
'use client';
import { Autocomplete, TextField, Box, Typography, Chip, Button, Dialog } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
export default function QuestionSelector({
templates,
selectedTemplate,
onTemplateChange,
answeredQuestions = [],
unansweredQuestions = [],
onOpenCreateQuestion,
onOpenCreateTemplate
}) {
const { t } = useTranslation();
const [showNoQuestionsMessage, setShowNoQuestionsMessage] = useState(false);
// 构建未完成标注的问题选项(用于下拉框)
const dropdownOptions = unansweredQuestions.map(q => ({
...q,
isUnanswered: true
}));
const getAnswerTypeLabel = answerType => {
switch (answerType) {
case 'text':
return t('images.answerTypeText', { defaultValue: '文字' });
case 'label':
return t('images.answerTypeLabel', { defaultValue: '标签' });
case 'custom_format':
return t('images.answerTypeCustomFormat', { defaultValue: '自定义格式' });
default:
return answerType;
}
};
// 判断是否有待标注问题
const hasUnansweredQuestions = unansweredQuestions.length > 0;
const hasAnsweredQuestions = answeredQuestions.length > 0;
const hasAnyQuestions = hasUnansweredQuestions || hasAnsweredQuestions;
return (
<Box>
{/* 已标注问题区域 - 优化显示为一行,添加最大高度 */}
{answeredQuestions.length > 0 && (
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" fontWeight="600" gutterBottom sx={{ mb: 1.5, color: 'text.secondary' }}>
{t('images.answeredQuestions', { defaultValue: '已标注问题' })} ({answeredQuestions.length})
</Typography>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
maxHeight: 120,
overflowY: 'auto',
paddingRight: 1,
'&::-webkit-scrollbar': {
width: '6px'
},
'&::-webkit-scrollbar-track': {
bgcolor: 'transparent'
},
'&::-webkit-scrollbar-thumb': {
bgcolor: 'action.disabled',
borderRadius: 1,
'&:hover': {
bgcolor: 'action.active'
}
}
}}
>
{answeredQuestions.map(question => (
<Chip
key={question.id}
label={question.question}
size="small"
color="success"
variant="outlined"
sx={{
borderRadius: 2,
fontWeight: 500,
fontSize: '0.875rem',
maxWidth: '100%',
'& .MuiChip-label': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}}
/>
))}
</Box>
</Box>
)}
{/* 问题选择下拉框 */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" fontWeight="600" gutterBottom sx={{ mb: 2 }}>
{t('images.selectNewQuestion', { defaultValue: '选择新问题' })}
</Typography>
{!hasUnansweredQuestions ? (
// 没有待标注问题的提示
<Box
sx={{
p: 3,
textAlign: 'center',
bgcolor: 'background.paper',
border: '1px dashed',
borderColor: 'divider',
borderRadius: 2,
mb: 2
}}
>
{hasAnsweredQuestions ? (
<Typography color="text.secondary" sx={{ mb: 1 }}>
{t('images.allQuestionsAnnotated', { defaultValue: '当前图片所有问题已标注完成' })}
</Typography>
) : (
<Typography color="text.secondary" sx={{ mb: 1 }}>
{t('images.noQuestionsAssociated', { defaultValue: '当前图片未关联任何问题' })}
</Typography>
)}
</Box>
) : (
// 有待标注问题时显示下拉框
<Autocomplete
fullWidth
options={dropdownOptions}
value={selectedTemplate}
onChange={(event, newValue) => {
if (newValue) {
onTemplateChange(newValue);
}
}}
getOptionLabel={option => option.question || ''}
renderOption={(props, option) => (
<Box
component="li"
{...props}
sx={{
py: 1.5,
borderRadius: 1,
mx: 1,
my: 0.5,
'&:hover': {
bgcolor: 'action.hover'
}
}}
>
<Box sx={{ flex: 1 }}>
<Typography variant="body2" fontWeight="500">
{option.question}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
<Chip label={getAnswerTypeLabel(option.answerType)} size="small" sx={{ borderRadius: 1 }} />
<Chip
label={t('images.pendingAnswer', { defaultValue: '待标注' })}
size="small"
color="warning"
variant="filled"
sx={{ borderRadius: 1, fontSize: '0.75rem' }}
/>
</Box>
</Box>
</Box>
)}
renderInput={params => (
<TextField
{...params}
placeholder={t('images.selectQuestionPlaceholder', { defaultValue: '请选择问题进行标注...' })}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 2,
'& fieldset': {
borderWidth: 2
},
'&:hover fieldset': {
borderColor: 'primary.main'
}
}
}}
/>
)}
isOptionEqualToValue={(option, value) => option.id === value.id}
/>
)}
{selectedTemplate && selectedTemplate.description && (
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
{selectedTemplate.description}
</Typography>
)}
</Box>
</Box>
);
}