516 lines
16 KiB
JavaScript
516 lines
16 KiB
JavaScript
'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;
|