first-update
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user