first-update
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 确认对话框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调函数
|
||||
* @param {Function} props.onConfirm - 确认操作的回调函数
|
||||
* @param {string} props.title - 对话框标题
|
||||
* @param {string} props.content - 对话框内容
|
||||
* @param {string} props.confirmText - 确认按钮文本,默认为 "确认删除"
|
||||
* @param {string} props.cancelText - 取消按钮文本,默认为 "取消"
|
||||
* @param {string} props.confirmColor - 确认按钮颜色,默认为 "error"
|
||||
*/
|
||||
export default function ConfirmDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
content,
|
||||
confirmText,
|
||||
cancelText,
|
||||
confirmColor = 'error'
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleConfirm = () => {
|
||||
onClose();
|
||||
if (onConfirm) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-description"
|
||||
>
|
||||
<DialogTitle id="confirm-dialog-title">{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="confirm-dialog-description">{content}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{cancelText || t('common.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} color={confirmColor} variant="contained" autoFocus>
|
||||
{confirmText || t('common.confirmDelete')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Box,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
|
||||
export default function ExportQuestionsDialog({ open, onClose, onExport, selectedCount, totalCount }) {
|
||||
const { t } = useTranslation();
|
||||
const [format, setFormat] = useState('json');
|
||||
const [exportScope, setExportScope] = useState('all');
|
||||
|
||||
const handleExport = () => {
|
||||
const exportOptions = {
|
||||
format,
|
||||
selectedIds: exportScope === 'selected' ? [] : undefined
|
||||
};
|
||||
|
||||
onExport(exportOptions);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('questions.exportQuestions')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, mt: 2 }}>
|
||||
{/* 导出范围 */}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{t('questions.exportScope')}</FormLabel>
|
||||
<RadioGroup value={exportScope} onChange={e => setExportScope(e.target.value)}>
|
||||
<FormControlLabel
|
||||
value="all"
|
||||
control={<Radio />}
|
||||
label={t('questions.exportAll', { count: totalCount })}
|
||||
/>
|
||||
{selectedCount > 0 && (
|
||||
<FormControlLabel
|
||||
value="selected"
|
||||
control={<Radio />}
|
||||
label={t('questions.exportSelected', { count: selectedCount })}
|
||||
/>
|
||||
)}
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 导出格式 */}
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">{t('questions.exportFormat')}</FormLabel>
|
||||
<RadioGroup value={format} onChange={e => setFormat(e.target.value)}>
|
||||
<FormControlLabel value="json" control={<Radio />} label="JSON" />
|
||||
<FormControlLabel value="jsonl" control={<Radio />} label="JSONL" />
|
||||
<FormControlLabel value="txt" control={<Radio />} label={t('questions.txtFormat')} />
|
||||
<FormControlLabel value="csv" control={<Radio />} label="CSV" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleExport} variant="contained" startIcon={<DownloadIcon />}>
|
||||
{t('export.title')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
Box,
|
||||
Autocomplete,
|
||||
TextField as MuiTextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem
|
||||
} from '@mui/material';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function QuestionEditDialog({
|
||||
open,
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
projectId,
|
||||
tags,
|
||||
mode = 'create' // 'create' or 'edit'
|
||||
}) {
|
||||
const [chunks, setChunks] = useState([]);
|
||||
const [images, setImages] = useState([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 获取文本块的标题
|
||||
const getChunkTitle = chunkId => {
|
||||
const chunk = chunks.find(c => c.id === chunkId);
|
||||
return chunk?.name || chunkId; // 直接使用文件名
|
||||
};
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
id: '',
|
||||
question: '',
|
||||
sourceType: 'text', // 新增:数据源类型
|
||||
chunkId: '',
|
||||
imageId: '', // 新增:图片ID
|
||||
label: '' // 默认不选中任何标签
|
||||
});
|
||||
|
||||
const getChunks = async projectId => {
|
||||
// 获取文本块列表
|
||||
const response = await axios.get(`/api/projects/${projectId}/split`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(t('common.fetchError'));
|
||||
}
|
||||
setChunks(response.data.chunks || []);
|
||||
};
|
||||
|
||||
const getImages = async projectId => {
|
||||
// 获取图片列表(只获取ID和名称)
|
||||
try {
|
||||
const response = await axios.get(`/api/projects/${projectId}/images?page=1&pageSize=10000&simple=true`);
|
||||
if (response.status === 200) {
|
||||
setImages(response.data.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch images:', error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getChunks(projectId);
|
||||
getImages(projectId);
|
||||
if (initialData) {
|
||||
// 根据 imageId 判断数据源类型
|
||||
console.log('initialData:', initialData);
|
||||
const sourceType = initialData.imageId ? 'image' : 'text';
|
||||
setFormData({
|
||||
id: initialData.id,
|
||||
question: initialData.question || '',
|
||||
sourceType: sourceType,
|
||||
chunkId: initialData.chunkId || '',
|
||||
imageId: initialData.imageId || '',
|
||||
label: initialData.label || 'other' // 改用 label 而不是 label
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
id: '',
|
||||
question: '',
|
||||
sourceType: 'text',
|
||||
chunkId: '',
|
||||
imageId: '',
|
||||
label: ''
|
||||
});
|
||||
}
|
||||
}, [initialData]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSubmit(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const flattenTags = (tags = [], prefix = '') => {
|
||||
let flatTags = [];
|
||||
const traverse = node => {
|
||||
flatTags.push({
|
||||
id: node.label, // 使用标签名作为 id
|
||||
label: node.label, // 直接使用原始标签名
|
||||
originalLabel: node.label
|
||||
});
|
||||
if (node.child && node.child.length > 0) {
|
||||
node.child.forEach(child => traverse(child));
|
||||
}
|
||||
};
|
||||
tags.forEach(tag => traverse(tag));
|
||||
flatTags.push({
|
||||
id: 'other',
|
||||
label: t('datasets.uncategorized'),
|
||||
originalLabel: 'other'
|
||||
});
|
||||
return flatTags;
|
||||
};
|
||||
|
||||
const flattenedTags = useMemo(() => flattenTags(tags), [tags, t]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{mode === 'create' ? t('questions.createQuestion') : t('questions.editQuestion')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
|
||||
{/* 数据源类型选择 */}
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('questions.sourceType', { defaultValue: '数据源类型' })}</InputLabel>
|
||||
<Select
|
||||
value={formData.sourceType}
|
||||
label={t('questions.sourceType', { defaultValue: '数据源类型' })}
|
||||
onChange={e => {
|
||||
setFormData({
|
||||
...formData,
|
||||
sourceType: e.target.value,
|
||||
chunkId: '',
|
||||
imageId: ''
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MenuItem value="text">{t('questions.template.sourceType.text')}</MenuItem>
|
||||
<MenuItem value="image">{t('questions.template.sourceType.image')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 问题内容 */}
|
||||
<TextField
|
||||
label={t('questions.questionContent')}
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
value={formData.question}
|
||||
onChange={e => setFormData({ ...formData, question: e.target.value })}
|
||||
/>
|
||||
|
||||
{/* 文本块选择(仅当数据源为文本时显示) */}
|
||||
{formData.sourceType === 'text' && (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
options={chunks}
|
||||
getOptionLabel={chunk => getChunkTitle(chunk.id)}
|
||||
value={chunks.find(chunk => chunk.id === formData.chunkId) || null}
|
||||
onChange={(e, newValue) => setFormData({ ...formData, chunkId: newValue ? newValue.id : '' })}
|
||||
renderInput={params => (
|
||||
<MuiTextField {...params} label={t('questions.selectChunk')} placeholder={t('questions.searchChunk')} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 图片选择(仅当数据源为图片时显示) */}
|
||||
{formData.sourceType === 'image' && (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
options={images}
|
||||
getOptionLabel={image => image.imageName || ''}
|
||||
value={images.find(image => image.id === formData.imageId) || null}
|
||||
onChange={(e, newValue) => setFormData({ ...formData, imageId: newValue ? newValue.id : '' })}
|
||||
renderInput={params => (
|
||||
<MuiTextField
|
||||
{...params}
|
||||
label={t('questions.selectImage', { defaultValue: '选择图片' })}
|
||||
placeholder={t('questions.searchImage', { defaultValue: '搜索图片...' })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 标签选择 */}
|
||||
{formData.sourceType === 'text' && (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
options={flattenedTags}
|
||||
getOptionLabel={tag => tag.label}
|
||||
value={flattenedTags.find(tag => tag.id === formData.label) || null}
|
||||
onChange={(e, newValue) => setFormData({ ...formData, label: newValue ? newValue.id : '' })}
|
||||
renderInput={params => (
|
||||
<MuiTextField {...params} label={t('questions.selectTag')} placeholder={t('questions.searchTag')} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="contained"
|
||||
disabled={!formData.question || (formData.sourceType === 'text' ? !formData.chunkId : !formData.imageId)}
|
||||
>
|
||||
{mode === 'create' ? t('common.create') : t('common.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { Box, Stack, Checkbox, Typography, TextField, InputAdornment, Select, MenuItem, useTheme } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
export default function QuestionsFilter({
|
||||
// 选择相关
|
||||
selectedQuestionsCount,
|
||||
totalQuestions,
|
||||
isAllSelected,
|
||||
isIndeterminate,
|
||||
onSelectAll,
|
||||
|
||||
// 搜索相关
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
searchMatchMode,
|
||||
onSearchMatchModeChange,
|
||||
|
||||
// 过滤相关
|
||||
answerFilter,
|
||||
onFilterChange,
|
||||
|
||||
// 文本块名称筛选
|
||||
chunkNameFilter,
|
||||
onChunkNameFilterChange,
|
||||
|
||||
// 数据源类型筛选
|
||||
sourceTypeFilter,
|
||||
onSourceTypeFilterChange,
|
||||
|
||||
activeTab
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (activeTab === 1) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
{/* 选择区域 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Checkbox checked={isAllSelected} indeterminate={isIndeterminate} onChange={onSelectAll} />
|
||||
<Typography variant="body2" sx={{ ml: 1 }}>
|
||||
{selectedQuestionsCount > 0
|
||||
? t('questions.selectedCount', { count: selectedQuestionsCount })
|
||||
: t('questions.selectAll')}
|
||||
(
|
||||
{t('questions.totalCount', {
|
||||
count: totalQuestions
|
||||
})}
|
||||
)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* 搜索和过滤区域 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||
{/* 组合搜索框:下拉选择(匹配/不匹配)+ 输入框 */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', width: { xs: '100%', sm: 300 } }}>
|
||||
<Select
|
||||
value={searchMatchMode}
|
||||
onChange={onSearchMatchModeChange}
|
||||
size="small"
|
||||
sx={{
|
||||
width: 110,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'white',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderRight: 'none',
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.23)'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.87)'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="match">{t('questions.searchMatch')}</MenuItem>
|
||||
<MenuItem value="notMatch">{t('questions.searchNotMatch')}</MenuItem>
|
||||
</Select>
|
||||
<TextField
|
||||
placeholder={t('questions.searchPlaceholder')}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
fullWidth
|
||||
value={searchTerm}
|
||||
onChange={onSearchChange}
|
||||
sx={{
|
||||
flex: 1,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
}
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" color="action" />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<TextField
|
||||
placeholder={t('questions.filterChunkNamePlaceholder')}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ width: { xs: '100%', sm: 200 } }}
|
||||
value={chunkNameFilter}
|
||||
onChange={onChunkNameFilterChange}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" color="action" />
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={sourceTypeFilter}
|
||||
onChange={onSourceTypeFilterChange}
|
||||
size="small"
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 150 },
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'white',
|
||||
borderRadius: '8px',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.23)'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.87)'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
elevation: 2,
|
||||
sx: { mt: 1, borderRadius: 2 }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">{t('questions.sourceTypeAll')}</MenuItem>
|
||||
<MenuItem value="text">{t('questions.sourceTypeText')}</MenuItem>
|
||||
<MenuItem value="image">{t('questions.sourceTypeImage')}</MenuItem>
|
||||
</Select>
|
||||
<Select
|
||||
value={answerFilter}
|
||||
onChange={onFilterChange}
|
||||
size="small"
|
||||
sx={{
|
||||
width: { xs: '100%', sm: 150 },
|
||||
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'white',
|
||||
borderRadius: '8px',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.23)'
|
||||
},
|
||||
'&:hover .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: theme.palette.mode === 'dark' ? 'transparent' : 'rgba(0, 0, 0, 0.87)'
|
||||
},
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'primary.main'
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
elevation: 2,
|
||||
sx: { mt: 1, borderRadius: 2 }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuItem value="all">{t('questions.filterAll')}</MenuItem>
|
||||
<MenuItem value="answered">{t('questions.filterAnswered')}</MenuItem>
|
||||
<MenuItem value="unanswered">{t('questions.filterUnanswered')}</MenuItem>
|
||||
</Select>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Box, Typography, Button, Tooltip, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
import DatasetIcon from '@mui/icons-material/Dataset';
|
||||
import ChatIcon from '@mui/icons-material/Chat';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import LibraryAddIcon from '@mui/icons-material/LibraryAdd';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
|
||||
export default function QuestionsPageHeader({
|
||||
questionsTotal,
|
||||
selectedQuestionsCount,
|
||||
onBatchDeleteQuestions,
|
||||
onOpenCreateDialog,
|
||||
onOpenCreateTemplateDialog,
|
||||
onBatchGenerateAnswers,
|
||||
onAutoGenerateDatasets,
|
||||
onAutoGenerateMultiTurnDatasets,
|
||||
onAutoGenerateImageDatasets,
|
||||
onExportQuestions,
|
||||
activeTab
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [createAnchorEl, setCreateAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const createMenuOpen = Boolean(createAnchorEl);
|
||||
|
||||
const handleMenuClick = event => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleCreateMenuClick = event => {
|
||||
setCreateAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCreateMenuClose = () => {
|
||||
setCreateAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleCreateQuestion = () => {
|
||||
handleCreateMenuClose();
|
||||
onOpenCreateDialog();
|
||||
};
|
||||
|
||||
const handleCreateTemplate = () => {
|
||||
handleCreateMenuClose();
|
||||
onOpenCreateTemplateDialog();
|
||||
};
|
||||
|
||||
const handleSingleTurnGenerate = () => {
|
||||
handleMenuClose();
|
||||
onAutoGenerateDatasets();
|
||||
};
|
||||
|
||||
const handleMultiTurnGenerate = () => {
|
||||
handleMenuClose();
|
||||
onAutoGenerateMultiTurnDatasets();
|
||||
};
|
||||
|
||||
const handleImageDatasetGenerate = () => {
|
||||
handleMenuClose();
|
||||
onAutoGenerateImageDatasets();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h4">
|
||||
{t('questions.title')} ({questionsTotal})
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="outlined" startIcon={<DownloadIcon />} onClick={onExportQuestions}>
|
||||
{t('questions.exportQuestions')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color={selectedQuestionsCount > 0 ? 'error' : 'primary'}
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={onBatchDeleteQuestions}
|
||||
disabled={selectedQuestionsCount === 0}
|
||||
>
|
||||
{t('questions.deleteSelected')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleCreateMenuClick}
|
||||
>
|
||||
{t('questions.createQuestion')}
|
||||
</Button>
|
||||
|
||||
<Menu
|
||||
anchorEl={createAnchorEl}
|
||||
open={createMenuOpen}
|
||||
onClose={handleCreateMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleCreateQuestion}>
|
||||
<ListItemIcon>
|
||||
<AddIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('questions.createNormalQuestion')} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCreateTemplate}>
|
||||
<ListItemIcon>
|
||||
<LibraryAddIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('questions.createQuestionTemplate')} />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* <Button
|
||||
variant="contained"
|
||||
startIcon={<AutoFixHighIcon />}
|
||||
onClick={onBatchGenerateAnswers}
|
||||
disabled={selectedQuestionsCount === 0}
|
||||
>
|
||||
{t('questions.batchGenerate')}
|
||||
</Button> */}
|
||||
|
||||
<Tooltip
|
||||
title={t('questions.autoGenerateDatasetTip', {
|
||||
defaultValue: '创建后台批量处理任务:自动查询待生成答案的问题并生成数据集'
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
startIcon={<AutoFixHighIcon />}
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
onClick={handleMenuClick}
|
||||
>
|
||||
{t('questions.autoGenerateDataset', { defaultValue: '自动生成数据集' })}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleMenuClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleSingleTurnGenerate}>
|
||||
<ListItemIcon>
|
||||
<DatasetIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('questions.generateSingleTurnDataset', { defaultValue: '生成单轮对话数据集' })} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleMultiTurnGenerate}>
|
||||
<ListItemIcon>
|
||||
<ChatIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('questions.generateMultiTurnDataset', { defaultValue: '生成多轮对话数据集' })} />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleImageDatasetGenerate}>
|
||||
<ListItemIcon>
|
||||
<ImageIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('questions.generateImageDataset', { defaultValue: '生成图像问答数据集' })} />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
IconButton,
|
||||
Chip,
|
||||
Typography,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function TemplateListView({ templates, onEditTemplate, onDeleteTemplate, loading }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getAnswerTypeLabel = type => {
|
||||
const labels = {
|
||||
text: t('questions.template.answerType.text'),
|
||||
label: t('questions.template.answerType.tags'),
|
||||
custom_format: t('questions.template.answerType.customFormat')
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
const getSourceTypeLabel = type => {
|
||||
const labels = {
|
||||
image: t('questions.template.sourceType.image'),
|
||||
text: t('questions.template.sourceType.text')
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography>{t('common.loading')}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!templates || templates.length === 0) {
|
||||
return (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Alert severity="info">{t('questions.template.noTemplates')}</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t('questions.template.question')}</TableCell>
|
||||
<TableCell>{t('questions.template.sourceType.label')}</TableCell>
|
||||
<TableCell>{t('questions.template.answerType.label')}</TableCell>
|
||||
<TableCell>{t('questions.template.description')}</TableCell>
|
||||
<TableCell>{t('questions.template.used')}</TableCell>
|
||||
<TableCell align="right">{t('common.actions')}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{templates.map(template => (
|
||||
<TableRow key={template.id} hover>
|
||||
<TableCell>
|
||||
<Typography variant="body2" sx={{ maxWidth: 300 }}>
|
||||
{template.question}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip
|
||||
label={getSourceTypeLabel(template.sourceType)}
|
||||
size="small"
|
||||
color={template.sourceType === 'image' ? 'primary' : 'secondary'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Chip label={getAnswerTypeLabel(template.answerType)} size="small" color="default" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 200 }}>
|
||||
{template.description || '-'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{template.usageCount > 0 ? (
|
||||
<Chip label={template.usageCount} size="small" color="success" />
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
0
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconButton size="small" onClick={() => onEditTemplate(template)} sx={{ mr: 1 }}>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onDeleteTemplate(template.id)}
|
||||
disabled={template.usageCount > 0}
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Box,
|
||||
Chip,
|
||||
Typography,
|
||||
Alert,
|
||||
FormControlLabel,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function TemplateFormDialog({ open, onClose, onSubmit, template }) {
|
||||
const { t } = useTranslation();
|
||||
const [formData, setFormData] = useState({
|
||||
question: '',
|
||||
sourceType: 'text',
|
||||
answerType: 'text',
|
||||
description: '',
|
||||
labels: [],
|
||||
customFormat: '',
|
||||
autoGenerate: true
|
||||
});
|
||||
const [labelInput, setLabelInput] = useState('');
|
||||
const [errors, setErrors] = useState({});
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (template) {
|
||||
setFormData({
|
||||
question: template.question || '',
|
||||
sourceType: template.sourceType || 'text',
|
||||
answerType: template.answerType || 'text',
|
||||
description: template.description || '',
|
||||
labels: template.labels || [],
|
||||
customFormat: template.customFormat ? JSON.stringify(template.customFormat, null, 2) : '',
|
||||
autoGenerate: true // 编辑模式下默认不自动生成
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
question: '',
|
||||
sourceType: 'text',
|
||||
answerType: 'text',
|
||||
description: '',
|
||||
labels: [],
|
||||
customFormat: '',
|
||||
autoGenerate: true
|
||||
});
|
||||
}
|
||||
setErrors({});
|
||||
setShowConfirmDialog(false);
|
||||
}, [template, open]);
|
||||
|
||||
const handleChange = (field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
// 清除该字段的错误
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: null }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddLabel = () => {
|
||||
const trimmed = labelInput.trim();
|
||||
if (trimmed && !formData.labels.includes(trimmed)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
labels: [...prev.labels, trimmed]
|
||||
}));
|
||||
setLabelInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLabel = labelToDelete => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
labels: prev.labels.filter(label => label !== labelToDelete)
|
||||
}));
|
||||
};
|
||||
|
||||
const validate = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.question.trim()) {
|
||||
newErrors.question = t('questions.template.errors.questionRequired');
|
||||
}
|
||||
|
||||
if (formData.answerType === 'label' && formData.labels.length === 0) {
|
||||
newErrors.labels = t('questions.template.errors.labelsRequired');
|
||||
}
|
||||
|
||||
if (formData.answerType === 'custom_format') {
|
||||
if (!formData.customFormat.trim()) {
|
||||
newErrors.customFormat = t('questions.template.errors.customFormatRequired');
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(formData.customFormat);
|
||||
} catch (e) {
|
||||
newErrors.customFormat = t('questions.template.errors.invalidJson');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果选择了自动生成,显示确认对话框
|
||||
if (formData.autoGenerate) {
|
||||
setShowConfirmDialog(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 直接提交
|
||||
submitTemplate();
|
||||
};
|
||||
|
||||
const submitTemplate = () => {
|
||||
const submitData = {
|
||||
question: formData.question.trim(),
|
||||
sourceType: formData.sourceType,
|
||||
answerType: formData.answerType,
|
||||
description: formData.description.trim(),
|
||||
autoGenerate: formData.autoGenerate,
|
||||
templateId: template?.id // 编辑模式时传递模板ID,用于查找未创建问题的数据源
|
||||
};
|
||||
|
||||
if (formData.answerType === 'label') {
|
||||
submitData.labels = formData.labels;
|
||||
}
|
||||
|
||||
if (formData.answerType === 'custom_format') {
|
||||
try {
|
||||
submitData.customFormat = JSON.parse(formData.customFormat);
|
||||
} catch (e) {
|
||||
// 已在验证中处理
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(submitData);
|
||||
setShowConfirmDialog(false);
|
||||
};
|
||||
|
||||
const handleConfirmGenerate = () => {
|
||||
submitTemplate();
|
||||
};
|
||||
|
||||
const handleCancelGenerate = () => {
|
||||
setShowConfirmDialog(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{template ? t('questions.template.edit') : t('questions.template.create')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{/* 数据源类型 */}
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('questions.template.sourceTypeInfo')}</InputLabel>
|
||||
<Select
|
||||
value={formData.sourceType}
|
||||
label={t('questions.template.sourceTypeInfo')}
|
||||
onChange={e => handleChange('sourceType', e.target.value)}
|
||||
>
|
||||
<MenuItem value="text">{t('questions.template.sourceType.text')}</MenuItem>
|
||||
<MenuItem value="image">{t('questions.template.sourceType.image')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 问题内容 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.question')}
|
||||
value={formData.question}
|
||||
onChange={e => handleChange('question', e.target.value)}
|
||||
error={!!errors.question}
|
||||
helperText={errors.question}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* 答案类型 */}
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{t('questions.template.answerType.label')}</InputLabel>
|
||||
<Select
|
||||
value={formData.answerType}
|
||||
label={t('questions.template.answerType.label')}
|
||||
onChange={e => handleChange('answerType', e.target.value)}
|
||||
>
|
||||
<MenuItem value="text">{t('questions.template.answerType.text')}</MenuItem>
|
||||
<MenuItem value="label">{t('questions.template.answerType.tags')}</MenuItem>
|
||||
<MenuItem value="custom_format">{t('questions.template.answerType.customFormat')}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* 描述 */}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.description')}
|
||||
value={formData.description}
|
||||
onChange={e => handleChange('description', e.target.value)}
|
||||
helperText={t('questions.template.descriptionHelp')}
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
{/* 标签输入 (仅当答案类型为 label 时显示) */}
|
||||
{formData.answerType === 'label' && (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.addLabel')}
|
||||
value={labelInput}
|
||||
onChange={e => setLabelInput(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleAddLabel();
|
||||
}
|
||||
}}
|
||||
error={!!errors.labels}
|
||||
helperText={errors.labels}
|
||||
/>
|
||||
<Button variant="outlined" onClick={handleAddLabel} sx={{ minWidth: '100px' }}>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{formData.labels.map(label => (
|
||||
<Chip
|
||||
key={label}
|
||||
label={label}
|
||||
onDelete={() => handleDeleteLabel(label)}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 自定义格式输入 (仅当答案类型为 custom_format 时显示) */}
|
||||
{formData.answerType === 'custom_format' && (
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('questions.template.customFormat')}
|
||||
value={formData.customFormat}
|
||||
onChange={e => handleChange('customFormat', e.target.value)}
|
||||
multiline
|
||||
rows={6}
|
||||
error={!!errors.customFormat}
|
||||
helperText={errors.customFormat || t('questions.template.customFormatHelp')}
|
||||
placeholder='{"field1": "description", "field2": "description"}'
|
||||
/>
|
||||
<Alert severity="info" sx={{ mt: 1 }}>
|
||||
{t('questions.template.customFormatInfo')}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 自动生成问题选项 */}
|
||||
<Box>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={formData.autoGenerate}
|
||||
onChange={e => handleChange('autoGenerate', e.target.checked)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={t('questions.template.autoGenerate')}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ ml: 4, mt: 0.5 }}>
|
||||
{formData.sourceType === 'text'
|
||||
? t('questions.template.autoGenerateHelpText')
|
||||
: t('questions.template.autoGenerateHelpImage')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleSubmit} variant="contained">
|
||||
{template ? t('common.save') : t('common.create')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
{/* 自动生成确认对话框 */}
|
||||
<Dialog open={showConfirmDialog} onClose={handleCancelGenerate}>
|
||||
<DialogTitle>{t('questions.template.confirmAutoGenerate')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
{template
|
||||
? formData.sourceType === 'text'
|
||||
? t('questions.template.confirmAutoGenerateEditTextMessage', {
|
||||
defaultValue: '您选择了自动生成问题。系统将为所有还未创建此模板问题的文本块创建问题。'
|
||||
})
|
||||
: t('questions.template.confirmAutoGenerateEditImageMessage', {
|
||||
defaultValue: '您选择了自动生成问题。系统将为所有还未创建此模板问题的图片创建问题。'
|
||||
})
|
||||
: formData.sourceType === 'text'
|
||||
? t('questions.template.confirmAutoGenerateTextMessage')
|
||||
: t('questions.template.confirmAutoGenerateImageMessage')}
|
||||
</Typography>
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
{t('questions.template.autoGenerateWarning')}
|
||||
</Alert>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleCancelGenerate}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleConfirmGenerate} variant="contained" color="primary">
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Chip,
|
||||
Typography,
|
||||
Alert,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import TemplateFormDialog from './TemplateFormDialog';
|
||||
|
||||
export default function TemplateManagementDialog({
|
||||
open,
|
||||
onClose,
|
||||
templates,
|
||||
onCreateTemplate,
|
||||
onUpdateTemplate,
|
||||
onDeleteTemplate,
|
||||
loading
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||
const [currentTab, setCurrentTab] = useState(0); // 0: image, 1: text
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingTemplate(null);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = template => {
|
||||
setEditingTemplate(template);
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = async templateId => {
|
||||
const confirmed = window.confirm(t('questions.template.deleteConfirm'));
|
||||
if (confirmed) {
|
||||
await onDeleteTemplate(templateId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async data => {
|
||||
// 根据当前tab添加sourceType
|
||||
const sourceType = currentTab === 0 ? 'image' : 'text';
|
||||
const templateData = { ...data, sourceType };
|
||||
|
||||
if (editingTemplate) {
|
||||
await onUpdateTemplate(editingTemplate.id, templateData);
|
||||
} else {
|
||||
await onCreateTemplate(templateData);
|
||||
}
|
||||
setFormOpen(false);
|
||||
};
|
||||
|
||||
const getAnswerTypeLabel = type => {
|
||||
const labels = {
|
||||
text: t('questions.template.answerType.text'),
|
||||
label: t('questions.template.answerType.tags'),
|
||||
custom_format: t('questions.template.answerType.customFormat')
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
// 按数据源类型分组模板
|
||||
const imageTemplates = templates.filter(t => t.sourceType === 'image');
|
||||
const textTemplates = templates.filter(t => t.sourceType === 'text');
|
||||
|
||||
const currentTemplates = currentTab === 0 ? imageTemplates : textTemplates;
|
||||
|
||||
const renderTemplateList = templateList => {
|
||||
if (templateList.length === 0) {
|
||||
return <Alert severity="info">{t('questions.template.noTemplates')}</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{templateList.map(template => (
|
||||
<ListItem key={template.id} divider>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography>{template.question}</Typography>
|
||||
<Chip
|
||||
label={getAnswerTypeLabel(template.answerType)}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
{template.usageCount > 0 && (
|
||||
<Chip
|
||||
label={`${t('questions.template.used')} ${template.usageCount}`}
|
||||
size="small"
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={template.description}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" onClick={() => handleEdit(template)} sx={{ mr: 1 }}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton edge="end" onClick={() => handleDelete(template.id)} disabled={template.usageCount > 0}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6">{t('questions.template.management')}</Typography>
|
||||
<Button variant="contained" startIcon={<AddIcon />} onClick={handleCreate} size="small">
|
||||
{t('questions.template.create')}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={currentTab} onChange={(e, newValue) => setCurrentTab(newValue)}>
|
||||
<Tab label={t('questions.template.sourceType.image')} />
|
||||
<Tab label={t('questions.template.sourceType.text')} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
{renderTemplateList(currentTemplates)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.close')}</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<TemplateFormDialog
|
||||
open={formOpen}
|
||||
onClose={() => setFormOpen(false)}
|
||||
onSubmit={handleFormSubmit}
|
||||
template={editingTemplate}
|
||||
sourceType={currentTab === 0 ? 'image' : 'text'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user