import React, { type FC, useEffect, useState, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Col } from 'antd' import RbCard from '@/components/RbCard/Card' import ReactEcharts from 'echarts-for-react' import zoom from '@/assets/images/userMemory/zoom.svg' import drag from '@/assets/images/userMemory/drag.svg' import pointer from '@/assets/images/userMemory/pointer.svg' import empty from '@/assets/images/userMemory/empty.svg' import Empty from '@/components/Empty' // 知识图谱数据类型定义 export interface KnowledgeNode { id: string entity_name: string entity_type: string description: string pagerank: number source_id: string[] // ECharts 需要的属性 name: string category: number symbolSize: number itemStyle: { color: string } } export interface KnowledgeEdge { src_id: string tgt_id: string description: string keywords: string[] weight: number source_id: string[] source: string target: string // ECharts 需要的属性 value: number } export interface KnowledgeGraphData { directed: boolean multigraph: boolean graph: { source_id: string[] } nodes: KnowledgeNode[] edges: KnowledgeEdge[] } export interface KnowledgeGraphResponse { graph: KnowledgeGraphData mind_map: Record } interface KnowledgeGraphProps { data?: KnowledgeGraphResponse loading?: boolean } const operations = [ { name: 'click', icon: pointer }, { name: 'drag', icon: drag }, { name: 'zoom', icon: zoom }, ] // 预定义的颜色调色板 const colorPalette = [ '#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21', '#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6', '#3498DB', '#1ABC9C', '#F39C12', '#D35400', '#C0392B', '#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22' ] // 动态生成实体类型颜色映射 const generateEntityTypeColors = (entityTypes: string[]): Record => { const colorMap: Record = {} entityTypes.forEach((type, index) => { colorMap[type] = colorPalette[index % colorPalette.length] }) return colorMap } const KnowledgeGraph: FC = ({ data, loading = false }) => { const { t } = useTranslation() const chartRef = useRef(null) const resizeScheduledRef = useRef(false) const modalRef = useRef(null) const [nodes, setNodes] = useState([]) const [links, setLinks] = useState([]) const [categories, setCategories] = useState<{ name: string }[]>([]) const [selectedNode, setSelectedNode] = useState(null) const [entityTypeColors, setEntityTypeColors] = useState>({}) // 弹框拖动相关状态 const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 }) const [isDragging, setIsDragging] = useState(false) const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) // 拖动处理函数 const handleMouseDown = useCallback((e: React.MouseEvent) => { setIsDragging(true) setDragStart({ x: e.clientX - modalPosition.x, y: e.clientY - modalPosition.y }) }, [modalPosition]) const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging) return const newX = e.clientX - dragStart.x const newY = e.clientY - dragStart.y // 限制拖动范围,确保弹框不会超出容器 const container = chartRef.current?.getEchartsInstance().getDom().parentElement if (container && modalRef.current) { const containerRect = container.getBoundingClientRect() const modalRect = modalRef.current.getBoundingClientRect() const maxX = containerRect.width - modalRect.width const maxY = containerRect.height - modalRect.height setModalPosition({ x: Math.max(0, Math.min(newX, maxX)), y: Math.max(0, Math.min(newY, maxY)) }) } }, [isDragging, dragStart]) const handleMouseUp = useCallback(() => { setIsDragging(false) }, []) // 添加全局鼠标事件监听 useEffect(() => { if (isDragging) { document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } } }, [isDragging, handleMouseMove, handleMouseUp]) // 关闭弹框 const handleCloseModal = useCallback(() => { setSelectedNode(null) }, []) // 处理知识图谱数据 const processGraphData = useCallback(() => { if (!data?.graph) { setNodes([]) setLinks([]) setCategories([]) setSelectedNode(null) return } const { nodes: rawNodes, edges: rawEdges } = data.graph const processedNodes: KnowledgeNode[] = [] const processedEdges: KnowledgeEdge[] = [] // 获取所有实体类型 const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))] const categoryMap = entityTypes.reduce((acc, type, index) => { acc[type] = index return acc }, {} as Record) // 动态生成实体类型颜色映射 const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes) setEntityTypeColors(dynamicEntityTypeColors) // 计算每个节点的连接数 const connectionCount: Record = {} rawEdges.forEach(edge => { // 使用 src_id 和 tgt_id 计算连接数 connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1 connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1 }) // 处理节点数据 rawNodes.forEach(node => { const connections = connectionCount[node.id] || 0 const categoryIndex = categoryMap[node.entity_type] || 0 // 根据 pagerank 和连接数计算节点大小 let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2)) processedNodes.push({ ...node, name: node.entity_name, category: categoryIndex, symbolSize, itemStyle: { color: dynamicEntityTypeColors[node.entity_type] || colorPalette[0] } }) }) // 处理边数据 rawEdges.forEach(edge => { // 注意:根据数据结构,source 和 target 字段可能与 src_id 和 tgt_id 相反 // 我们使用 src_id 和 tgt_id 作为正确的连接关系 processedEdges.push({ ...edge, // 保留所有原始字段 source: edge.src_id, // 使用 src_id 作为源节点 target: edge.tgt_id, // 使用 tgt_id 作为目标节点 value: edge.weight || 1 }) }) // 验证节点ID和边的连接 const nodeIds = new Set(processedNodes.map(n => n.id)) const validEdges = processedEdges.filter(edge => { const sourceExists = nodeIds.has(edge.source) const targetExists = nodeIds.has(edge.target) if (!sourceExists || !targetExists) { console.warn('Invalid edge:', edge, 'Source exists:', sourceExists, 'Target exists:', targetExists) } return sourceExists && targetExists }) // 调试信息 console.log('Total nodes:', processedNodes.length) console.log('Total edges:', processedEdges.length) console.log('Valid edges:', validEdges.length) console.log('Node IDs:', Array.from(nodeIds).slice(0, 5)) console.log('Edge sample:', validEdges.slice(0, 3)) // 设置分类 const processedCategories = entityTypes.map(type => ({ name: type })) setNodes(processedNodes) setLinks(validEdges) // 只使用有效的边 setCategories(processedCategories) }, [data]) useEffect(() => { processGraphData() }, [processGraphData]) useEffect(() => { const handleResize = () => { if (chartRef.current && !resizeScheduledRef.current) { resizeScheduledRef.current = true requestAnimationFrame(() => { chartRef.current?.getEchartsInstance().resize() resizeScheduledRef.current = false }) } } const resizeObserver = new ResizeObserver(handleResize) const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement if (chartElement) { resizeObserver.observe(chartElement) } return () => { resizeObserver.disconnect() } }, [nodes]) return (
{loading ? (
加载中...
) : nodes.length === 0 ? ( ) : ( <> { if (params.dataType === 'node') { const node = params.data as KnowledgeNode return `
${node.entity_name}
类型: ${node.entity_type}
重要度: ${(node.pagerank * 100).toFixed(2)}%
` } else if (params.dataType === 'edge') { const edge = params.data as KnowledgeEdge return `
关系
权重: ${edge.weight}
${edge.description}
` } return '' } }, legend: { data: categories.map(cat => cat.name), orient: 'vertical', left: 'right', top: 'center' }, series: [ { type: 'graph', layout: 'force', data: nodes, links: links, categories: categories, roam: true, label: { show: true, position: 'right', formatter: '{b}', fontSize: 12 }, lineStyle: { color: '#5B6167', curveness: 0.3, width: 2, // 固定线宽,避免函数问题 opacity: 0.8 }, force: { repulsion: 300, edgeLength: 150, gravity: 0.1, layoutAnimation: true, preventOverlap: true }, selectedMode: 'single', draggable: true, animationDurationUpdate: 0, select: { itemStyle: { borderWidth: 2, borderColor: '#ffffff', shadowBlur: 10, } }, emphasis: { focus: 'adjacency', lineStyle: { width: 3 } } } ] }} style={{ height: '496px', width: '100%' }} notMerge={false} lazyUpdate={true} onEvents={{ click: (params: { dataType: string; data: KnowledgeNode }) => { if (params.dataType === 'node') { console.log('Knowledge node clicked:', params.data) setSelectedNode(params.data) } } }} /> {/* 实体详情弹框 */} {selectedNode && (
{/* 弹框头部 - 可拖动区域 */}
{t('knowledgeBase.entityDetails')}
{/* 弹框内容 */}
{selectedNode.entity_name}
{selectedNode.entity_type} 重要度: {(selectedNode.pagerank * 100).toFixed(2)}%
{t('knowledgeBase.entityDescription')}
{selectedNode.description}
{t('knowledgeBase.sourceDocuments')}
{selectedNode.source_id.length} 个文档
)} )}
{operations.map((item) => (
{t(`userMemory.${item.name}`)}
))}
) } export default React.memo(KnowledgeGraph)