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'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function useQuestionDelete(projectId, onDeleteSuccess) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 确认对话框状态
|
||||
const [confirmDialog, setConfirmDialog] = useState({
|
||||
open: false,
|
||||
title: '',
|
||||
content: '',
|
||||
confirmAction: null
|
||||
});
|
||||
|
||||
// 执行单个问题删除
|
||||
const executeDeleteQuestion = async (questionId, selectedQuestions, setSelectedQuestions) => {
|
||||
toast.promise(axios.delete(`/api/projects/${projectId}/questions/${questionId}`), {
|
||||
loading: '数据删除中',
|
||||
success: data => {
|
||||
// 更新选中状态
|
||||
setSelectedQuestions(prev => (prev.includes(questionId) ? prev.filter(id => id !== questionId) : prev));
|
||||
// 调用成功回调
|
||||
if (onDeleteSuccess) {
|
||||
onDeleteSuccess();
|
||||
}
|
||||
return t('common.deleteSuccess');
|
||||
},
|
||||
error: error => {
|
||||
return error.response?.data?.message || '删除失败';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 确认删除单个问题
|
||||
const confirmDeleteQuestion = (questionId, selectedQuestions, setSelectedQuestions) => {
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: t('common.confirmDelete'),
|
||||
content: t('common.confirmDeleteQuestion'),
|
||||
confirmAction: () => executeDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions)
|
||||
});
|
||||
};
|
||||
|
||||
// 处理删除单个问题的入口函数
|
||||
const handleDeleteQuestion = (questionId, selectedQuestions, setSelectedQuestions) => {
|
||||
confirmDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions);
|
||||
};
|
||||
|
||||
// 执行批量删除问题
|
||||
const executeBatchDeleteQuestions = async (selectedQuestions, setSelectedQuestions) => {
|
||||
toast.promise(
|
||||
axios.delete(`/api/projects/${projectId}/questions/batch-delete`, {
|
||||
data: { questionIds: selectedQuestions }
|
||||
}),
|
||||
{
|
||||
loading: `正在删除 ${selectedQuestions.length} 个问题...`,
|
||||
success: data => {
|
||||
// 调用成功回调
|
||||
if (onDeleteSuccess) {
|
||||
onDeleteSuccess();
|
||||
}
|
||||
// 清空选中状态
|
||||
setSelectedQuestions([]);
|
||||
return `成功删除 ${selectedQuestions.length} 个问题`;
|
||||
},
|
||||
error: error => {
|
||||
return error.response?.data?.message || '批量删除问题失败';
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 确认批量删除问题
|
||||
const confirmBatchDeleteQuestions = (selectedQuestions, setSelectedQuestions) => {
|
||||
if (selectedQuestions.length === 0) {
|
||||
toast.warning('请先选择问题');
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: '确认批量删除问题',
|
||||
content: `您确定要删除选中的 ${selectedQuestions.length} 个问题吗?此操作不可恢复。`,
|
||||
confirmAction: () => executeBatchDeleteQuestions(selectedQuestions, setSelectedQuestions)
|
||||
});
|
||||
};
|
||||
|
||||
// 处理批量删除问题的入口函数
|
||||
const handleBatchDeleteQuestions = (selectedQuestions, setSelectedQuestions) => {
|
||||
confirmBatchDeleteQuestions(selectedQuestions, setSelectedQuestions);
|
||||
};
|
||||
|
||||
// 关闭确认对话框
|
||||
const closeConfirmDialog = () => {
|
||||
setConfirmDialog({
|
||||
open: false,
|
||||
title: '',
|
||||
content: '',
|
||||
confirmAction: null
|
||||
});
|
||||
};
|
||||
|
||||
// 确认对话框的确认操作
|
||||
const handleConfirmAction = () => {
|
||||
closeConfirmDialog();
|
||||
if (confirmDialog.confirmAction) {
|
||||
confirmDialog.confirmAction();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
confirmDialog,
|
||||
|
||||
// 方法
|
||||
handleDeleteQuestion,
|
||||
handleBatchDeleteQuestions,
|
||||
closeConfirmDialog,
|
||||
handleConfirmAction
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import request from '@/lib/util/request';
|
||||
|
||||
export function useQuestionEdit(projectId, onSuccess) {
|
||||
const { t } = useTranslation();
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editMode, setEditMode] = useState('create');
|
||||
const [editingQuestion, setEditingQuestion] = useState(null);
|
||||
|
||||
const handleOpenCreateDialog = () => {
|
||||
setEditMode('create');
|
||||
setEditingQuestion(null);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleOpenEditDialog = question => {
|
||||
setEditMode('edit');
|
||||
setEditingQuestion(question);
|
||||
setEditDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setEditDialogOpen(false);
|
||||
setEditingQuestion(null);
|
||||
};
|
||||
|
||||
const handleSubmitQuestion = async formData => {
|
||||
try {
|
||||
const response = await request(`/api/projects/${projectId}/questions`, {
|
||||
method: editMode === 'create' ? 'POST' : 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(
|
||||
editMode === 'create'
|
||||
? {
|
||||
question: formData.question,
|
||||
chunkId: formData.chunkId,
|
||||
label: formData.label,
|
||||
imageId: formData.imageId,
|
||||
imageName: formData.imageName
|
||||
}
|
||||
: {
|
||||
id: formData.id,
|
||||
question: formData.question,
|
||||
chunkId: formData.chunkId,
|
||||
label: formData.label,
|
||||
imageId: formData.imageId,
|
||||
imageName: formData.imageName
|
||||
}
|
||||
)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || t('questions.operationFailed'));
|
||||
}
|
||||
|
||||
// 获取更新后的问题数据
|
||||
const updatedQuestion = await response.json();
|
||||
|
||||
// 直接更新问题列表中的数据,而不是重新获取整个列表
|
||||
if (onSuccess) {
|
||||
onSuccess(updatedQuestion);
|
||||
}
|
||||
handleCloseDialog();
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
editDialogOpen,
|
||||
editMode,
|
||||
editingQuestion,
|
||||
handleOpenCreateDialog,
|
||||
handleOpenEditDialog,
|
||||
handleCloseDialog,
|
||||
handleSubmitQuestion
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
|
||||
const useQuestionExport = projectId => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 导出问题集
|
||||
const exportQuestions = async exportOptions => {
|
||||
try {
|
||||
const apiUrl = `/api/projects/${projectId}/questions/export`;
|
||||
const requestBody = {
|
||||
format: exportOptions.format || 'json'
|
||||
};
|
||||
|
||||
// 如果有选中的问题 ID,传递 ID 列表
|
||||
if (exportOptions.selectedIds && exportOptions.selectedIds.length > 0) {
|
||||
requestBody.selectedIds = exportOptions.selectedIds;
|
||||
}
|
||||
|
||||
// 如果有筛选条件,传递筛选参数
|
||||
if (exportOptions.filters) {
|
||||
requestBody.filters = exportOptions.filters;
|
||||
}
|
||||
|
||||
const response = await axios.post(apiUrl, requestBody);
|
||||
const questions = response.data;
|
||||
|
||||
// 处理和下载数据
|
||||
await processAndDownloadData(questions, exportOptions);
|
||||
|
||||
toast.success(t('questions.exportSuccess'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
toast.error(error.message || t('questions.exportFailed'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理和下载数据的通用函数
|
||||
const processAndDownloadData = async (data, exportOptions) => {
|
||||
const format = exportOptions.format || 'json';
|
||||
let content;
|
||||
let filename;
|
||||
let mimeType;
|
||||
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
|
||||
switch (format) {
|
||||
case 'json':
|
||||
content = JSON.stringify(data, null, 2);
|
||||
filename = `questions-${projectId}-${timestamp}.json`;
|
||||
mimeType = 'application/json';
|
||||
break;
|
||||
|
||||
case 'jsonl':
|
||||
content = data.map(item => JSON.stringify(item)).join('\n');
|
||||
filename = `questions-${projectId}-${timestamp}.jsonl`;
|
||||
mimeType = 'application/jsonl';
|
||||
break;
|
||||
|
||||
case 'txt':
|
||||
content = data.map(item => item.question).join('\n\n');
|
||||
filename = `questions-${projectId}-${timestamp}.txt`;
|
||||
mimeType = 'text/plain';
|
||||
break;
|
||||
|
||||
case 'csv':
|
||||
// CSV 格式
|
||||
const headers = Object.keys(data[0] || {});
|
||||
const csvRows = [headers.join(',')];
|
||||
data.forEach(item => {
|
||||
const values = headers.map(header => {
|
||||
const value = item[header] || '';
|
||||
// 处理包含逗号或引号的值
|
||||
if (typeof value === 'string' && (value.includes(',') || value.includes('"') || value.includes('\n'))) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
csvRows.push(values.join(','));
|
||||
});
|
||||
content = csvRows.join('\n');
|
||||
filename = `questions-${projectId}-${timestamp}.csv`;
|
||||
mimeType = 'text/csv';
|
||||
break;
|
||||
|
||||
default:
|
||||
content = JSON.stringify(data, null, 2);
|
||||
filename = `questions-${projectId}-${timestamp}.json`;
|
||||
mimeType = 'application/json';
|
||||
}
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return {
|
||||
exportQuestions
|
||||
};
|
||||
};
|
||||
|
||||
export default useQuestionExport;
|
||||
@@ -0,0 +1,291 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import axios from 'axios';
|
||||
import i18n from '@/lib/i18n';
|
||||
import request from '@/lib/util/request';
|
||||
import { processInParallel } from '@/lib/util/processInParallel';
|
||||
|
||||
export function useQuestionGeneration(projectId, model, taskSettings, getQuestionList) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 处理状态
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
// 进度状态
|
||||
const [progress, setProgress] = useState({
|
||||
total: 0, // 总共选择的问题数量
|
||||
completed: 0, // 已处理完成的数量
|
||||
percentage: 0, // 进度百分比
|
||||
datasetCount: 0 // 已生成的数据集数量
|
||||
});
|
||||
|
||||
// 批量生成答案
|
||||
const handleBatchGenerateAnswers = async selectedQuestions => {
|
||||
if (selectedQuestions.length === 0) {
|
||||
toast.warning(t('questions.noQuestionsSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
toast.warning(t('models.configNotFound'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setProgress({
|
||||
total: selectedQuestions.length,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
datasetCount: 0
|
||||
});
|
||||
|
||||
// 然后设置处理状态为真,确保进度条显示
|
||||
setProcessing(true);
|
||||
|
||||
toast.info(t('questions.batchGenerateStart', { count: selectedQuestions.length }));
|
||||
|
||||
// 单个问题处理函数
|
||||
const processQuestion = async questionId => {
|
||||
try {
|
||||
console.log('开始生成数据集:', { questionId });
|
||||
const language = i18n.language === 'zh-CN' ? '中文' : 'en';
|
||||
// 调用API生成数据集
|
||||
const response = await request(`/api/projects/${projectId}/datasets`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
questionId,
|
||||
model,
|
||||
language
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.error(t('datasets.generateError'), errorData.error || t('datasets.generateFailed'));
|
||||
|
||||
// 更新进度状态(即使失败也计入已处理)
|
||||
setProgress(prev => {
|
||||
const completed = prev.completed + 1;
|
||||
const percentage = Math.round((completed / prev.total) * 100);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
completed,
|
||||
percentage
|
||||
};
|
||||
});
|
||||
|
||||
return { success: false, questionId, error: errorData.error || t('datasets.generateFailed') };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 更新进度状态
|
||||
setProgress(prev => {
|
||||
const completed = prev.completed + 1;
|
||||
const percentage = Math.round((completed / prev.total) * 100);
|
||||
const datasetCount = prev.datasetCount + 1;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
completed,
|
||||
percentage,
|
||||
datasetCount
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`数据集生成成功: ${questionId}`);
|
||||
return { success: true, questionId, data: data.dataset };
|
||||
} catch (error) {
|
||||
console.error('生成数据集失败:', error);
|
||||
|
||||
// 更新进度状态(即使失败也计入已处理)
|
||||
setProgress(prev => {
|
||||
const completed = prev.completed + 1;
|
||||
const percentage = Math.round((completed / prev.total) * 100);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
completed,
|
||||
percentage
|
||||
};
|
||||
});
|
||||
|
||||
return { success: false, questionId, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// 并行处理所有问题,最多同时处理2个
|
||||
const results = await processInParallel(selectedQuestions, processQuestion, taskSettings.concurrencyLimit);
|
||||
|
||||
// 刷新数据
|
||||
getQuestionList();
|
||||
|
||||
// 处理完成后设置结果消息
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.filter(r => !r.success).length;
|
||||
|
||||
if (failCount > 0) {
|
||||
toast.warning(
|
||||
t('datasets.partialSuccess', {
|
||||
successCount,
|
||||
total: selectedQuestions.length,
|
||||
failCount
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.success(t('common.success', { successCount }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成数据集出错:', error);
|
||||
toast.error(error.message || '生成数据集失败');
|
||||
} finally {
|
||||
// 延迟关闭处理状态,确保用户可以看到完成的进度
|
||||
setTimeout(() => {
|
||||
setProcessing(false);
|
||||
// 再次延迟重置进度状态
|
||||
setTimeout(() => {
|
||||
setProgress({
|
||||
total: 0,
|
||||
completed: 0,
|
||||
percentage: 0,
|
||||
datasetCount: 0
|
||||
});
|
||||
}, 500);
|
||||
}, 2000); // 延迟关闭处理状态,让用户看到完成的进度
|
||||
}
|
||||
};
|
||||
|
||||
// 自动生成数据集
|
||||
const handleAutoGenerateDatasets = async () => {
|
||||
try {
|
||||
if (!model) {
|
||||
toast.error(t('questions.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'answer-generation',
|
||||
modelInfo: model,
|
||||
language: i18n.language
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(t('tasks.createSuccess', { defaultValue: '后台任务已创建,系统将自动处理未生成答案的问题' }));
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建后台任务失败' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动生成图像问答数据集
|
||||
const handleAutoGenerateImageDatasets = async () => {
|
||||
try {
|
||||
if (!model) {
|
||||
toast.error(t('questions.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.type !== 'vision') {
|
||||
toast.error(t('images.visionModelRequired', { defaultValue: '请选择支持视觉的模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'image-dataset-generation',
|
||||
modelInfo: model,
|
||||
language: i18n.language
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(t('tasks.createSuccess', { defaultValue: '后台任务已创建,系统将自动处理未生成答案的图片问题' }));
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建后台任务失败' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建图片数据集任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 自动生成多轮对话数据集
|
||||
const handleAutoGenerateMultiTurnDatasets = async () => {
|
||||
try {
|
||||
if (!model) {
|
||||
toast.error(t('questions.selectModelFirst', { defaultValue: '请先选择模型' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 首先检查项目是否配置了多轮对话设置
|
||||
const configResponse = await axios.get(`/api/projects/${projectId}/tasks`);
|
||||
if (configResponse.status !== 200) {
|
||||
throw new Error('获取项目配置失败');
|
||||
}
|
||||
|
||||
const config = configResponse.data;
|
||||
const multiTurnConfig = {
|
||||
systemPrompt: config.multiTurnSystemPrompt,
|
||||
scenario: config.multiTurnScenario,
|
||||
rounds: config.multiTurnRounds,
|
||||
roleA: config.multiTurnRoleA,
|
||||
roleB: config.multiTurnRoleB
|
||||
};
|
||||
|
||||
// 检查是否已配置必要的多轮对话设置
|
||||
if (
|
||||
!multiTurnConfig.scenario ||
|
||||
!multiTurnConfig.roleA ||
|
||||
!multiTurnConfig.roleB ||
|
||||
!multiTurnConfig.rounds ||
|
||||
multiTurnConfig.rounds < 1
|
||||
) {
|
||||
toast.error(t('questions.multiTurnNotConfigured', '请先在项目设置中配置多轮对话相关参数'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用创建任务接口
|
||||
const response = await axios.post(`/api/projects/${projectId}/tasks`, {
|
||||
taskType: 'multi-turn-generation',
|
||||
modelInfo: model,
|
||||
language: i18n.language,
|
||||
config: JSON.stringify(multiTurnConfig)
|
||||
});
|
||||
|
||||
if (response.data?.code === 0) {
|
||||
toast.success(
|
||||
t('tasks.multiTurnCreateSuccess', {
|
||||
defaultValue: '多轮对话生成任务已创建,系统将自动处理未生成多轮对话的问题'
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建后台任务失败' }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建多轮对话任务失败:', error);
|
||||
toast.error(t('tasks.createFailed', { defaultValue: '创建任务失败' }) + ': ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
processing,
|
||||
progress,
|
||||
|
||||
// 方法
|
||||
handleBatchGenerateAnswers,
|
||||
handleAutoGenerateDatasets,
|
||||
handleAutoGenerateMultiTurnDatasets,
|
||||
handleAutoGenerateImageDatasets
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 问题模板管理 Hook
|
||||
* @param {string} projectId - 项目ID
|
||||
* @param {string} sourceType - 数据源类型: 'image' | 'text' | null (null表示获取所有)
|
||||
*/
|
||||
export function useQuestionTemplates(projectId, sourceType = null) {
|
||||
const { t } = useTranslation();
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const params = sourceType ? `?sourceType=${sourceType}` : '';
|
||||
const response = await axios.get(`/api/projects/${projectId}/questions/templates${params}`);
|
||||
if (response.data.success) {
|
||||
setTemplates(response.data.templates);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch templates:', error);
|
||||
toast.error(t('questions.fetchTemplatesFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 创建模板
|
||||
const createTemplate = async data => {
|
||||
try {
|
||||
const response = await axios.post(`/api/projects/${projectId}/questions/templates`, data);
|
||||
if (response.data.success) {
|
||||
const { template, generation } = response.data;
|
||||
|
||||
// 显示模板创建成功消息
|
||||
toast.success(t('questions.createTemplateSuccess'));
|
||||
|
||||
// 如果有自动生成结果,显示相应消息
|
||||
if (generation) {
|
||||
if (generation.success) {
|
||||
if (generation.successCount > 0) {
|
||||
toast.success(
|
||||
t('questions.template.autoGenerateSuccess', {
|
||||
count: generation.successCount
|
||||
})
|
||||
);
|
||||
}
|
||||
if (generation.failCount > 0) {
|
||||
toast.warning(
|
||||
t('questions.template.autoGeneratePartialFail', {
|
||||
success: generation.successCount,
|
||||
fail: generation.failCount
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
toast.error(generation.message || t('questions.template.autoGenerateFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
fetchTemplates();
|
||||
return template;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create template:', error);
|
||||
toast.error(t('questions.createTemplateFailed'));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 更新模板
|
||||
const updateTemplate = async (templateId, data) => {
|
||||
try {
|
||||
const response = await axios.put(`/api/projects/${projectId}/questions/templates/${templateId}`, data);
|
||||
if (response.data.success) {
|
||||
toast.success(t('questions.updateTemplateSuccess'));
|
||||
fetchTemplates();
|
||||
return response.data.template;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update template:', error);
|
||||
toast.error(t('questions.updateTemplateFailed'));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 删除模板
|
||||
const deleteTemplate = async templateId => {
|
||||
try {
|
||||
const response = await axios.delete(`/api/projects/${projectId}/questions/templates/${templateId}`);
|
||||
if (response.data.success) {
|
||||
toast.success(t('questions.deleteTemplateSuccess'));
|
||||
fetchTemplates();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete template:', error);
|
||||
toast.error(t('questions.deleteTemplateFailed'));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
fetchTemplates();
|
||||
}
|
||||
}, [projectId, sourceType]);
|
||||
|
||||
return {
|
||||
templates,
|
||||
loading,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
refetch: fetchTemplates
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useDebounce } from '@/hooks/useDebounce';
|
||||
import axios from 'axios';
|
||||
|
||||
export function useQuestionsFilter(projectId) {
|
||||
// 过滤和搜索状态
|
||||
const [answerFilter, setAnswerFilter] = useState('all'); // 'all', 'answered', 'unanswered'
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [searchMatchMode, setSearchMatchMode] = useState('match'); // 'match', 'notMatch'
|
||||
const [chunkNameFilter, setChunkNameFilter] = useState('');
|
||||
const [sourceTypeFilter, setSourceTypeFilter] = useState('all'); // 'all', 'text', 'image'
|
||||
const debouncedSearchTerm = useDebounce(searchTerm);
|
||||
const debouncedChunkNameFilter = useDebounce(chunkNameFilter);
|
||||
|
||||
// 选择状态
|
||||
const [selectedQuestions, setSelectedQuestions] = useState([]);
|
||||
|
||||
// 处理问题选择
|
||||
const handleSelectQuestion = (questionKey, newSelected) => {
|
||||
if (newSelected) {
|
||||
// 处理批量选择的情况
|
||||
setSelectedQuestions(newSelected);
|
||||
} else {
|
||||
// 处理单个问题选择的情况
|
||||
setSelectedQuestions(prev => {
|
||||
if (prev.includes(questionKey)) {
|
||||
return prev.filter(id => id !== questionKey);
|
||||
} else {
|
||||
return [...prev, questionKey];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 全选/取消全选
|
||||
const handleSelectAll = async () => {
|
||||
if (selectedQuestions.length > 0) {
|
||||
setSelectedQuestions([]);
|
||||
} else {
|
||||
const response = await axios.get(
|
||||
`/api/projects/${projectId}/questions?status=${answerFilter}&input=${searchTerm}&searchMatchMode=${searchMatchMode}&chunkName=${encodeURIComponent(chunkNameFilter)}&sourceType=${sourceTypeFilter}&selectedAll=1`
|
||||
);
|
||||
setSelectedQuestions(response.data.map(dataset => dataset.id));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理搜索输入变化
|
||||
const handleSearchChange = event => {
|
||||
setSearchTerm(event.target.value);
|
||||
};
|
||||
|
||||
// 处理过滤器变化
|
||||
const handleFilterChange = event => {
|
||||
setAnswerFilter(event.target.value);
|
||||
};
|
||||
|
||||
// 处理文本块名称筛选变化
|
||||
const handleChunkNameFilterChange = event => {
|
||||
setChunkNameFilter(event.target.value);
|
||||
};
|
||||
|
||||
// 处理数据源类型筛选变化
|
||||
const handleSourceTypeFilterChange = event => {
|
||||
setSourceTypeFilter(event.target.value);
|
||||
};
|
||||
|
||||
// 处理搜索匹配模式变化
|
||||
const handleSearchMatchModeChange = event => {
|
||||
setSearchMatchMode(event.target.value);
|
||||
};
|
||||
|
||||
// 清空选择
|
||||
const clearSelection = () => {
|
||||
setSelectedQuestions([]);
|
||||
};
|
||||
|
||||
// 重置所有过滤条件
|
||||
const resetFilters = () => {
|
||||
setSearchTerm('');
|
||||
setSearchMatchMode('match');
|
||||
setAnswerFilter('all');
|
||||
setChunkNameFilter('');
|
||||
setSourceTypeFilter('all');
|
||||
setSelectedQuestions([]);
|
||||
};
|
||||
|
||||
return {
|
||||
// 状态
|
||||
answerFilter,
|
||||
searchTerm,
|
||||
debouncedSearchTerm,
|
||||
searchMatchMode,
|
||||
chunkNameFilter,
|
||||
debouncedChunkNameFilter,
|
||||
sourceTypeFilter,
|
||||
selectedQuestions,
|
||||
|
||||
// 方法
|
||||
setAnswerFilter,
|
||||
setSearchTerm,
|
||||
setSearchMatchMode,
|
||||
setChunkNameFilter,
|
||||
setSourceTypeFilter,
|
||||
setSelectedQuestions,
|
||||
handleSelectQuestion,
|
||||
handleSelectAll,
|
||||
handleSearchChange,
|
||||
handleFilterChange,
|
||||
handleChunkNameFilterChange,
|
||||
handleSourceTypeFilterChange,
|
||||
handleSearchMatchModeChange,
|
||||
clearSelection,
|
||||
resetFilters
|
||||
};
|
||||
}
|
||||
416
easy-dataset-main/app/projects/[projectId]/questions/page.js
Normal file
416
easy-dataset-main/app/projects/[projectId]/questions/page.js
Normal file
@@ -0,0 +1,416 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Container, Typography, Box, Paper, Tabs, Tab, CircularProgress, Divider, LinearProgress } from '@mui/material';
|
||||
|
||||
import QuestionListView from '@/components/questions/QuestionListView';
|
||||
import QuestionTreeView from '@/components/questions/QuestionTreeView';
|
||||
import TabPanel from '@/components/text-split/components/TabPanel';
|
||||
import useTaskSettings from '@/hooks/useTaskSettings';
|
||||
import QuestionEditDialog from './components/QuestionEditDialog';
|
||||
import QuestionsPageHeader from './components/QuestionsPageHeader';
|
||||
import ConfirmDialog from './components/ConfirmDialog';
|
||||
import TemplateListView from './components/TemplateListView';
|
||||
import TemplateFormDialog from './components/template/TemplateFormDialog';
|
||||
import ExportQuestionsDialog from './components/ExportQuestionsDialog';
|
||||
import { useQuestionTemplates } from './hooks/useQuestionTemplates';
|
||||
import { useQuestionEdit } from './hooks/useQuestionEdit';
|
||||
import { useQuestionDelete } from './hooks/useQuestionDelete';
|
||||
import { useQuestionsFilter } from './hooks/useQuestionsFilter';
|
||||
import QuestionsFilter from './components/QuestionsFilter';
|
||||
import { useQuestionGeneration } from './hooks/useQuestionGeneration';
|
||||
import useQuestionExport from './hooks/useQuestionExport';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
import { useAtomValue } from 'jotai/index';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
|
||||
export default function QuestionsPage({ params }) {
|
||||
const { t } = useTranslation();
|
||||
const { projectId } = params;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [questions, setQuestions] = useState({});
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [tags, setTags] = useState([]);
|
||||
const model = useAtomValue(selectedModelInfoAtom);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
// 模板管理
|
||||
const {
|
||||
templates,
|
||||
loading: templatesLoading,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate
|
||||
} = useQuestionTemplates(projectId, null); // null 表示获取所有类型的模板
|
||||
|
||||
const [templateDialogOpen, setTemplateDialogOpen] = useState(false);
|
||||
const [editingTemplate, setEditingTemplate] = useState(null);
|
||||
const [exportDialogOpen, setExportDialogOpen] = useState(false);
|
||||
|
||||
// 使用新的过滤和搜索 Hook
|
||||
const {
|
||||
answerFilter,
|
||||
searchTerm,
|
||||
debouncedSearchTerm,
|
||||
searchMatchMode,
|
||||
chunkNameFilter,
|
||||
debouncedChunkNameFilter,
|
||||
sourceTypeFilter,
|
||||
selectedQuestions,
|
||||
setSelectedQuestions,
|
||||
handleSelectQuestion,
|
||||
handleSelectAll,
|
||||
handleSearchChange,
|
||||
handleFilterChange,
|
||||
handleChunkNameFilterChange,
|
||||
handleSourceTypeFilterChange,
|
||||
handleSearchMatchModeChange
|
||||
} = useQuestionsFilter(projectId);
|
||||
|
||||
const getQuestionList = async () => {
|
||||
try {
|
||||
// 获取问题列表
|
||||
const questionsResponse = await axios.get(
|
||||
`/api/projects/${projectId}/questions?page=${currentPage}&size=10&status=${answerFilter}&input=${searchTerm}&searchMatchMode=${searchMatchMode}&chunkName=${encodeURIComponent(debouncedChunkNameFilter)}&sourceType=${sourceTypeFilter}`
|
||||
);
|
||||
if (questionsResponse.status !== 200) {
|
||||
throw new Error(t('common.fetchError'));
|
||||
}
|
||||
setQuestions(questionsResponse.data || {});
|
||||
|
||||
// 获取标签树
|
||||
const tagsResponse = await axios.get(`/api/projects/${projectId}/tags`);
|
||||
if (tagsResponse.status !== 200) {
|
||||
throw new Error(t('common.fetchError'));
|
||||
}
|
||||
setTags(tagsResponse.data.tags || []);
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error(t('common.fetchError'), error);
|
||||
toast.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 当筛选条件改变时,重置页码到第1页
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [answerFilter, debouncedSearchTerm, debouncedChunkNameFilter, sourceTypeFilter, searchMatchMode]);
|
||||
|
||||
useEffect(() => {
|
||||
getQuestionList();
|
||||
}, [currentPage, answerFilter, debouncedSearchTerm, debouncedChunkNameFilter, sourceTypeFilter, searchMatchMode]);
|
||||
|
||||
const { taskSettings } = useTaskSettings(projectId);
|
||||
|
||||
// 使用新的问题生成 Hook
|
||||
const {
|
||||
processing,
|
||||
progress,
|
||||
handleBatchGenerateAnswers,
|
||||
handleAutoGenerateDatasets,
|
||||
handleAutoGenerateMultiTurnDatasets,
|
||||
handleAutoGenerateImageDatasets
|
||||
} = useQuestionGeneration(projectId, model, taskSettings, getQuestionList);
|
||||
|
||||
const {
|
||||
editDialogOpen,
|
||||
editMode,
|
||||
editingQuestion,
|
||||
handleOpenCreateDialog,
|
||||
handleOpenEditDialog,
|
||||
handleCloseDialog,
|
||||
handleSubmitQuestion
|
||||
} = useQuestionEdit(projectId, updatedQuestion => {
|
||||
getQuestionList();
|
||||
toast.success(t('questions.operationSuccess'));
|
||||
});
|
||||
|
||||
const { confirmDialog, handleDeleteQuestion, handleBatchDeleteQuestions, closeConfirmDialog, handleConfirmAction } =
|
||||
useQuestionDelete(projectId, () => {
|
||||
getQuestionList();
|
||||
});
|
||||
|
||||
const { exportQuestions } = useQuestionExport(projectId);
|
||||
|
||||
// 获取所有数据
|
||||
useEffect(() => {
|
||||
getQuestionList();
|
||||
}, [projectId]);
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabChange = (event, newValue) => {
|
||||
setActiveTab(newValue);
|
||||
};
|
||||
|
||||
// 模板管理函数
|
||||
const handleOpenCreateTemplateDialog = () => {
|
||||
setEditingTemplate(null);
|
||||
setTemplateDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditTemplate = template => {
|
||||
setEditingTemplate(template);
|
||||
setTemplateDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseTemplateDialog = () => {
|
||||
setTemplateDialogOpen(false);
|
||||
setEditingTemplate(null);
|
||||
};
|
||||
|
||||
const handleSubmitTemplate = async data => {
|
||||
try {
|
||||
if (editingTemplate) {
|
||||
await updateTemplate(editingTemplate.id, data);
|
||||
} else {
|
||||
await createTemplate(data);
|
||||
}
|
||||
getQuestionList();
|
||||
handleCloseTemplateDialog();
|
||||
} catch (error) {
|
||||
console.error('Failed to save template:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = async templateId => {
|
||||
const confirmed = window.confirm(t('questions.template.deleteConfirm'));
|
||||
if (confirmed) {
|
||||
try {
|
||||
await deleteTemplate(templateId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete template:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenExportDialog = () => {
|
||||
setExportDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseExportDialog = () => {
|
||||
setExportDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleExportQuestions = async exportOptions => {
|
||||
const options = {
|
||||
...exportOptions,
|
||||
selectedIds: selectedQuestions,
|
||||
filters: {
|
||||
searchTerm: debouncedSearchTerm,
|
||||
chunkName: debouncedChunkNameFilter,
|
||||
sourceType: sourceTypeFilter
|
||||
}
|
||||
};
|
||||
await exportQuestions(options);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '70vh' }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ mt: 4, mb: 8 }}>
|
||||
{/* 处理中的进度显示 - 全局蒙版样式 */}
|
||||
{processing && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={6}
|
||||
sx={{
|
||||
width: '90%',
|
||||
maxWidth: 500,
|
||||
p: 3,
|
||||
borderRadius: 2,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: 'primary.main', fontWeight: 'bold' }}>
|
||||
{t('datasets.generatingDataset')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
||||
<Typography variant="body1" sx={{ mr: 1 }}>
|
||||
{progress.percentage}%
|
||||
</Typography>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress.percentage}
|
||||
sx={{ height: 8, borderRadius: 4 }}
|
||||
color="primary"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mt: 2 }}>
|
||||
<Typography variant="body2">
|
||||
{t('questions.generatingProgress', {
|
||||
completed: progress.completed,
|
||||
total: progress.total
|
||||
})}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="success.main" sx={{ fontWeight: 'medium' }}>
|
||||
{t('questions.generatedCount', { count: progress.datasetCount })}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<CircularProgress size={60} thickness={4} sx={{ mb: 2 }} />
|
||||
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('questions.pleaseWait')}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<QuestionsPageHeader
|
||||
questionsTotal={questions.total}
|
||||
selectedQuestionsCount={selectedQuestions.length}
|
||||
onBatchDeleteQuestions={() => handleBatchDeleteQuestions(selectedQuestions, setSelectedQuestions)}
|
||||
onOpenCreateDialog={handleOpenCreateDialog}
|
||||
onOpenCreateTemplateDialog={handleOpenCreateTemplateDialog}
|
||||
onBatchGenerateAnswers={() => handleBatchGenerateAnswers(selectedQuestions)}
|
||||
onAutoGenerateDatasets={handleAutoGenerateDatasets}
|
||||
onAutoGenerateMultiTurnDatasets={handleAutoGenerateMultiTurnDatasets}
|
||||
onAutoGenerateImageDatasets={handleAutoGenerateImageDatasets}
|
||||
onExportQuestions={handleOpenExportDialog}
|
||||
/>
|
||||
|
||||
<Paper sx={{ mb: 4 }}>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={handleTabChange}
|
||||
variant="fullWidth"
|
||||
indicatorColor="primary"
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: 'divider'
|
||||
}}
|
||||
>
|
||||
<Tab label={t('questions.listView')} />
|
||||
<Tab label={t('questions.template.management')} />
|
||||
<Tab label={t('questions.treeView')} />
|
||||
</Tabs>
|
||||
|
||||
<QuestionsFilter
|
||||
selectedQuestionsCount={selectedQuestions.length}
|
||||
totalQuestions={questions?.total || 0}
|
||||
isAllSelected={selectedQuestions.length > 0 && selectedQuestions.length === questions?.total}
|
||||
isIndeterminate={selectedQuestions.length > 0 && selectedQuestions.length < questions?.total}
|
||||
onSelectAll={handleSelectAll}
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={handleSearchChange}
|
||||
searchMatchMode={searchMatchMode}
|
||||
onSearchMatchModeChange={handleSearchMatchModeChange}
|
||||
answerFilter={answerFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
chunkNameFilter={chunkNameFilter}
|
||||
onChunkNameFilterChange={handleChunkNameFilterChange}
|
||||
sourceTypeFilter={sourceTypeFilter}
|
||||
onSourceTypeFilterChange={handleSourceTypeFilterChange}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<TabPanel value={activeTab} index={0}>
|
||||
<QuestionListView
|
||||
questions={questions.data}
|
||||
currentPage={currentPage}
|
||||
totalQuestions={Math.ceil(questions.total / pageSize)}
|
||||
handlePageChange={(_, newPage) => setCurrentPage(newPage)}
|
||||
selectedQuestions={selectedQuestions}
|
||||
onSelectQuestion={handleSelectQuestion}
|
||||
onDeleteQuestion={questionId => handleDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions)}
|
||||
onEditQuestion={handleOpenEditDialog}
|
||||
refreshQuestions={getQuestionList}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={1}>
|
||||
<TemplateListView
|
||||
templates={templates}
|
||||
onEditTemplate={handleEditTemplate}
|
||||
onDeleteTemplate={handleDeleteTemplate}
|
||||
loading={templatesLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={activeTab} index={2}>
|
||||
<QuestionTreeView
|
||||
questions={questions.data}
|
||||
tags={tags}
|
||||
selectedQuestions={selectedQuestions}
|
||||
onSelectQuestion={handleSelectQuestion}
|
||||
onDeleteQuestion={questionId => handleDeleteQuestion(questionId, selectedQuestions, setSelectedQuestions)}
|
||||
onEditQuestion={handleOpenEditDialog}
|
||||
projectId={projectId}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
|
||||
{/* 确认对话框 */}
|
||||
<ConfirmDialog
|
||||
open={confirmDialog.open}
|
||||
onClose={closeConfirmDialog}
|
||||
onConfirm={handleConfirmAction}
|
||||
title={confirmDialog.title}
|
||||
content={confirmDialog.content}
|
||||
/>
|
||||
|
||||
<QuestionEditDialog
|
||||
open={editDialogOpen}
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleSubmitQuestion}
|
||||
initialData={editingQuestion}
|
||||
tags={tags}
|
||||
mode={editMode}
|
||||
projectId={projectId}
|
||||
/>
|
||||
|
||||
<TemplateFormDialog
|
||||
open={templateDialogOpen}
|
||||
onClose={handleCloseTemplateDialog}
|
||||
onSubmit={handleSubmitTemplate}
|
||||
template={editingTemplate}
|
||||
/>
|
||||
|
||||
<ExportQuestionsDialog
|
||||
open={exportDialogOpen}
|
||||
onClose={handleCloseExportDialog}
|
||||
onExport={handleExportQuestions}
|
||||
selectedCount={selectedQuestions.length}
|
||||
totalCount={questions.total || 0}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user