Files
YG-Datasets/easy-dataset-main/components/text-split/DomainAnalysis.js

561 lines
16 KiB
JavaScript

'use client';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Box,
Paper,
Typography,
Divider,
CircularProgress,
Tabs,
Tab,
List,
ListItem,
ListItemText,
Collapse,
IconButton,
TextField,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Tooltip,
Menu,
MenuItem
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import TabPanel from './components/TabPanel';
import ReactMarkdown from 'react-markdown';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import axios from 'axios';
import { toast } from 'sonner';
import 'github-markdown-css/github-markdown-light.css';
/**
* 领域分析组件
* @param {Object} props
* @param {string} props.projectId - 项目ID
* @param {Array} props.toc - 目录结构数组
* @param {Array} props.tags - 标签树数组
* @param {boolean} props.loading - 是否加载中
* @param {Function} props.onTagsUpdate - 标签更新回调
*/
// 领域树节点组件
function TreeNode({ node, level = 0, onEdit, onDelete, onAddChild }) {
const [open, setOpen] = useState(true);
const theme = useTheme();
const hasChildren = node.child && node.child.length > 0;
const [anchorEl, setAnchorEl] = useState(null);
const menuOpen = Boolean(anchorEl);
const { t } = useTranslation();
const handleClick = () => {
if (hasChildren) {
setOpen(!open);
}
};
const handleMenuOpen = event => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const handleMenuClose = event => {
if (event) event.stopPropagation();
setAnchorEl(null);
};
const handleEdit = event => {
event.stopPropagation();
onEdit(node);
handleMenuClose();
};
const handleDelete = event => {
event.stopPropagation();
onDelete(node);
handleMenuClose();
};
const handleAddChild = event => {
event.stopPropagation();
onAddChild(node);
handleMenuClose();
};
return (
<>
<ListItem
button
onClick={handleClick}
sx={{
pl: level * 2 + 1,
bgcolor: level === 0 ? theme.palette.primary.light : 'transparent',
color: level === 0 ? theme.palette.primary.contrastText : 'inherit',
'&:hover': {
bgcolor: level === 0 ? theme.palette.primary.main : theme.palette.action.hover
},
borderRadius: '4px',
mb: 0.5,
pr: 1
}}
>
<ListItemText
primary={node.label}
primaryTypographyProps={{
fontWeight: level === 0 ? 600 : 400,
fontSize: level === 0 ? '1rem' : '0.9rem'
}}
/>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton
size="small"
onClick={handleMenuOpen}
sx={{
color: level === 0 ? 'inherit' : theme.palette.text.secondary,
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
{hasChildren && (open ? <ExpandLess /> : <ExpandMore />)}
</Box>
<Menu anchorEl={anchorEl} open={menuOpen} onClose={handleMenuClose} onClick={e => e.stopPropagation()}>
<MenuItem onClick={handleEdit}>
<EditIcon fontSize="small" sx={{ mr: 1 }} />
{t('textSplit.editTag')}
</MenuItem>
<MenuItem onClick={handleDelete}>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
{t('textSplit.deleteTag')}
</MenuItem>
{level === 0 && (
<MenuItem onClick={handleAddChild}>
<AddIcon fontSize="small" sx={{ mr: 1 }} />
{t('textSplit.addTag')}
</MenuItem>
)}
</Menu>
</ListItem>
{hasChildren && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{node.child.map((childNode, index) => (
<TreeNode
key={index}
node={childNode}
level={level + 1}
onEdit={onEdit}
onDelete={onDelete}
onAddChild={onAddChild}
/>
))}
</List>
</Collapse>
)}
</>
);
}
// 领域树组件
function DomainTree({ tags, onEdit, onDelete, onAddChild }) {
return (
<List component="nav" aria-label="domain tree">
{tags.map((node, index) => (
<TreeNode key={index} node={node} onEdit={onEdit} onDelete={onDelete} onAddChild={onAddChild} />
))}
</List>
);
}
export default function DomainAnalysis({ projectId, toc = '', loading = false }) {
const theme = useTheme();
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [currentNode, setCurrentNode] = useState(null);
const [parentNode, setParentNode] = useState('');
const [dialogMode, setDialogMode] = useState('add');
const [labelValue, setLabelValue] = useState({});
const [saving, setSaving] = useState(false);
const [tags, setTags] = useState([]);
const [snackbar, setSnackbar] = useState({
open: false,
message: '',
severity: 'success'
});
const handleCloseSnackbar = () => {
setSnackbar(prev => ({ ...prev, open: false }));
};
useEffect(() => {
getTags();
}, []);
const getTags = async () => {
const response = await axios.get(`/api/projects/${projectId}/tags`);
setTags(response.data.tags);
};
// 处理标签切换
const handleTabChange = (event, newValue) => {
setActiveTab(newValue);
};
// 打开添加标签对话框
const handleAddTag = () => {
setDialogMode('add');
setCurrentNode(null);
setParentNode(null);
setLabelValue({});
setDialogOpen(true);
};
// 打开编辑标签对话框
const handleEditTag = node => {
setDialogMode('edit');
setCurrentNode({ id: node.id, label: node.label });
setLabelValue({ id: node.id, label: node.label });
setDialogOpen(true);
};
// 打开添加子标签对话框
const handleAddChildTag = parentNode => {
setDialogMode('addChild');
setParentNode(parentNode.label);
setLabelValue({ parentId: parentNode.id });
setDialogOpen(true);
};
// 打开删除标签对话框
const handleDeleteTag = node => {
setCurrentNode(node);
setDeleteDialogOpen(true);
};
// 关闭对话框
const handleCloseDialog = () => {
setDialogOpen(false);
setDeleteDialogOpen(false);
};
// 查找并更新节点
const findAndUpdateNode = (nodes, targetNode, newLabel) => {
return nodes.map(node => {
if (node === targetNode) {
return { ...node, label: newLabel };
}
if (node.child && node.child.length > 0) {
return { ...node, child: findAndUpdateNode(node.child, targetNode, newLabel) };
}
return node;
});
};
// 查找并删除节点
const findAndDeleteNode = (nodes, targetNode) => {
return nodes
.filter(node => node !== targetNode)
.map(node => {
if (node.child && node.child.length > 0) {
return { ...node, child: findAndDeleteNode(node.child, targetNode) };
}
return node;
});
};
// 查找并添加子节点
const findAndAddChildNode = (nodes, parentNode, childLabel) => {
return nodes.map(node => {
if (node === parentNode) {
const childArray = node.child || [];
return {
...node,
child: [...childArray, { label: childLabel, child: [] }]
};
}
if (node.child && node.child.length > 0) {
return { ...node, child: findAndAddChildNode(node.child, parentNode, childLabel) };
}
return node;
});
};
// 保存标签更改
const saveTagChanges = async updatedTags => {
console.log('保存标签更改:', updatedTags);
setSaving(true);
try {
const response = await fetch(`/api/projects/${projectId}/tags`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags: updatedTags })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || t('domain.errors.saveFailed'));
}
getTags();
setSnackbar({
open: true,
message: t('domain.messages.updateSuccess'),
severity: 'success'
});
} catch (error) {
console.error('保存标签失败:', error);
setSnackbar({
open: true,
message: error.message || '保存标签失败',
severity: 'error'
});
} finally {
setSaving(false);
}
};
// 提交表单
const handleSubmit = async () => {
if (!labelValue.label.trim()) {
setSnackbar({
open: true,
message: '标签名称不能为空',
severity: 'error'
});
return;
}
await saveTagChanges(labelValue);
handleCloseDialog();
};
const handleConfirmDelete = async () => {
if (!currentNode) return;
const res = await axios.delete(`/api/projects/${projectId}/tags?id=${currentNode.id}`);
if (res.status === 200) {
toast.success('删除成功');
getTags();
}
setDeleteDialogOpen(false);
};
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
</Box>
);
}
if (toc.length === 0) {
return (
<Paper
sx={{
p: 4,
textAlign: 'center',
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2
}}
>
<Typography variant="body1" color="textSecondary">
{t('domain.noToc')}
</Typography>
</Paper>
);
}
return (
<Box>
<Paper
sx={{
p: 0,
mb: 3,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 2,
overflow: 'hidden'
}}
>
<Tabs
value={activeTab}
onChange={handleTabChange}
variant="fullWidth"
indicatorColor="secondary"
sx={{
borderBottom: 1,
borderColor: 'divider',
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)',
borderTopLeftRadius: 2,
borderTopRightRadius: 2
}}
>
<Tab label={t('domain.tabs.tree')} />
<Tab label={t('domain.tabs.structure')} />
</Tabs>
<Box
sx={{
p: 3,
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.02)' : 'rgba(255, 255, 255, 0.8)',
borderBottomLeftRadius: 2,
borderBottomRightRadius: 2,
boxShadow: 'inset 0 2px 4px rgba(0,0,0,0.03)'
}}
>
<TabPanel value={activeTab} index={0}>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">{t('domain.tabs.tree')}</Typography>
<Tooltip title="添加一级标签">
<Button variant="outlined" size="small" startIcon={<AddIcon />} onClick={handleAddTag}>
{t('domain.addRootTag')}
</Button>
</Tooltip>
</Box>
<Divider sx={{ mb: 2 }} />
<Box
sx={{
p: 2,
bgcolor: theme.palette.background.paper,
borderRadius: 1,
maxHeight: '800px',
overflow: 'auto'
}}
>
{tags && tags.length > 0 ? (
<DomainTree
tags={tags}
onEdit={handleEditTag}
onDelete={handleDeleteTag}
onAddChild={handleAddChildTag}
/>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body2" color="textSecondary" gutterBottom>
{t('domain.noTags')}
</Typography>
<Button
variant="outlined"
size="small"
startIcon={<AddIcon />}
onClick={handleAddTag}
sx={{ mt: 1 }}
>
{t('domain.addFirstTag')}
</Button>
</Box>
)}
</Box>
</Box>
</TabPanel>
<TabPanel value={activeTab} index={1}>
<Box>
<Typography variant="h6" gutterBottom>
{t('domain.docStructure')}
</Typography>
<Divider sx={{ mb: 2 }} />
<Box
sx={{
p: 2,
bgcolor: theme.palette.background.paper,
borderRadius: 1,
maxHeight: '600px',
overflow: 'auto'
}}
>
<div className="markdown-body">
<ReactMarkdown
components={{
root: ({ children }) => (
<div
style={{
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}
>
{children}
</div>
)
}}
>
{toc}
</ReactMarkdown>
</div>
</Box>
</Box>
</TabPanel>
</Box>
</Paper>
{/* 添加/编辑标签对话框 */}
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{dialogMode === 'add'
? t('domain.dialog.addTitle')
: dialogMode === 'edit'
? t('domain.dialog.editTitle')
: t('domain.dialog.addChildTitle')}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ mb: 2 }}>
{dialogMode === 'add'
? t('domain.dialog.inputRoot')
: dialogMode === 'edit'
? t('domain.dialog.inputEdit')
: t('domain.dialog.inputChild', { label: parentNode })}
</DialogContentText>
<TextField
autoFocus
margin="dense"
label={t('domain.dialog.labelName')}
type="text"
fullWidth
variant="outlined"
value={labelValue.label}
onChange={e => setLabelValue({ ...labelValue, label: e.target.value })}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
<Button onClick={handleSubmit} variant="contained" disabled={saving || !labelValue?.label?.trim()}>
{saving ? t('common.saving') : t('common.save')}
</Button>
</DialogActions>
</Dialog>
{/* 删除确认对话框 */}
<Dialog open={deleteDialogOpen} onClose={handleCloseDialog}>
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('domain.dialog.deleteConfirm', { label: currentNode?.label })}
{currentNode?.child && currentNode.child.length > 0 && t('domain.dialog.deleteWarning')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>{t('common.cancel')}</Button>
<Button onClick={handleConfirmDelete} color="error" variant="contained">
{saving ? t('common.deleting') : t('common.delete')}
</Button>
</DialogActions>
</Dialog>
</Box>
);
}