#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { build } = require('esbuild'); // 读取命令行参数 const args = process.argv.slice(2); if (args.length === 0) { console.error('Usage: node visualize_v2.js [output.html]'); process.exit(1); } const inputFile = args[0]; const outputFile = args[1] || 'query_graph_output.html'; // 读取输入数据 const data = JSON.parse(fs.readFileSync(inputFile, 'utf-8')); // 创建临时 React 组件文件 const reactComponentPath = path.join(__dirname, 'temp_flow_component_v2.jsx'); const reactComponent = ` import React, { useState, useCallback, useMemo, useEffect } from 'react'; import { createRoot } from 'react-dom/client'; import { ReactFlow, Controls, Background, useNodesState, useEdgesState, Handle, Position, useReactFlow, ReactFlowProvider, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; const data = ${JSON.stringify(data, null, 2)}; // 查询节点组件 - 卡片样式 function QueryNode({ id, data, sourcePosition, targetPosition }) { // 所有节点默认展开 const expanded = true; return (
{/* 折叠/展开子节点按钮 */} {data.hasChildren && (
{ e.stopPropagation(); data.onToggleCollapse(); }} title={data.isCollapsed ? '展开子节点' : '折叠子节点'} > {data.isCollapsed ? '+' : '−'}
)} {/* 卡片内容 */}
{/* 标题行 */}
{data.title}
{data.isSelected === false && (
未选中
)}
{/* 展开的详细信息 - 始终显示 */}
Lv.{data.level} {data.score} {data.strategy && data.strategy !== 'root' && ( {data.strategy} )}
{data.parent && (
Parent: {data.parent}
)} {data.evaluationReason && (
评估:
{data.evaluationReason}
)}
); } // 笔记节点组件 - 卡片样式 function NoteNode({ id, data, sourcePosition, targetPosition }) { // 所有节点默认展开 const expanded = true; return (
{/* 笔记图标和标题 */}
📝
{data.title}
{/* 标签 */}
{data.matchLevel} Score: {data.score}
{/* 描述 */} {expanded && data.description && (
{data.description}
)}
); } const nodeTypes = { query: QueryNode, note: NoteNode, }; // 树节点组件 function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) { const hasChildren = children && children.length > 0; return (
{ if (!isSelected) e.currentTarget.style.background = '#f9fafb'; }} onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }} > {hasChildren && ( { e.stopPropagation(); onToggle(); }} > {isCollapsed ? '▶' : '▼'} )} {!hasChildren && }
{node.id}
{node.data.isSelected === false && ( )}
{hasChildren && !isCollapsed && (
{children}
)}
); } // 使用 dagre 自动布局 function getLayoutedElements(nodes, edges, direction = 'LR') { console.log('🎯 Starting layout with dagre...'); console.log('Input:', nodes.length, 'nodes,', edges.length, 'edges'); // 检查 dagre 是否加载 if (typeof window === 'undefined' || typeof window.dagre === 'undefined') { console.warn('⚠️ Dagre not loaded, using fallback layout'); // 降级到简单布局 const levelGroups = {}; nodes.forEach(node => { const level = node.data.level || 0; if (!levelGroups[level]) levelGroups[level] = []; levelGroups[level].push(node); }); Object.entries(levelGroups).forEach(([level, nodeList]) => { const x = parseInt(level) * 350; nodeList.forEach((node, index) => { node.position = { x, y: index * 150 }; node.targetPosition = 'left'; node.sourcePosition = 'right'; }); }); return { nodes, edges }; } try { const dagreGraph = new window.dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); const nodeWidth = 280; const nodeHeight = 180; const isHorizontal = direction === 'LR'; dagreGraph.setGraph({ rankdir: direction, nodesep: 80, ranksep: 300, }); // 添加节点 nodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); }); // 添加边 edges.forEach((edge) => { dagreGraph.setEdge(edge.source, edge.target); }); // 计算布局 window.dagre.layout(dagreGraph); console.log('✅ Dagre layout completed'); // 更新节点位置和 handle 位置 nodes.forEach((node) => { const nodeWithPosition = dagreGraph.node(node.id); if (!nodeWithPosition) { console.warn('Node position not found for:', node.id); return; } node.targetPosition = isHorizontal ? 'left' : 'top'; node.sourcePosition = isHorizontal ? 'right' : 'bottom'; // 将 dagre 的中心点位置转换为 React Flow 的左上角位置 node.position = { x: nodeWithPosition.x - nodeWidth / 2, y: nodeWithPosition.y - nodeHeight / 2, }; }); console.log('✅ Layout completed, sample node:', nodes[0]); return { nodes, edges }; } catch (error) { console.error('❌ Error in dagre layout:', error); console.error('Error details:', error.message, error.stack); // 降级处理 console.log('Using fallback layout...'); const levelGroups = {}; nodes.forEach(node => { const level = node.data.level || 0; if (!levelGroups[level]) levelGroups[level] = []; levelGroups[level].push(node); }); Object.entries(levelGroups).forEach(([level, nodeList]) => { const x = parseInt(level) * 350; nodeList.forEach((node, index) => { node.position = { x, y: index * 150 }; node.targetPosition = 'left'; node.sourcePosition = 'right'; }); }); return { nodes, edges }; } } function transformData(data) { const nodes = []; const edges = []; // 创建节点 Object.entries(data.nodes).forEach(([id, node]) => { if (node.type === 'query') { nodes.push({ id: id, // 使用循环中的 id 参数,不是 node.id type: 'query', data: { title: node.query, level: node.level, score: node.relevance_score.toFixed(2), strategy: node.strategy, parent: node.parent_query, isSelected: node.is_selected, evaluationReason: node.evaluation_reason || '', }, position: { x: 0, y: 0 }, // 初始位置,会被 dagre 覆盖 }); } else if (node.type === 'note') { nodes.push({ id: id, // 使用循环中的 id 参数,不是 node.id type: 'note', data: { title: node.title, matchLevel: node.match_level, score: node.relevance_score.toFixed(2), description: node.desc, isSelected: node.is_selected !== undefined ? node.is_selected : true, }, position: { x: 0, y: 0 }, }); } }); // 创建边 - 使用虚线样式 data.edges.forEach((edge, index) => { const edgeColors = { direct_sug: '#10b981', rewrite_synonym: '#f59e0b', add_word: '#3b82f6', rewrite_abstract: '#8b5cf6', query_to_note: '#ec4899', }; const color = edgeColors[edge.strategy] || edgeColors[edge.edge_type] || '#d1d5db'; const isNoteEdge = edge.edge_type === 'query_to_note'; edges.push({ id: \`edge-\${index}\`, source: edge.from, target: edge.to, type: 'smoothstep', animated: isNoteEdge, style: { stroke: color, strokeWidth: isNoteEdge ? 2.5 : 2, strokeDasharray: isNoteEdge ? '5,5' : '8,4', }, markerEnd: { type: 'arrowclosed', color: color, width: 20, height: 20, }, }); }); // 使用 dagre 自动计算布局 - 从左到右 return getLayoutedElements(nodes, edges, 'LR'); } function FlowContent() { const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { console.log('🔍 Transforming data...'); const result = transformData(data); console.log('✅ Transformed:', result.nodes.length, 'nodes,', result.edges.length, 'edges'); return result; }, []); // 初始化:找出所有有子节点的节点,默认折叠(画布节点) const initialCollapsedNodes = useMemo(() => { const nodesWithChildren = new Set(); initialEdges.forEach(edge => { nodesWithChildren.add(edge.source); }); // 排除根节点(level 0),让根节点默认展开 const rootNode = initialNodes.find(n => n.data.level === 0); if (rootNode) { nodesWithChildren.delete(rootNode.id); } return nodesWithChildren; }, [initialNodes, initialEdges]); // 树节点的折叠状态需要在树构建后初始化 const [collapsedNodes, setCollapsedNodes] = useState(() => initialCollapsedNodes); const [collapsedTreeNodes, setCollapsedTreeNodes] = useState(new Set()); const [selectedNodeId, setSelectedNodeId] = useState(null); // 获取 React Flow 实例以控制画布 const { setCenter, fitView } = useReactFlow(); // 获取某个节点的所有后代节点ID const getDescendants = useCallback((nodeId) => { const descendants = new Set(); const queue = [nodeId]; while (queue.length > 0) { const current = queue.shift(); initialEdges.forEach(edge => { if (edge.source === current && !descendants.has(edge.target)) { descendants.add(edge.target); queue.push(edge.target); } }); } return descendants; }, [initialEdges]); // 获取直接父节点 const getDirectParents = useCallback((nodeId) => { const parents = []; initialEdges.forEach(edge => { if (edge.target === nodeId) { parents.push(edge.source); } }); return parents; }, [initialEdges]); // 获取直接子节点 const getDirectChildren = useCallback((nodeId) => { const children = []; initialEdges.forEach(edge => { if (edge.source === nodeId) { children.push(edge.target); } }); return children; }, [initialEdges]); // 切换节点折叠状态 const toggleNodeCollapse = useCallback((nodeId) => { setCollapsedNodes(prev => { const newSet = new Set(prev); const descendants = getDescendants(nodeId); if (newSet.has(nodeId)) { // 展开:移除此节点,但保持其他折叠的节点 newSet.delete(nodeId); } else { // 折叠:添加此节点 newSet.add(nodeId); } return newSet; }); }, [getDescendants]); // 过滤可见的节点和边,并重新计算布局 const { nodes, edges } = useMemo(() => { const hiddenNodes = new Set(); // 收集所有被折叠节点的后代 collapsedNodes.forEach(collapsedId => { const descendants = getDescendants(collapsedId); descendants.forEach(id => hiddenNodes.add(id)); }); const visibleNodes = initialNodes .filter(node => !hiddenNodes.has(node.id)) .map(node => ({ ...node, data: { ...node.data, isCollapsed: collapsedNodes.has(node.id), hasChildren: initialEdges.some(e => e.source === node.id), onToggleCollapse: () => toggleNodeCollapse(node.id), isHighlighted: selectedNodeId === node.id, } })); const visibleEdges = initialEdges.filter( edge => !hiddenNodes.has(edge.source) && !hiddenNodes.has(edge.target) ); // 重新计算布局 - 只对可见节点 if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') { try { const dagreGraph = new window.dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); const nodeWidth = 280; const nodeHeight = 180; dagreGraph.setGraph({ rankdir: 'LR', nodesep: 80, ranksep: 300, }); visibleNodes.forEach((node) => { dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); }); visibleEdges.forEach((edge) => { dagreGraph.setEdge(edge.source, edge.target); }); window.dagre.layout(dagreGraph); visibleNodes.forEach((node) => { const nodeWithPosition = dagreGraph.node(node.id); if (nodeWithPosition) { node.position = { x: nodeWithPosition.x - nodeWidth / 2, y: nodeWithPosition.y - nodeHeight / 2, }; node.targetPosition = 'left'; node.sourcePosition = 'right'; } }); console.log('✅ Dynamic layout recalculated for', visibleNodes.length, 'visible nodes'); } catch (error) { console.error('❌ Error in dynamic layout:', error); } } return { nodes: visibleNodes, edges: visibleEdges }; }, [initialNodes, initialEdges, collapsedNodes, getDescendants, toggleNodeCollapse, selectedNodeId]); // 构建树形结构 - 允许一个节点有多个父节点 const buildTree = useCallback(() => { const nodeMap = new Map(); initialNodes.forEach(node => { nodeMap.set(node.id, node); }); // 为每个节点创建树节点的副本(允许多次出现) const createTreeNode = (nodeId, pathKey) => { const node = nodeMap.get(nodeId); if (!node) return null; return { ...node, treeKey: pathKey, // 唯一的树路径key,用于React key children: [] }; }; // 构建父子关系映射:记录每个节点的所有父节点,去重边 const parentToChildren = new Map(); const childToParents = new Map(); initialEdges.forEach(edge => { // 记录父->子关系(去重:同一个父节点到同一个子节点只记录一次) if (!parentToChildren.has(edge.source)) { parentToChildren.set(edge.source, []); } const children = parentToChildren.get(edge.source); if (!children.includes(edge.target)) { children.push(edge.target); } // 记录子->父关系(用于判断是否有多个父节点,也去重) if (!childToParents.has(edge.target)) { childToParents.set(edge.target, []); } const parents = childToParents.get(edge.target); if (!parents.includes(edge.source)) { parents.push(edge.source); } }); // 递归构建树 const buildSubtree = (nodeId, pathKey, visitedInPath) => { // 避免循环引用:如果当前路径中已经访问过这个节点,跳过 if (visitedInPath.has(nodeId)) { return null; } const treeNode = createTreeNode(nodeId, pathKey); if (!treeNode) return null; const newVisitedInPath = new Set(visitedInPath); newVisitedInPath.add(nodeId); const children = parentToChildren.get(nodeId) || []; treeNode.children = children .map((childId, index) => buildSubtree(childId, pathKey + '-' + childId + '-' + index, newVisitedInPath)) .filter(child => child !== null); return treeNode; }; // 找出所有根节点(没有入边的节点) const hasParent = new Set(); initialEdges.forEach(edge => { hasParent.add(edge.target); }); const roots = []; initialNodes.forEach((node, index) => { if (!hasParent.has(node.id)) { const treeNode = buildSubtree(node.id, 'root-' + node.id + '-' + index, new Set()); if (treeNode) roots.push(treeNode); } }); return roots; }, [initialNodes, initialEdges]); const treeRoots = useMemo(() => buildTree(), [buildTree]); // 初始化树节点折叠状态 useEffect(() => { const getAllTreeKeys = (nodes) => { const keys = new Set(); const traverse = (node) => { if (node.children && node.children.length > 0) { // 排除根节点 if (node.data.level !== 0) { keys.add(node.treeKey); } node.children.forEach(traverse); } }; nodes.forEach(traverse); return keys; }; setCollapsedTreeNodes(getAllTreeKeys(treeRoots)); }, [treeRoots]); const renderTree = useCallback((treeNodes, level = 0) => { return treeNodes.map(node => { // 使用 treeKey 来区分树中的不同实例 const isCollapsed = collapsedTreeNodes.has(node.treeKey); const isSelected = selectedNodeId === node.id; return ( { setCollapsedTreeNodes(prev => { const newSet = new Set(prev); if (newSet.has(node.treeKey)) { newSet.delete(node.treeKey); } else { newSet.add(node.treeKey); } return newSet; }); }} onSelect={() => { const nodeId = node.id; setSelectedNodeId(nodeId); // 展开所有祖先节点 const ancestorIds = []; const findAncestors = (id) => { initialEdges.forEach(edge => { if (edge.target === id) { ancestorIds.push(edge.source); findAncestors(edge.source); } }); }; findAncestors(nodeId); setCollapsedNodes(prev => { const newSet = new Set(prev); ancestorIds.forEach(id => newSet.delete(id)); return newSet; }); // 延迟聚焦,等待节点展开和布局重新计算 setTimeout(() => { fitView({ nodes: [{ id: nodeId }], duration: 800, padding: 0.3, }); }, 300); }} > {node.children && node.children.length > 0 && renderTree(node.children, level + 1)} ); }); }, [collapsedTreeNodes, selectedNodeId, nodes, setCenter, initialEdges, setCollapsedNodes, fitView]); console.log('📊 Rendering with', nodes.length, 'visible nodes and', edges.length, 'visible edges'); if (nodes.length === 0) { return (
ERROR: No nodes to display!
); } return (
{/* 顶部标题栏 */}

查询图可视化

📊 {nodes.length} 节点 🔗 {edges.length} 连线
{/* 左侧目录树 */}
节点目录
{renderTree(treeRoots)}
{/* 右侧图例 */}

图例

节点类型
查询节点
笔记节点
策略类型
direct_sug
rewrite_synonym
add_word
rewrite_abstract
query_to_note
💡 点击节点选中并高亮
{/* React Flow 画布 */}
{ setSelectedNodeId(clickedNode.id); }} >
); } function App() { return ( ); } const root = createRoot(document.getElementById('root')); root.render(); `; fs.writeFileSync(reactComponentPath, reactComponent); // 使用 esbuild 打包 console.log('🎨 Building modern visualization...'); build({ entryPoints: [reactComponentPath], bundle: true, outfile: path.join(__dirname, 'bundle_v2.js'), format: 'iife', loader: { '.css': 'css', }, minify: false, sourcemap: 'inline', }).then(() => { // 读取打包后的 JS const bundleJs = fs.readFileSync(path.join(__dirname, 'bundle_v2.js'), 'utf-8'); // 读取 CSS const cssPath = path.join(__dirname, 'node_modules/@xyflow/react/dist/style.css'); const css = fs.readFileSync(cssPath, 'utf-8'); // 生成最终 HTML const html = ` 查询图可视化
`; // 写入输出文件 fs.writeFileSync(outputFile, html); // 清理临时文件 fs.unlinkSync(reactComponentPath); fs.unlinkSync(path.join(__dirname, 'bundle_v2.js')); console.log('✅ Visualization generated: ' + outputFile); console.log('📊 Nodes: ' + Object.keys(data.nodes).length); console.log('🔗 Edges: ' + data.edges.length); }).catch(error => { console.error('❌ Build error:', error); process.exit(1); });