438 lines
13 KiB
JavaScript
438 lines
13 KiB
JavaScript
|
|
'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;
|
|||
|
|
}
|