first-update
This commit is contained in:
325
easy-dataset-main/components/distill/AutoDistillDialog.js
Normal file
325
easy-dataset-main/components/distill/AutoDistillDialog.js
Normal file
@@ -0,0 +1,325 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Alert,
|
||||
Paper,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio
|
||||
} from '@mui/material';
|
||||
|
||||
/**
|
||||
* 全自动蒸馏数据集配置弹框
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调
|
||||
* @param {Function} props.onStart - 开始蒸馏任务的回调
|
||||
* @param {Function} props.onStartBackground - 开始后台蒸馏任务的回调
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Object} props.project - 项目信息
|
||||
* @param {Object} props.stats - 当前统计信息
|
||||
*/
|
||||
export default function AutoDistillDialog({
|
||||
open,
|
||||
onClose,
|
||||
onStart,
|
||||
onStartBackground,
|
||||
projectId,
|
||||
project,
|
||||
stats = {}
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 表单状态
|
||||
const [topic, setTopic] = useState('');
|
||||
const [levels, setLevels] = useState(2);
|
||||
const [tagsPerLevel, setTagsPerLevel] = useState(10);
|
||||
const [questionsPerTag, setQuestionsPerTag] = useState(10);
|
||||
const [datasetType, setDatasetType] = useState('single-turn'); // 'single-turn' | 'multi-turn' | 'both'
|
||||
|
||||
// 计算信息
|
||||
const [estimatedTags, setEstimatedTags] = useState(0); // 所有标签总数(包括根节点和中间节点)
|
||||
const [leafTags, setLeafTags] = useState(0); // 叶子节点数量(即最后一层标签数)
|
||||
const [estimatedQuestions, setEstimatedQuestions] = useState(0);
|
||||
const [newTags, setNewTags] = useState(0);
|
||||
const [newQuestions, setNewQuestions] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 初始化默认主题
|
||||
useEffect(() => {
|
||||
if (project && project.name) {
|
||||
setTopic(project.name);
|
||||
}
|
||||
}, [project]);
|
||||
|
||||
// 计算预估标签和问题数量
|
||||
useEffect(() => {
|
||||
/*
|
||||
* 根据公式:总问题数 = \left( \prod_{i=1}^{n} L_i \right) \times Q
|
||||
* 当每层标签数量相同(L)时:总问题数 = L^n \times Q
|
||||
*/
|
||||
|
||||
const leafTags = Math.pow(tagsPerLevel, levels);
|
||||
|
||||
// 总问题数 = 叶子节点数 * 每个节点的问题数
|
||||
const totalQuestions = leafTags * questionsPerTag;
|
||||
|
||||
let totalTags;
|
||||
if (tagsPerLevel === 1) {
|
||||
// 如果每层只有1个标签,总数就是 levels+1
|
||||
totalTags = levels + 1;
|
||||
} else {
|
||||
// 使用等比数列求和公式
|
||||
totalTags = (1 - Math.pow(tagsPerLevel, levels + 1)) / (1 - tagsPerLevel);
|
||||
}
|
||||
|
||||
setLeafTags(leafTags);
|
||||
setEstimatedTags(leafTags); // 改为只显示叶子节点数量,而非所有节点数量
|
||||
setEstimatedQuestions(totalQuestions);
|
||||
|
||||
// 计算新增标签和问题数量
|
||||
const currentTags = stats.tagsCount || 0;
|
||||
const currentQuestions = stats.questionsCount || 0;
|
||||
|
||||
// 只考虑最后一层的标签数量
|
||||
setNewTags(Math.max(0, leafTags - currentTags));
|
||||
setNewQuestions(Math.max(0, totalQuestions - currentQuestions));
|
||||
|
||||
// 验证是否可以执行任务
|
||||
if (leafTags <= currentTags && totalQuestions <= currentQuestions) {
|
||||
setError(t('distill.autoDistillInsufficientError'));
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
}, [levels, tagsPerLevel, questionsPerTag, stats, t]);
|
||||
|
||||
// 处理开始任务
|
||||
const handleStart = () => {
|
||||
if (error) return;
|
||||
|
||||
onStart({
|
||||
topic,
|
||||
levels,
|
||||
tagsPerLevel,
|
||||
questionsPerTag,
|
||||
estimatedTags,
|
||||
estimatedQuestions,
|
||||
datasetType
|
||||
});
|
||||
};
|
||||
|
||||
// 处理开始后台任务
|
||||
const handleStartBackground = () => {
|
||||
if (error) return;
|
||||
|
||||
onStartBackground({
|
||||
topic,
|
||||
levels,
|
||||
tagsPerLevel,
|
||||
questionsPerTag,
|
||||
estimatedTags,
|
||||
estimatedQuestions,
|
||||
datasetType
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>{t('distill.autoDistillTitle')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ py: 2, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 3 }}>
|
||||
{/* 左侧:输入区域 */}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<TextField
|
||||
label={t('distill.distillTopic')}
|
||||
value={topic}
|
||||
onChange={e => setTopic(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
disabled
|
||||
helperText={t('distill.rootTopicHelperText')}
|
||||
/>
|
||||
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<Typography gutterBottom>{t('distill.tagLevels')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputProps: { min: 1, max: 5 }
|
||||
}}
|
||||
value={levels}
|
||||
onChange={e => {
|
||||
const value = Math.min(5, Math.max(1, Number(e.target.value)));
|
||||
setLevels(value);
|
||||
}}
|
||||
helperText={t('distill.tagLevelsHelper', { max: 5 })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<Typography gutterBottom>{t('distill.tagsPerLevel')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputProps: { min: 1, max: 50 }
|
||||
}}
|
||||
value={tagsPerLevel}
|
||||
onChange={e => {
|
||||
const value = Math.min(50, Math.max(1, Number(e.target.value)));
|
||||
setTagsPerLevel(value);
|
||||
}}
|
||||
helperText={t('distill.tagsPerLevelHelper', { max: 50 })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<Typography gutterBottom>{t('distill.questionsPerTag')}</Typography>
|
||||
<TextField
|
||||
type="number"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
inputProps: { min: 1, max: 50 }
|
||||
}}
|
||||
value={questionsPerTag}
|
||||
onChange={e => {
|
||||
const value = Math.min(50, Math.max(1, Number(e.target.value)));
|
||||
setQuestionsPerTag(value);
|
||||
}}
|
||||
helperText={t('distill.questionsPerTagHelper', { max: 50 })}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend" sx={{ mb: 2, fontWeight: 'medium' }}>
|
||||
{t('distill.datasetType', { defaultValue: '数据集类型' })}
|
||||
</FormLabel>
|
||||
<RadioGroup value={datasetType} onChange={e => setDatasetType(e.target.value)}>
|
||||
<FormControlLabel
|
||||
value="single-turn"
|
||||
control={<Radio />}
|
||||
label={t('distill.singleTurnDataset', { defaultValue: '单轮对话数据集' })}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="multi-turn"
|
||||
control={<Radio />}
|
||||
label={t('distill.multiTurnDataset', { defaultValue: '多轮对话数据集' })}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="both"
|
||||
control={<Radio />}
|
||||
label={t('distill.bothDatasetTypes', { defaultValue: '两种数据集都生成' })}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:预估信息区域 */}
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 3,
|
||||
mt: 1,
|
||||
borderRadius: 2,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" fontWeight="bold" gutterBottom>
|
||||
{t('distill.estimationInfo')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2, mt: 2 }}>
|
||||
<Typography variant="subtitle2">{t('distill.estimatedTags')}:</Typography>
|
||||
<Typography variant="subtitle1" fontWeight="medium">
|
||||
{estimatedTags}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="subtitle2">{t('distill.estimatedQuestions')}:</Typography>
|
||||
<Typography variant="subtitle1" fontWeight="medium">
|
||||
{estimatedQuestions}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="subtitle2">{t('distill.currentTags')}:</Typography>
|
||||
<Typography variant="subtitle1" fontWeight="medium">
|
||||
{stats.tagsCount || 0}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Typography variant="subtitle2">{t('distill.currentQuestions')}:</Typography>
|
||||
<Typography variant="subtitle1" fontWeight="medium">
|
||||
{stats.questionsCount || 0}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ pt: 2, borderTop: '1px dashed', borderColor: 'divider' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="subtitle1" color="primary">
|
||||
{t('distill.newTags')}:
|
||||
</Typography>
|
||||
<Typography variant="h6" fontWeight="bold" color="primary.main">
|
||||
{newTags}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography variant="subtitle1" color="primary">
|
||||
{t('distill.newQuestions')}:
|
||||
</Typography>
|
||||
<Typography variant="h6" fontWeight="bold" color="primary.main">
|
||||
{newQuestions}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||
<Button onClick={handleStartBackground} color="secondary" variant="outlined" disabled={!!error || !topic}>
|
||||
{t('distill.startAutoDistillBackground', { defaultValue: '开始自动蒸馏(后台运行)' })}
|
||||
</Button>
|
||||
<Button onClick={handleStart} color="primary" variant="contained" disabled={!!error || !topic}>
|
||||
{t('distill.startAutoDistill')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
212
easy-dataset-main/components/distill/AutoDistillProgress.js
Normal file
212
easy-dataset-main/components/distill/AutoDistillProgress.js
Normal file
@@ -0,0 +1,212 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Box,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Divider,
|
||||
IconButton,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
/**
|
||||
* 全自动蒸馏进度组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调
|
||||
* @param {Object} props.progress - 进度信息
|
||||
*/
|
||||
export default function AutoDistillProgress({ open, onClose, progress = {} }) {
|
||||
const { t } = useTranslation();
|
||||
const logContainerRef = useRef(null);
|
||||
|
||||
// 自动滚动到底部
|
||||
useEffect(() => {
|
||||
if (logContainerRef.current) {
|
||||
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, [progress.logs]);
|
||||
|
||||
const getStageText = () => {
|
||||
const { stage } = progress;
|
||||
switch (stage) {
|
||||
case 'level1':
|
||||
return t('distill.stageBuildingLevel1');
|
||||
case 'level2':
|
||||
return t('distill.stageBuildingLevel2');
|
||||
case 'level3':
|
||||
return t('distill.stageBuildingLevel3');
|
||||
case 'level4':
|
||||
return t('distill.stageBuildingLevel4');
|
||||
case 'level5':
|
||||
return t('distill.stageBuildingLevel5');
|
||||
case 'questions':
|
||||
return t('distill.stageBuildingQuestions');
|
||||
case 'datasets':
|
||||
return t('distill.stageBuildingDatasets');
|
||||
case 'multi-turn-datasets':
|
||||
return t('distill.stageBuildingMultiTurnDatasets', { defaultValue: '生成多轮对话数据集中...' });
|
||||
case 'completed':
|
||||
return t('distill.stageCompleted');
|
||||
default:
|
||||
return t('distill.stageInitializing');
|
||||
}
|
||||
};
|
||||
|
||||
const getOverallProgress = () => {
|
||||
const { tagsBuilt, tagsTotal, questionsBuilt, questionsTotal, datasetsBuilt, datasetsTotal } = progress;
|
||||
|
||||
// 整体进度按比例计算:标签构建占30%,问题生成占35%,数据集生成占35%
|
||||
let tagProgress = tagsTotal ? (tagsBuilt / tagsTotal) * 30 : 0;
|
||||
let questionProgress = questionsTotal ? (questionsBuilt / questionsTotal) * 35 : 0;
|
||||
let datasetProgress = datasetsTotal ? (datasetsBuilt / datasetsTotal) * 35 : 0;
|
||||
|
||||
return Math.min(100, Math.round(tagProgress + questionProgress + datasetProgress));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={progress.stage === 'completed' || !progress.stage ? onClose : null}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{t('distill.autoDistillProgress')}
|
||||
{(progress.stage === 'completed' || !progress.stage) && (
|
||||
<IconButton onClick={onClose} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ py: 1 }}>
|
||||
{/* 整体进度 */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('distill.overallProgress')}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<LinearProgress variant="determinate" value={getOverallProgress()} sx={{ height: 10, borderRadius: 5 }} />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 0.5 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{getOverallProgress()}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: progress.multiTurnDatasetsTotal > 0 ? 'repeat(4, 1fr)' : 'repeat(3, 1fr)',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('distill.tagsProgress')}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{progress.tagsBuilt || 0} / {progress.tagsTotal || 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('distill.questionsProgress')}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{progress.questionsBuilt || 0} / {progress.questionsTotal || 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('distill.datasetsProgress')}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{progress.datasetsBuilt || 0} / {progress.datasetsTotal || 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
{progress.multiTurnDatasetsTotal > 0 && (
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('distill.multiTurnDatasetsProgress', { defaultValue: '多轮对话进度' })}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
{progress.multiTurnDatasetsBuilt || 0} / {progress.multiTurnDatasetsTotal || 0}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* 当前阶段 */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('distill.currentStage')}
|
||||
</Typography>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2, bgcolor: 'primary.light', color: 'primary.contrastText' }}>
|
||||
<Typography variant="h6">{getStageText()}</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* 实时日志 */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t('distill.realTimeLogs')}
|
||||
</Typography>
|
||||
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 2,
|
||||
maxHeight: 250,
|
||||
overflow: 'auto',
|
||||
bgcolor: 'grey.900',
|
||||
color: 'grey.100',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.875rem'
|
||||
}}
|
||||
ref={logContainerRef}
|
||||
>
|
||||
{progress.logs?.length > 0 ? (
|
||||
progress.logs.map((log, index) => {
|
||||
// 检测成功日志,显示为绿色 Successfully
|
||||
let color = 'inherit';
|
||||
if (log.includes('成功') || log.includes('完成') || log.includes('Successfully')) {
|
||||
color = '#4caf50';
|
||||
}
|
||||
if (log.includes('失败') || log.toLowerCase().includes('error')) {
|
||||
color = '#f44336';
|
||||
}
|
||||
return (
|
||||
<Box key={index} sx={{ mb: 0.5, color: color }}>
|
||||
{log}
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Typography variant="body2" color="grey.500">
|
||||
{t('distill.waitingForLogs')}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
37
easy-dataset-main/components/distill/ConfirmDialog.js
Normal file
37
easy-dataset-main/components/distill/ConfirmDialog.js
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { Dialog, DialogActions, DialogTitle, Button } from '@mui/material';
|
||||
|
||||
/**
|
||||
* 通用确认对话框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调
|
||||
* @param {Function} props.onConfirm - 确认操作的回调
|
||||
* @param {string} props.title - 对话框标题
|
||||
* @param {string} props.cancelText - 取消按钮文本
|
||||
* @param {string} props.confirmText - 确认按钮文本
|
||||
*/
|
||||
export default function ConfirmDialog({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
cancelText = '取消',
|
||||
confirmText = '确认',
|
||||
confirmColor = 'error'
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} aria-labelledby="confirm-dialog-title">
|
||||
<DialogTitle id="confirm-dialog-title">{title}</DialogTitle>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} color="primary">
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button onClick={onConfirm} color={confirmColor} autoFocus>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
515
easy-dataset-main/components/distill/DistillTreeView.js
Normal file
515
easy-dataset-main/components/distill/DistillTreeView.js
Normal file
@@ -0,0 +1,515 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Box, Typography, List } from '@mui/material';
|
||||
import axios from 'axios';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { selectedModelInfoAtom } from '@/lib/store';
|
||||
import { useGenerateDataset } from '@/hooks/useGenerateDataset';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// 导入子组件
|
||||
import TagTreeItem from './TagTreeItem';
|
||||
import TagMenu from './TagMenu';
|
||||
import TagEditDialog from './TagEditDialog';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import { sortTagsByNumber } from './utils';
|
||||
|
||||
/**
|
||||
* 蒸馏树形视图组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Array} props.tags - 标签列表
|
||||
* @param {Function} props.onGenerateSubTags - 生成子标签的回调函数
|
||||
* @param {Function} props.onGenerateQuestions - 生成问题的回调函数
|
||||
* @param {Function} props.onTagsUpdate - 标签更新的回调函数
|
||||
*/
|
||||
const DistillTreeView = forwardRef(function DistillTreeView(
|
||||
{ projectId, tags = [], onGenerateSubTags, onGenerateQuestions, onTagsUpdate },
|
||||
ref
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const selectedModel = useAtomValue(selectedModelInfoAtom);
|
||||
const [expandedTags, setExpandedTags] = useState({});
|
||||
const [tagQuestions, setTagQuestions] = useState({});
|
||||
const [loadingTags, setLoadingTags] = useState({});
|
||||
const [loadingQuestions, setLoadingQuestions] = useState({});
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState(null);
|
||||
const [selectedTagForMenu, setSelectedTagForMenu] = useState(null);
|
||||
const [allQuestions, setAllQuestions] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [processingQuestions, setProcessingQuestions] = useState({});
|
||||
const [processingMultiTurnQuestions, setProcessingMultiTurnQuestions] = useState({});
|
||||
const [deleteQuestionConfirmOpen, setDeleteQuestionConfirmOpen] = useState(false);
|
||||
const [questionToDelete, setQuestionToDelete] = useState(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [tagToDelete, setTagToDelete] = useState(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [tagToEdit, setTagToEdit] = useState(null);
|
||||
const [project, setProject] = useState(null);
|
||||
const [projectName, setProjectName] = useState('');
|
||||
|
||||
// 使用生成数据集的hook
|
||||
const { generateSingleDataset } = useGenerateDataset();
|
||||
|
||||
// 获取问题统计信息
|
||||
const fetchQuestionsStats = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get(`/api/projects/${projectId}/questions/tree?isDistill=true`);
|
||||
setAllQuestions(response.data);
|
||||
console.log('获取问题统计信息成功:', { totalQuestions: response.data.length });
|
||||
} catch (error) {
|
||||
console.error('获取问题统计信息失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
fetchQuestionsStats
|
||||
}));
|
||||
|
||||
// 获取标签下的问题
|
||||
const fetchQuestionsByTag = useCallback(
|
||||
async tagId => {
|
||||
try {
|
||||
setLoadingQuestions(prev => ({ ...prev, [tagId]: true }));
|
||||
const response = await axios.get(`/api/projects/${projectId}/distill/questions/by-tag?tagId=${tagId}`);
|
||||
setTagQuestions(prev => ({
|
||||
...prev,
|
||||
[tagId]: response.data
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取标签问题失败:', error);
|
||||
} finally {
|
||||
setLoadingQuestions(prev => ({ ...prev, [tagId]: false }));
|
||||
}
|
||||
},
|
||||
[projectId]
|
||||
);
|
||||
|
||||
// 获取项目信息,获取项目名称
|
||||
useEffect(() => {
|
||||
if (projectId) {
|
||||
axios
|
||||
.get(`/api/projects/${projectId}`)
|
||||
.then(response => {
|
||||
setProject(response.data);
|
||||
setProjectName(response.data.name || '');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取项目信息失败:', error);
|
||||
});
|
||||
}
|
||||
}, [projectId]);
|
||||
|
||||
// 初始化时获取问题统计信息
|
||||
useEffect(() => {
|
||||
fetchQuestionsStats();
|
||||
}, [fetchQuestionsStats]);
|
||||
|
||||
// 构建标签树
|
||||
const tagTree = useMemo(() => {
|
||||
const rootTags = [];
|
||||
const tagMap = {};
|
||||
|
||||
// 创建标签映射
|
||||
tags.forEach(tag => {
|
||||
tagMap[tag.id] = { ...tag, children: [] };
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
tags.forEach(tag => {
|
||||
if (tag.parentId && tagMap[tag.parentId]) {
|
||||
tagMap[tag.parentId].children.push(tagMap[tag.id]);
|
||||
} else {
|
||||
rootTags.push(tagMap[tag.id]);
|
||||
}
|
||||
});
|
||||
|
||||
return rootTags;
|
||||
}, [tags]);
|
||||
|
||||
// 切换标签展开/折叠状态
|
||||
const toggleTag = useCallback(
|
||||
tagId => {
|
||||
setExpandedTags(prev => ({
|
||||
...prev,
|
||||
[tagId]: !prev[tagId]
|
||||
}));
|
||||
|
||||
// 如果展开且还没有加载过问题,则加载问题
|
||||
if (!expandedTags[tagId] && !tagQuestions[tagId]) {
|
||||
fetchQuestionsByTag(tagId);
|
||||
}
|
||||
},
|
||||
[expandedTags, tagQuestions, fetchQuestionsByTag]
|
||||
);
|
||||
|
||||
// 处理菜单打开
|
||||
const handleMenuOpen = (event, tag) => {
|
||||
event.stopPropagation();
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
setSelectedTagForMenu(tag);
|
||||
};
|
||||
|
||||
// 处理菜单关闭
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
setSelectedTagForMenu(null);
|
||||
};
|
||||
|
||||
// 打开编辑标签对话框
|
||||
const openEditDialog = () => {
|
||||
setTagToEdit(selectedTagForMenu);
|
||||
setEditDialogOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
// 关闭编辑标签对话框
|
||||
const closeEditDialog = () => {
|
||||
setEditDialogOpen(false);
|
||||
setTagToEdit(null);
|
||||
};
|
||||
|
||||
// 处理编辑标签成功
|
||||
const handleEditTagSuccess = updatedTag => {
|
||||
// 更新标签数据,不刷新页面
|
||||
const updateTagInTree = tagList => {
|
||||
return tagList.map(tag => {
|
||||
if (tag.id === updatedTag.id) {
|
||||
return { ...tag, label: updatedTag.label };
|
||||
}
|
||||
if (tag.children && tag.children.length > 0) {
|
||||
return { ...tag, children: updateTagInTree(tag.children) };
|
||||
}
|
||||
return tag;
|
||||
});
|
||||
};
|
||||
|
||||
// 调用父组件的回调更新标签列表
|
||||
const updatedTags = updateTagInTree(tags);
|
||||
onTagsUpdate?.(updatedTags);
|
||||
};
|
||||
|
||||
// 打开删除确认对话框
|
||||
const openDeleteConfirm = () => {
|
||||
console.log('打开删除确认对话框', selectedTagForMenu);
|
||||
// 保存要删除的标签
|
||||
setTagToDelete(selectedTagForMenu);
|
||||
setDeleteConfirmOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
// 关闭删除确认对话框
|
||||
const closeDeleteConfirm = () => {
|
||||
setDeleteConfirmOpen(false);
|
||||
};
|
||||
|
||||
// 处理删除标签
|
||||
const handleDeleteTag = () => {
|
||||
if (!tagToDelete) {
|
||||
console.log('没有要删除的标签信息');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始删除标签:', tagToDelete.id, tagToDelete.label);
|
||||
|
||||
// 先关闭确认对话框
|
||||
closeDeleteConfirm();
|
||||
|
||||
// 执行删除操作
|
||||
const deleteTagAction = async () => {
|
||||
try {
|
||||
console.log('发送删除请求:', `/api/projects/${projectId}/tags?id=${tagToDelete.id}`);
|
||||
|
||||
// 发送删除请求
|
||||
const response = await axios.delete(`/api/projects/${projectId}/tags?id=${tagToDelete.id}`);
|
||||
|
||||
console.log('删除标签成功:', response.data);
|
||||
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error('删除标签失败:', error);
|
||||
console.error('错误详情:', error.response ? error.response.data : '无响应数据');
|
||||
alert(`删除标签失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 立即执行删除操作
|
||||
deleteTagAction();
|
||||
};
|
||||
|
||||
// 打开删除问题确认对话框
|
||||
const openDeleteQuestionConfirm = (questionId, event) => {
|
||||
event.stopPropagation();
|
||||
setQuestionToDelete(questionId);
|
||||
setDeleteQuestionConfirmOpen(true);
|
||||
};
|
||||
|
||||
// 关闭删除问题确认对话框
|
||||
const closeDeleteQuestionConfirm = () => {
|
||||
setDeleteQuestionConfirmOpen(false);
|
||||
setQuestionToDelete(null);
|
||||
};
|
||||
|
||||
// 处理删除问题
|
||||
const handleDeleteQuestion = async () => {
|
||||
if (!questionToDelete) return;
|
||||
|
||||
try {
|
||||
await axios.delete(`/api/projects/${projectId}/questions/${questionToDelete}`);
|
||||
// 更新问题列表
|
||||
setTagQuestions(prev => {
|
||||
const newQuestions = { ...prev };
|
||||
Object.keys(newQuestions).forEach(tagId => {
|
||||
newQuestions[tagId] = newQuestions[tagId].filter(q => q.id !== questionToDelete);
|
||||
});
|
||||
return newQuestions;
|
||||
});
|
||||
// 关闭确认对话框
|
||||
closeDeleteQuestionConfirm();
|
||||
} catch (error) {
|
||||
console.error('删除问题失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理生成数据集
|
||||
const handleGenerateDataset = async (questionId, questionInfo, event) => {
|
||||
event.stopPropagation();
|
||||
// 设置处理状态
|
||||
setProcessingQuestions(prev => ({
|
||||
...prev,
|
||||
[questionId]: true
|
||||
}));
|
||||
await generateSingleDataset({ projectId, questionId, questionInfo });
|
||||
// 重置处理状态
|
||||
setProcessingQuestions(prev => ({
|
||||
...prev,
|
||||
[questionId]: false
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理生成多轮对话数据集
|
||||
const handleGenerateMultiTurnDataset = async (questionId, questionInfo, event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
try {
|
||||
// 设置处理状态
|
||||
setProcessingMultiTurnQuestions(prev => ({
|
||||
...prev,
|
||||
[questionId]: true
|
||||
}));
|
||||
|
||||
// 首先检查项目是否配置了多轮对话设置
|
||||
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
|
||||
) {
|
||||
throw new Error('请先在项目设置中配置多轮对话相关参数');
|
||||
}
|
||||
|
||||
// 检查是否选择了模型
|
||||
if (!selectedModel || Object.keys(selectedModel).length === 0) {
|
||||
throw new Error('请先选择一个模型');
|
||||
}
|
||||
|
||||
// 调用多轮对话生成API
|
||||
const response = await axios.post(`/api/projects/${projectId}/dataset-conversations`, {
|
||||
questionId,
|
||||
...multiTurnConfig,
|
||||
model: selectedModel,
|
||||
language: 'zh-CN'
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
// 成功后刷新问题统计
|
||||
fetchQuestionsStats();
|
||||
toast.success(t('datasets.multiTurnGenerateSuccess', { defaultValue: '多轮对话数据集生成成功!' }));
|
||||
|
||||
// 通知父组件刷新统计信息
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('refreshDistillStats'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成多轮对话数据集失败:', error);
|
||||
toast.error(error.message || t('datasets.multiTurnGenerateError', { defaultValue: '生成多轮对话数据集失败' }));
|
||||
} finally {
|
||||
// 重置处理状态
|
||||
setProcessingMultiTurnQuestions(prev => ({
|
||||
...prev,
|
||||
[questionId]: false
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// 获取标签路径
|
||||
const getTagPath = useCallback(
|
||||
tag => {
|
||||
if (!tag) return '';
|
||||
|
||||
const findPath = (currentTag, path = []) => {
|
||||
const newPath = [currentTag.label, ...path];
|
||||
|
||||
if (!currentTag.parentId) {
|
||||
// 如果是顶级标签,确保路径以项目名称开始
|
||||
if (projectName && !newPath.includes(projectName)) {
|
||||
return [projectName, ...newPath];
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
const parentTag = tags.find(t => t.id === currentTag.parentId);
|
||||
if (!parentTag) {
|
||||
// 如果没有找到父标签,确保路径以项目名称开始
|
||||
if (projectName && !newPath.includes(projectName)) {
|
||||
return [projectName, ...newPath];
|
||||
}
|
||||
return newPath;
|
||||
}
|
||||
|
||||
return findPath(parentTag, newPath);
|
||||
};
|
||||
|
||||
const path = findPath(tag);
|
||||
|
||||
// 最终检查,确保路径以项目名称开始
|
||||
if (projectName && path.length > 0 && path[0] !== projectName) {
|
||||
path.unshift(projectName);
|
||||
}
|
||||
|
||||
return path.join(' > ');
|
||||
},
|
||||
[tags, projectName]
|
||||
);
|
||||
|
||||
// 渲染标签树
|
||||
const renderTagTree = (tagList, level = 0) => {
|
||||
// 对同级标签进行排序
|
||||
const sortedTagList = sortTagsByNumber(tagList);
|
||||
|
||||
return (
|
||||
<List disablePadding sx={{ px: 2 }}>
|
||||
{sortedTagList.map(tag => (
|
||||
<TagTreeItem
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
level={level}
|
||||
expanded={expandedTags[tag.id]}
|
||||
onToggle={toggleTag}
|
||||
onMenuOpen={handleMenuOpen}
|
||||
onGenerateQuestions={tag => {
|
||||
// 包装函数,处理问题生成后的刷新
|
||||
const handleGenerateQuestionsWithRefresh = async () => {
|
||||
// 调用父组件传入的函数生成问题
|
||||
await onGenerateQuestions(tag, getTagPath(tag));
|
||||
|
||||
// 生成问题后刷新数据
|
||||
await fetchQuestionsStats();
|
||||
|
||||
// 如果标签已展开,刷新该标签的问题详情
|
||||
if (expandedTags[tag.id]) {
|
||||
await fetchQuestionsByTag(tag.id);
|
||||
}
|
||||
};
|
||||
|
||||
handleGenerateQuestionsWithRefresh();
|
||||
}}
|
||||
onGenerateSubTags={tag => onGenerateSubTags(tag, getTagPath(tag))}
|
||||
questions={tagQuestions[tag.id] || []}
|
||||
loadingQuestions={loadingQuestions[tag.id]}
|
||||
processingQuestions={processingQuestions}
|
||||
processingMultiTurnQuestions={processingMultiTurnQuestions}
|
||||
onDeleteQuestion={openDeleteQuestionConfirm}
|
||||
onGenerateDataset={handleGenerateDataset}
|
||||
onGenerateMultiTurnDataset={handleGenerateMultiTurnDataset}
|
||||
allQuestions={allQuestions}
|
||||
tagQuestions={tagQuestions}
|
||||
>
|
||||
{/* 递归渲染子标签 */}
|
||||
{tag.children && tag.children.length > 0 && expandedTags[tag.id] && renderTagTree(tag.children, level + 1)}
|
||||
</TagTreeItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{tagTree.length > 0 ? (
|
||||
renderTagTree(tagTree)
|
||||
) : (
|
||||
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{t('distill.noTags')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 标签操作菜单 */}
|
||||
<TagMenu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
onClose={handleMenuClose}
|
||||
onEdit={openEditDialog}
|
||||
onDelete={openDeleteConfirm}
|
||||
/>
|
||||
|
||||
{/* 编辑标签对话框 */}
|
||||
<TagEditDialog
|
||||
open={editDialogOpen}
|
||||
tag={tagToEdit}
|
||||
projectId={projectId}
|
||||
onClose={closeEditDialog}
|
||||
onSuccess={handleEditTagSuccess}
|
||||
/>
|
||||
|
||||
{/* 删除标签确认对话框 */}
|
||||
<ConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onClose={closeDeleteConfirm}
|
||||
onConfirm={handleDeleteTag}
|
||||
title={t('distill.deleteTagConfirmTitle')}
|
||||
cancelText={t('common.cancel')}
|
||||
confirmText={t('common.delete')}
|
||||
confirmColor="error"
|
||||
/>
|
||||
|
||||
{/* 删除问题确认对话框 */}
|
||||
<ConfirmDialog
|
||||
open={deleteQuestionConfirmOpen}
|
||||
onClose={closeDeleteQuestionConfirm}
|
||||
onConfirm={handleDeleteQuestion}
|
||||
title={t('questions.deleteConfirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
confirmText={t('common.delete')}
|
||||
confirmColor="error"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
export default DistillTreeView;
|
||||
194
easy-dataset-main/components/distill/QuestionGenerationDialog.js
Normal file
194
easy-dataset-main/components/distill/QuestionGenerationDialog.js
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Paper,
|
||||
IconButton,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import axios from 'axios';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
/**
|
||||
* 问题生成对话框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调函数
|
||||
* @param {Function} props.onGenerated - 问题生成完成的回调函数
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Object} props.tag - 标签对象
|
||||
* @param {string} props.tagPath - 标签路径
|
||||
* @param {Object} props.model - 选择的模型配置
|
||||
*/
|
||||
export default function QuestionGenerationDialog({ open, onClose, onGenerated, projectId, tag, tagPath, model }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [count, setCount] = useState(5);
|
||||
const [generatedQuestions, setGeneratedQuestions] = useState([]);
|
||||
|
||||
// 处理生成问题
|
||||
const handleGenerateQuestions = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const response = await axios.post(`/api/projects/${projectId}/distill/questions`, {
|
||||
tagPath,
|
||||
currentTag: tag.label,
|
||||
tagId: tag.id,
|
||||
count,
|
||||
model,
|
||||
language: i18n.language
|
||||
});
|
||||
|
||||
setGeneratedQuestions(response.data);
|
||||
} catch (error) {
|
||||
console.error('生成问题失败:', error);
|
||||
setError(error.response?.data?.error || t('distill.generateQuestionsError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理生成完成
|
||||
const handleGenerateComplete = async () => {
|
||||
if (onGenerated) {
|
||||
onGenerated(generatedQuestions);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// 处理关闭对话框
|
||||
const handleClose = () => {
|
||||
setGeneratedQuestions([]);
|
||||
setError('');
|
||||
setCount(5);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理数量变化
|
||||
const handleCountChange = event => {
|
||||
const value = parseInt(event.target.value);
|
||||
if (!isNaN(value) && value >= 1 && value <= 100) {
|
||||
setCount(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 2 }
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" component="div">
|
||||
{t('distill.generateQuestionsTitle', { tag: tag?.label || t('distill.unknownTag') })}
|
||||
</Typography>
|
||||
<IconButton edge="end" color="inherit" onClick={handleClose} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.tagPath')}:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
|
||||
<Typography variant="body1">{tagPath || tag?.label || t('distill.unknownTag')}</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.questionCount')}:
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
type="number"
|
||||
value={count}
|
||||
onChange={handleCountChange}
|
||||
inputProps={{ min: 1, max: 100 }}
|
||||
disabled={loading}
|
||||
helperText={t('distill.questionCountHelp')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{generatedQuestions.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.generatedQuestions')}:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 0, borderRadius: 1, backgroundColor: 'background.paper' }}>
|
||||
<List disablePadding>
|
||||
{generatedQuestions.map((question, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{index > 0 && <Divider />}
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={question.question}
|
||||
primaryTypographyProps={{
|
||||
style: { whiteSpace: 'normal', wordBreak: 'break-word' }
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button onClick={handleClose} color="inherit">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
{generatedQuestions.length > 0 ? (
|
||||
<Button onClick={handleGenerateComplete} color="primary" variant="contained">
|
||||
{t('common.complete')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleGenerateQuestions}
|
||||
disabled={loading}
|
||||
startIcon={loading && <CircularProgress size={20} color="inherit" />}
|
||||
>
|
||||
{loading ? t('common.generating') : t('distill.generateQuestions')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
121
easy-dataset-main/components/distill/QuestionListItem.js
Normal file
121
easy-dataset-main/components/distill/QuestionListItem.js
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Box,
|
||||
Typography,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import ChatIcon from '@mui/icons-material/Chat';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 问题列表项组件
|
||||
* @param {Object} props
|
||||
* @param {Object} props.question - 问题对象
|
||||
* @param {number} props.level - 缩进级别
|
||||
* @param {Function} props.onDelete - 删除问题的回调
|
||||
* @param {Function} props.onGenerateDataset - 生成数据集的回调
|
||||
* @param {Function} props.onGenerateMultiTurnDataset - 生成多轮对话数据集的回调
|
||||
* @param {boolean} props.processing - 是否正在处理
|
||||
* @param {boolean} props.processingMultiTurn - 是否正在生成多轮对话
|
||||
*/
|
||||
export default function QuestionListItem({
|
||||
question,
|
||||
level,
|
||||
onDelete,
|
||||
onGenerateDataset,
|
||||
onGenerateMultiTurnDataset,
|
||||
processing = false,
|
||||
processingMultiTurn = false
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
pl: (level + 1) * 2,
|
||||
py: 0.75,
|
||||
borderLeft: '1px dashed rgba(0, 0, 0, 0.1)',
|
||||
ml: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
'&:hover': {
|
||||
bgcolor: 'action.hover'
|
||||
}
|
||||
}}
|
||||
secondaryAction={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Tooltip title={t('datasets.generateDataset')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={e => onGenerateDataset(e)}
|
||||
disabled={processing || processingMultiTurn}
|
||||
>
|
||||
{processing ? <CircularProgress size={16} /> : <AutoFixHighIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('questions.generateMultiTurnDataset', { defaultValue: '生成多轮对话数据集' })}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="secondary"
|
||||
onClick={e => onGenerateMultiTurnDataset && onGenerateMultiTurnDataset(e)}
|
||||
disabled={processing || processingMultiTurn || !onGenerateMultiTurnDataset}
|
||||
>
|
||||
{processingMultiTurn ? <CircularProgress size={16} /> : <ChatIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={e => onDelete(e)}
|
||||
disabled={processing || processingMultiTurn}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 32, color: 'secondary.main' }}>
|
||||
<HelpOutlineIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
paddingRight: '28px' // 留出删除按钮的空间
|
||||
}}
|
||||
>
|
||||
{question.question}
|
||||
</Typography>
|
||||
{question.answered && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t('datasets.answered')}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
115
easy-dataset-main/components/distill/TagEditDialog.js
Normal file
115
easy-dataset-main/components/distill/TagEditDialog.js
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import axios from 'axios';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
/**
|
||||
* 标签编辑对话框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Object} props.tag - 要编辑的标签对象
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Function} props.onClose - 关闭对话框的回调
|
||||
* @param {Function} props.onSuccess - 编辑成功的回调
|
||||
*/
|
||||
export default function TagEditDialog({ open, tag, projectId, onClose, onSuccess }) {
|
||||
const { t } = useTranslation();
|
||||
const [newLabel, setNewLabel] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (open && tag) {
|
||||
setNewLabel(tag.label);
|
||||
setError('');
|
||||
}
|
||||
}, [open, tag]);
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!newLabel.trim()) {
|
||||
setError(t('distill.labelRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newLabel === tag.label) {
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const response = await axios.put(`/api/projects/${projectId}/distill/tags/${tag.id}`, { label: newLabel.trim() });
|
||||
|
||||
if (response.status === 200) {
|
||||
toast.success(t('distill.tagUpdateSuccess'));
|
||||
onSuccess?.(response.data);
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('更新标签失败:', err);
|
||||
setError(err.response?.data?.error || t('distill.tagUpdateFailed'));
|
||||
toast.error(err.response?.data?.error || t('distill.tagUpdateFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>{t('distill.editTagTitle')}</DialogTitle>
|
||||
<DialogContent sx={{ pt: 2 }}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t('distill.tagName')}
|
||||
value={newLabel}
|
||||
onChange={e => setNewLabel(e.target.value)}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
onKeyPress={e => {
|
||||
if (e.key === 'Enter' && !loading) {
|
||||
handleConfirm();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} disabled={loading}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
variant="contained"
|
||||
disabled={loading || !newLabel.trim()}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : null}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
230
easy-dataset-main/components/distill/TagGenerationDialog.js
Normal file
230
easy-dataset-main/components/distill/TagGenerationDialog.js
Normal file
@@ -0,0 +1,230 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField,
|
||||
Typography,
|
||||
Box,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Chip,
|
||||
Paper,
|
||||
IconButton
|
||||
} from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import axios from 'axios';
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
/**
|
||||
* 标签生成对话框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.open - 对话框是否打开
|
||||
* @param {Function} props.onClose - 关闭对话框的回调函数
|
||||
* @param {Function} props.onGenerated - 标签生成完成的回调函数
|
||||
* @param {string} props.projectId - 项目ID
|
||||
* @param {Object} props.parentTag - 父标签对象,为null时表示生成根标签
|
||||
* @param {string} props.tagPath - 标签链路
|
||||
* @param {Object} props.model - 选择的模型配置
|
||||
*/
|
||||
export default function TagGenerationDialog({ open, onClose, onGenerated, projectId, parentTag, tagPath, model }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [count, setCount] = useState(5);
|
||||
const [generatedTags, setGeneratedTags] = useState([]);
|
||||
const [parentTagName, setParentTagName] = useState('');
|
||||
const [project, setProject] = useState(null);
|
||||
|
||||
// 获取项目信息,如果是顶级标签,默认填写项目名称
|
||||
useEffect(() => {
|
||||
if (projectId && !parentTag) {
|
||||
axios
|
||||
.get(`/api/projects/${projectId}`)
|
||||
.then(response => {
|
||||
setProject(response.data);
|
||||
setParentTagName(response.data.name || '');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取项目信息失败:', error);
|
||||
});
|
||||
} else if (parentTag) {
|
||||
setParentTagName(parentTag.label || '');
|
||||
}
|
||||
}, [projectId, parentTag]);
|
||||
|
||||
// 处理生成标签
|
||||
const handleGenerateTags = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
const response = await axios.post(`/api/projects/${projectId}/distill/tags`, {
|
||||
parentTag: parentTagName,
|
||||
parentTagId: parentTag ? parentTag.id : null,
|
||||
tagPath: tagPath || parentTagName,
|
||||
count,
|
||||
model,
|
||||
language: i18n.language
|
||||
});
|
||||
|
||||
setGeneratedTags(response.data);
|
||||
} catch (error) {
|
||||
console.error('生成标签失败:', error);
|
||||
setError(error.response?.data?.error || t('distill.generateTagsError'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理生成完成
|
||||
const handleGenerateComplete = async () => {
|
||||
if (onGenerated) {
|
||||
onGenerated(generatedTags);
|
||||
}
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// 处理关闭对话框
|
||||
const handleClose = () => {
|
||||
setGeneratedTags([]);
|
||||
setError('');
|
||||
setCount(5);
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理数量变化
|
||||
const handleCountChange = event => {
|
||||
const value = parseInt(event.target.value);
|
||||
if (!isNaN(value) && value >= 1 && value <= 100) {
|
||||
setCount(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: { borderRadius: 2 }
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography variant="h6" component="div">
|
||||
{parentTag
|
||||
? t('distill.generateSubTagsTitle', { parentTag: parentTag.label })
|
||||
: t('distill.generateRootTagsTitle')}
|
||||
</Typography>
|
||||
<IconButton edge="end" color="inherit" onClick={handleClose} aria-label="close">
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 标签路径显示 */}
|
||||
{parentTag && tagPath && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.tagPath')}:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
|
||||
<Typography variant="body1">{tagPath || parentTag.label}</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.parentTag')}:
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={parentTagName}
|
||||
onChange={e => setParentTagName(e.target.value)}
|
||||
placeholder={t('distill.parentTagPlaceholder')}
|
||||
disabled={loading || !parentTag}
|
||||
// 如果是顶级标签,设置为只读
|
||||
InputProps={{
|
||||
readOnly: !parentTag
|
||||
}}
|
||||
// 显示适当的帮助文本
|
||||
helperText={
|
||||
!parentTag
|
||||
? t('distill.rootTopicHelperText', { defaultValue: '使用项目名称作为顶级主题' })
|
||||
: t('distill.parentTagHelp')
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.tagCount')}:
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
type="number"
|
||||
value={count}
|
||||
onChange={handleCountChange}
|
||||
inputProps={{ min: 1, max: 100 }}
|
||||
disabled={loading}
|
||||
helperText={t('distill.tagCountHelp')}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{generatedTags.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
{t('distill.generatedTags')}:
|
||||
</Typography>
|
||||
<Paper variant="outlined" sx={{ p: 2, borderRadius: 1, backgroundColor: 'background.paper' }}>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{generatedTags.map((tag, index) => (
|
||||
<Chip key={index} label={tag.label} color="primary" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button onClick={handleClose} color="inherit">
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
{generatedTags.length > 0 ? (
|
||||
<Button onClick={handleGenerateComplete} color="primary" variant="contained">
|
||||
{t('common.complete')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleGenerateTags}
|
||||
disabled={loading || !parentTagName}
|
||||
startIcon={loading && <CircularProgress size={20} color="inherit" />}
|
||||
>
|
||||
{loading ? t('common.generating') : t('distill.generateTags')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
46
easy-dataset-main/components/distill/TagMenu.js
Normal file
46
easy-dataset-main/components/distill/TagMenu.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* 标签操作菜单组件
|
||||
* @param {Object} props
|
||||
* @param {HTMLElement} props.anchorEl - 菜单锚点元素
|
||||
* @param {boolean} props.open - 菜单是否打开
|
||||
* @param {Function} props.onClose - 关闭菜单的回调
|
||||
* @param {Function} props.onEdit - 编辑操作的回调
|
||||
* @param {Function} props.onDelete - 删除操作的回调
|
||||
*/
|
||||
export default function TagMenu({ anchorEl, open, onClose, onEdit, onDelete }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEdit = () => {
|
||||
onEdit?.();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
onDelete?.();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
|
||||
<MenuItem onClick={handleEdit}>
|
||||
<ListItemIcon>
|
||||
<EditIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('common.edit')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDelete}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('common.delete')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
240
easy-dataset-main/components/distill/TagTreeItem.js
Normal file
240
easy-dataset-main/components/distill/TagTreeItem.js
Normal file
@@ -0,0 +1,240 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Collapse,
|
||||
Chip,
|
||||
Tooltip,
|
||||
List,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import QuestionListItem from './QuestionListItem';
|
||||
|
||||
/**
|
||||
* 标签树项组件
|
||||
* @param {Object} props
|
||||
* @param {Object} props.tag - 标签对象
|
||||
* @param {number} props.level - 缩进级别
|
||||
* @param {boolean} props.expanded - 是否展开
|
||||
* @param {Function} props.onToggle - 切换展开/折叠的回调
|
||||
* @param {Function} props.onMenuOpen - 打开菜单的回调
|
||||
* @param {Function} props.onGenerateQuestions - 生成问题的回调
|
||||
* @param {Function} props.onGenerateSubTags - 生成子标签的回调
|
||||
* @param {Array} props.questions - 标签下的问题列表
|
||||
* @param {boolean} props.loadingQuestions - 是否正在加载问题
|
||||
* @param {Object} props.processingQuestions - 正在处理的问题ID映射
|
||||
* @param {Function} props.onDeleteQuestion - 删除问题的回调
|
||||
* @param {Function} props.onGenerateDataset - 生成数据集的回调
|
||||
* @param {Function} props.onGenerateMultiTurnDataset - 生成多轮对话数据集的回调
|
||||
* @param {Object} props.processingMultiTurnQuestions - 正在生成多轮对话的问题ID映射
|
||||
* @param {Array} props.allQuestions - 所有问题列表(用于计算问题数量)
|
||||
* @param {Object} props.tagQuestions - 标签问题映射
|
||||
* @param {React.ReactNode} props.children - 子标签内容
|
||||
*/
|
||||
export default function TagTreeItem({
|
||||
tag,
|
||||
level = 0,
|
||||
expanded = false,
|
||||
onToggle,
|
||||
onMenuOpen,
|
||||
onGenerateQuestions,
|
||||
onGenerateSubTags,
|
||||
questions = [],
|
||||
loadingQuestions = false,
|
||||
processingQuestions = {},
|
||||
onDeleteQuestion,
|
||||
onGenerateDataset,
|
||||
onGenerateMultiTurnDataset,
|
||||
processingMultiTurnQuestions = {},
|
||||
allQuestions = [],
|
||||
tagQuestions = {},
|
||||
children
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 递归计算所有层级的子标签数量
|
||||
const getTotalSubTagsCount = childrenTags => {
|
||||
let count = childrenTags.length;
|
||||
childrenTags.forEach(childTag => {
|
||||
if (childTag.children && childTag.children.length > 0) {
|
||||
count += getTotalSubTagsCount(childTag.children);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
// 递归获取所有子标签的问题数量
|
||||
const getChildrenQuestionsCount = childrenTags => {
|
||||
let count = 0;
|
||||
childrenTags.forEach(childTag => {
|
||||
// 子标签的问题
|
||||
if (tagQuestions[childTag.id] && tagQuestions[childTag.id].length > 0) {
|
||||
count += tagQuestions[childTag.id].length;
|
||||
} else {
|
||||
count += allQuestions.filter(q => q.label === childTag.label).length;
|
||||
}
|
||||
|
||||
// 子标签的子标签的问题
|
||||
if (childTag.children && childTag.children.length > 0) {
|
||||
count += getChildrenQuestionsCount(childTag.children);
|
||||
}
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
// 计算当前标签的问题数量
|
||||
const getCurrentTagQuestionsCount = () => {
|
||||
let currentTagQuestions = 0;
|
||||
if (tagQuestions[tag.id] && tagQuestions[tag.id].length > 0) {
|
||||
currentTagQuestions = tagQuestions[tag.id].length;
|
||||
} else {
|
||||
currentTagQuestions = allQuestions.filter(q => q.label === tag.label).length;
|
||||
}
|
||||
return currentTagQuestions;
|
||||
};
|
||||
|
||||
// 总问题数量 = 当前标签的问题 + 所有子标签的问题
|
||||
const totalQuestions =
|
||||
getCurrentTagQuestionsCount() + (tag.children ? getChildrenQuestionsCount(tag.children || []) : 0);
|
||||
|
||||
return (
|
||||
<Box key={tag.id} sx={{ my: 0.5 }}>
|
||||
<ListItem
|
||||
disablePadding
|
||||
sx={{
|
||||
pl: level * 2,
|
||||
borderLeft: level > 0 ? '1px dashed rgba(0, 0, 0, 0.1)' : 'none',
|
||||
ml: level > 0 ? 2 : 0
|
||||
}}
|
||||
>
|
||||
<ListItemButton onClick={() => onToggle(tag.id)} sx={{ borderRadius: 1, py: 0.5 }}>
|
||||
<ListItemIcon sx={{ minWidth: 36 }}>
|
||||
<FolderIcon color="primary" fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ fontWeight: 'medium' }}>{tag.label}</Typography>
|
||||
{tag.children && tag.children.length > 0 && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${getTotalSubTagsCount(tag.children)} ${t('distill.subTags')}`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
{totalQuestions > 0 && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${totalQuestions} ${t('distill.questions')}`}
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
primaryTypographyProps={{ component: 'div' }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Tooltip title={t('distill.generateQuestions')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onGenerateQuestions(tag);
|
||||
}}
|
||||
>
|
||||
<QuestionMarkIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={t('distill.addChildTag')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onGenerateSubTags(tag);
|
||||
}}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<IconButton size="small" onClick={e => onMenuOpen(e, tag)}>
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
{tag.children && tag.children.length > 0 ? (
|
||||
expanded ? (
|
||||
<ExpandLessIcon fontSize="small" />
|
||||
) : (
|
||||
<ExpandMoreIcon fontSize="small" />
|
||||
)
|
||||
) : null}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{/* 子标签 */}
|
||||
{tag.children && tag.children.length > 0 && (
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
{children}
|
||||
</Collapse>
|
||||
)}
|
||||
|
||||
{/* 标签下的问题 */}
|
||||
{expanded && (
|
||||
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||
<List disablePadding sx={{ mt: 0.5, mb: 1 }}>
|
||||
{loadingQuestions ? (
|
||||
<ListItem sx={{ pl: (level + 1) * 2, py: 0.75 }}>
|
||||
<CircularProgress size={20} />
|
||||
<Typography variant="body2" sx={{ ml: 2 }}>
|
||||
{t('common.loading')}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
) : questions && questions.length > 0 ? (
|
||||
questions.map(question => (
|
||||
<QuestionListItem
|
||||
key={question.id}
|
||||
question={question}
|
||||
level={level}
|
||||
processing={processingQuestions[question.id]}
|
||||
processingMultiTurn={processingMultiTurnQuestions[question.id]}
|
||||
onDelete={e => onDeleteQuestion(question.id, e)}
|
||||
onGenerateDataset={e => onGenerateDataset(question.id, question.question, e)}
|
||||
onGenerateMultiTurnDataset={
|
||||
onGenerateMultiTurnDataset ? e => onGenerateMultiTurnDataset(question.id, question, e) : undefined
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<ListItem sx={{ pl: (level + 1) * 2, py: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('distill.noQuestions')}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Collapse>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
72
easy-dataset-main/components/distill/utils.js
Normal file
72
easy-dataset-main/components/distill/utils.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* 按照标签前面的序号对标签进行排序
|
||||
* @param {Array} tags - 标签数组
|
||||
* @returns {Array} 排序后的标签数组
|
||||
*/
|
||||
export const sortTagsByNumber = tags => {
|
||||
return [...tags].sort((a, b) => {
|
||||
// 提取标签前面的序号
|
||||
const getNumberPrefix = label => {
|
||||
// 匹配形如 1, 1.1, 1.1.2 的序号
|
||||
const match = label.match(/^([\d.]+)\s/);
|
||||
if (match) {
|
||||
return match[1]; // 返回完整的序号字符串,如 "1.10"
|
||||
}
|
||||
return null; // 没有序号
|
||||
};
|
||||
|
||||
const aPrefix = getNumberPrefix(a.label);
|
||||
const bPrefix = getNumberPrefix(b.label);
|
||||
|
||||
// 如果两个标签都有序号,按序号比较
|
||||
if (aPrefix && bPrefix) {
|
||||
// 将序号分解为数组,然后按数值比较
|
||||
const aParts = aPrefix.split('.').map(num => parseInt(num, 10));
|
||||
const bParts = bPrefix.split('.').map(num => parseInt(num, 10));
|
||||
|
||||
// 比较序号数组
|
||||
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
||||
if (aParts[i] !== bParts[i]) {
|
||||
return aParts[i] - bParts[i]; // 数值比较,确保 1.2 排在 1.10 前面
|
||||
}
|
||||
}
|
||||
// 如果前面的数字都相同,则较短的序号在前
|
||||
return aParts.length - bParts.length;
|
||||
}
|
||||
// 如果只有一个标签有序号,则有序号的在前
|
||||
else if (aPrefix) {
|
||||
return -1;
|
||||
} else if (bPrefix) {
|
||||
return 1;
|
||||
}
|
||||
// 如果都没有序号,则按原来的字母序排序
|
||||
else {
|
||||
return a.label.localeCompare(b.label, 'zh-CN');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取标签的完整路径
|
||||
* @param {Object} tag - 标签对象
|
||||
* @param {Array} allTags - 所有标签数组
|
||||
* @returns {string} 标签路径,如 "标签1 > 标签2 > 标签3"
|
||||
*/
|
||||
export const getTagPath = (tag, allTags) => {
|
||||
if (!tag) return '';
|
||||
|
||||
const findPath = (currentTag, path = []) => {
|
||||
const newPath = [currentTag.label, ...path];
|
||||
|
||||
if (!currentTag.parentId) return newPath;
|
||||
|
||||
const parentTag = allTags.find(t => t.id === currentTag.parentId);
|
||||
if (!parentTag) return newPath;
|
||||
|
||||
return findPath(parentTag, newPath);
|
||||
};
|
||||
|
||||
return findPath(tag).join(' > ');
|
||||
};
|
||||
Reference in New Issue
Block a user