Files
YG-Datasets/easy-dataset-main/app/projects/[projectId]/images/components/annotation/AnswerInput.js

438 lines
13 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 { 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;
}