#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { build } = require('esbuild'); const { convertV8ToGraph } = require('./convert_v8_to_graph'); const { convertV8ToGraphV2 } = require('./convert_v8_to_graph_v2'); // 读取命令行参数 const args = process.argv.slice(2); if (args.length === 0) { console.error('Usage: node index.js [output.html]'); process.exit(1); } const inputFile = args[0]; const outputFile = args[1] || 'query_graph_output.html'; // 读取输入数据 const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8')); // 检测数据格式并转换 let data; if (inputData.rounds && inputData.o) { // v6.1.2.8 格式,需要转换 console.log('✨ 检测到 v6.1.2.8 格式,正在转换为图结构...'); // 尝试读取 search_results.json let searchResults = null; const searchResultsPath = path.join(path.dirname(inputFile), 'search_results.json'); if (fs.existsSync(searchResultsPath)) { console.log('📄 读取搜索结果数据...'); searchResults = JSON.parse(fs.readFileSync(searchResultsPath, 'utf-8')); } // 使用新的转换函数(按 Round > 步骤 > 数据 组织) const graphData = convertV8ToGraphV2(inputData, searchResults); data = { nodes: graphData.nodes, edges: graphData.edges, iterations: graphData.iterations }; console.log(`✅ 转换完成: ${Object.keys(data.nodes).length} 个节点, ${data.edges.length} 条边`); } else if (inputData.nodes && inputData.edges) { // v6.1.2.5 格式,直接使用 console.log('✨ 检测到 v6.1.2.5 格式,直接使用'); data = inputData; } else { console.error('❌ 无法识别的数据格式'); process.exit(1); } // 创建临时 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 getNodeTypeColor(type) { const typeColors = { 'root': '#6b21a8', // 紫色 - 根节点 'round': '#7c3aed', // 深紫 - Round节点 'step': '#f59e0b', // 橙色 - 步骤节点 'seg': '#10b981', // 绿色 - 分词 'q': '#3b82f6', // 蓝色 - Query 'sug': '#06b6d4', // 青色 - Sug建议词 'seed': '#84cc16', // 黄绿 - Seed 'add_word': '#22c55e', // 绿色 - 加词生成 'search_word': '#8b5cf6', // 紫色 - 搜索词 'post': '#ec4899', // 粉色 - 帖子 'filtered_sug': '#14b8a6',// 青绿 - 筛选的sug 'next_q': '#2563eb', // 深蓝 - 下轮查询 'next_seed': '#65a30d', // 深黄绿 - 下轮种子 'search': '#8b5cf6', // 深紫 - 搜索(兼容旧版) 'operation': '#f59e0b', // 橙色 - 操作节点(兼容旧版) 'query': '#3b82f6', // 蓝色 - 查询(兼容旧版) 'note': '#ec4899', // 粉色 - 帖子(兼容旧版) }; return typeColors[type] || '#9ca3af'; } // 查询节点组件 - 卡片样式 function QueryNode({ id, data, sourcePosition, targetPosition }) { // 所有节点默认展开 const expanded = true; // 获取节点类型颜色 const typeColor = getNodeTypeColor(data.nodeType || 'query'); return (
{/* 折叠当前节点按钮 - 左边 */}
{ e.stopPropagation(); if (data.onHideSelf) { data.onHideSelf(); } }} onMouseEnter={(e) => { e.currentTarget.style.background = '#d97706'; }} onMouseLeave={(e) => { e.currentTarget.style.background = '#f59e0b'; }} title="隐藏当前节点" > ×
{/* 聚焦按钮 - 右上角 */}
{ e.stopPropagation(); if (data.onFocus) { data.onFocus(); } }} onMouseEnter={(e) => { if (!data.isFocused) { e.currentTarget.style.background = '#d1d5db'; } }} onMouseLeave={(e) => { if (!data.isFocused) { e.currentTarget.style.background = '#e5e7eb'; } }} title={data.isFocused ? '取消聚焦' : '聚焦到此节点'} > 🎯
{/* 折叠/展开子节点按钮 - 右边第二个位置 */} {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 [currentImageIndex, setCurrentImageIndex] = useState(0); const expanded = true; const hasImages = data.imageList && data.imageList.length > 0; const nextImage = (e) => { e.stopPropagation(); if (hasImages) { setCurrentImageIndex((prev) => (prev + 1) % data.imageList.length); } }; const prevImage = (e) => { e.stopPropagation(); if (hasImages) { setCurrentImageIndex((prev) => (prev - 1 + data.imageList.length) % data.imageList.length); } }; return (
{/* 笔记标题 */}
{data.title}
{/* 轮播图 */} {hasImages && (
{\`Image { e.target.style.display = 'none'; }} /> {data.imageList.length > 1 && ( <> {/* 左右切换按钮 */} {/* 图片计数 */}
{currentImageIndex + 1}/{data.imageList.length}
)}
)} {/* 互动数据 */} {data.interact_info && (
{data.interact_info.liked_count > 0 && ( ❤️ {data.interact_info.liked_count} )} {data.interact_info.collected_count > 0 && ( ⭐ {data.interact_info.collected_count} )} {data.interact_info.comment_count > 0 && ( 💬 {data.interact_info.comment_count} )} {data.interact_info.shared_count > 0 && ( 🔗 {data.interact_info.shared_count} )}
)} {/* 标签 */} {(data.matchLevel || data.score) && (
{data.matchLevel && ( {data.matchLevel} )} {data.score && ( Score: {data.score} )}
)} {/* 描述 */} {expanded && data.description && (
{data.description}
)} {/* 评估理由 */} {expanded && data.evaluationReason && (
评估:
{data.evaluationReason}
)}
); } const nodeTypes = { query: QueryNode, note: NoteNode, }; // 根据 score 获取颜色 function getScoreColor(score) { if (score >= 0.7) return '#10b981'; // 绿色 - 高分 if (score >= 0.4) return '#f59e0b'; // 橙色 - 中分 return '#ef4444'; // 红色 - 低分 } // 截断文本,保留头尾,中间显示省略号 function truncateMiddle(text, maxLength = 20) { if (!text || text.length <= maxLength) return text; const headLength = Math.ceil(maxLength * 0.4); const tailLength = Math.floor(maxLength * 0.4); const head = text.substring(0, headLength); const tail = text.substring(text.length - tailLength); return \`\${head}...\${tail}\`; } // 根据策略获取颜色 function getStrategyColor(strategy) { const strategyColors = { '初始分词': '#10b981', '调用sug': '#06b6d4', '同义改写': '#f59e0b', '加词': '#3b82f6', '抽象改写': '#8b5cf6', '基于部分匹配改进': '#ec4899', '结果分支-抽象改写': '#a855f7', '结果分支-同义改写': '#fb923c', // v6.1.2.8 新增策略 '原始问题': '#6b21a8', '来自分词': '#10b981', '加词生成': '#ef4444', '建议词': '#06b6d4', '执行搜索': '#8b5cf6', }; return strategyColors[strategy] || '#9ca3af'; } // 树节点组件 function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, onSelect }) { const hasChildren = children && children.length > 0; const score = node.data.score ? parseFloat(node.data.score) : 0; const strategy = node.data.strategy || ''; const strategyColor = getStrategyColor(strategy); const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型 return (
{ if (!isSelected) e.currentTarget.style.background = '#f9fafb'; }} onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.background = 'transparent'; }} > {/* 策略类型竖线 */}
{hasChildren && ( { e.stopPropagation(); onToggle(); }} > {isCollapsed ? '▶' : '▼'} )} {!hasChildren && }
{truncateMiddle(node.data.title || node.id, 18)}
{/* 分数显示 - 步骤和轮次节点不显示分数 */} {nodeActualType !== 'step' && nodeActualType !== 'round' && ( {score.toFixed(2)} )}
{/* 分数下划线 - 步骤和轮次节点不显示 */} {nodeActualType !== 'step' && nodeActualType !== 'round' && (
)}
{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 isHorizontal = direction === 'LR'; dagreGraph.setGraph({ rankdir: direction, nodesep: 120, // 垂直间距 - 增加以避免节点重叠 ranksep: 280, // 水平间距 - 增加以容纳更宽的节点 }); // 添加节点 - 根据节点类型设置不同的尺寸 nodes.forEach((node) => { let nodeWidth = 280; let nodeHeight = 180; // note 节点有轮播图,需要更大的空间 if (node.type === 'note') { nodeWidth = 320; nodeHeight = 350; // 增加高度以容纳轮播图 } 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'; // 根据节点类型获取尺寸 let nodeWidth = 280; let nodeHeight = 180; if (node.type === 'note') { nodeWidth = 320; nodeHeight = 350; } // 将 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 = []; const originalIdToCanvasId = {}; // 原始ID -> 画布ID的映射 const canvasIdToNodeData = {}; // 避免重复创建相同的节点 // 创建节点 Object.entries(data.nodes).forEach(([originalId, node]) => { // 统一处理所有类型的节点 const nodeType = node.type || 'query'; // 直接使用originalId作为canvasId,避免冲突 const canvasId = originalId; originalIdToCanvasId[originalId] = canvasId; // 如果这个 canvasId 还没有创建过节点,则创建 if (!canvasIdToNodeData[canvasId]) { canvasIdToNodeData[canvasId] = true; // 根据节点类型创建不同的数据结构 if (nodeType === 'note' || nodeType === 'post') { nodes.push({ id: canvasId, originalId: originalId, type: 'note', data: { title: node.query || node.title || '帖子', matchLevel: node.match_level, score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00', description: node.body_text || node.desc || '', isSelected: node.is_selected !== undefined ? node.is_selected : true, imageList: node.image_list || [], noteUrl: node.note_url || '', evaluationReason: node.evaluation_reason || '', interact_info: node.interact_info || {}, nodeType: nodeType, }, position: { x: 0, y: 0 }, }); } else { // query, seg, q, search, root 等节点 let displayTitle = node.query || originalId; nodes.push({ id: canvasId, originalId: originalId, type: 'query', // 使用 query 组件渲染所有非note节点 data: { title: displayTitle, level: node.level || 0, score: node.relevance_score ? node.relevance_score.toFixed(2) : '0.00', strategy: node.strategy || '', parent: node.parent_query || '', isSelected: node.is_selected !== undefined ? node.is_selected : true, evaluationReason: node.evaluation_reason || '', nodeType: nodeType, // 传递实际节点类型用于样式 searchCount: node.search_count, // search 节点特有 totalPosts: node.total_posts, // search 节点特有 }, position: { x: 0, y: 0 }, }); } } }); // 创建边 - 使用虚线样式,映射到画布ID data.edges.forEach((edge, index) => { const edgeColors = { '初始分词': '#10b981', '调用sug': '#06b6d4', '同义改写': '#f59e0b', '加词': '#3b82f6', '抽象改写': '#8b5cf6', '基于部分匹配改进': '#ec4899', '结果分支-抽象改写': '#a855f7', '结果分支-同义改写': '#fb923c', '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: originalIdToCanvasId[edge.from], // 使用画布ID target: originalIdToCanvasId[edge.to], // 使用画布ID type: 'simplebezier', // 使用简单贝塞尔曲线 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); const [hiddenNodes, setHiddenNodes] = useState(new Set()); // 用户手动隐藏的节点 const [focusMode, setFocusMode] = useState(false); // 全局聚焦模式,默认关闭 const [focusedNodeId, setFocusedNodeId] = useState(null); // 单独聚焦的节点ID // 获取 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 nodesToHide = new Set(); // 判断使用哪个节点ID进行聚焦:优先使用单独聚焦的节点,否则使用全局聚焦模式的选中节点 const effectiveFocusNodeId = focusedNodeId || (focusMode ? selectedNodeId : null); // 聚焦模式:只显示聚焦节点、其父节点和直接子节点 if (effectiveFocusNodeId) { const visibleInFocus = new Set([effectiveFocusNodeId]); // 添加所有父节点 initialEdges.forEach(edge => { if (edge.target === effectiveFocusNodeId) { visibleInFocus.add(edge.source); } }); // 添加所有直接子节点 initialEdges.forEach(edge => { if (edge.source === effectiveFocusNodeId) { visibleInFocus.add(edge.target); } }); // 隐藏不在聚焦范围内的节点 initialNodes.forEach(node => { if (!visibleInFocus.has(node.id)) { nodesToHide.add(node.id); } }); } else { // 非聚焦模式:使用原有的折叠逻辑 // 收集所有被折叠节点的后代 collapsedNodes.forEach(collapsedId => { const descendants = getDescendants(collapsedId); descendants.forEach(id => nodesToHide.add(id)); }); } // 添加用户手动隐藏的节点 hiddenNodes.forEach(id => nodesToHide.add(id)); const visibleNodes = initialNodes .filter(node => !nodesToHide.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), onHideSelf: () => { setHiddenNodes(prev => { const newSet = new Set(prev); newSet.add(node.id); return newSet; }); }, onFocus: () => { // 切换聚焦状态 if (focusedNodeId === node.id) { setFocusedNodeId(null); // 如果已经聚焦,则取消聚焦 } else { // 先取消之前的聚焦,然后聚焦到当前节点 setFocusedNodeId(node.id); // 延迟聚焦视图到该节点 setTimeout(() => { fitView({ nodes: [{ id: node.id }], duration: 800, padding: 0.3, }); }, 100); } }, isFocused: focusedNodeId === node.id, isHighlighted: selectedNodeId === node.id, } })); const visibleEdges = initialEdges.filter( edge => !nodesToHide.has(edge.source) && !nodesToHide.has(edge.target) ); // 重新计算布局 - 只对可见节点 if (typeof window !== 'undefined' && typeof window.dagre !== 'undefined') { try { const dagreGraph = new window.dagre.graphlib.Graph(); dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setGraph({ rankdir: 'LR', nodesep: 120, // 垂直间距 - 增加以避免节点重叠 ranksep: 280, // 水平间距 - 增加以容纳更宽的节点 }); visibleNodes.forEach((node) => { let nodeWidth = 280; let nodeHeight = 180; // note 节点有轮播图,需要更大的空间 if (node.type === 'note') { nodeWidth = 320; nodeHeight = 350; } 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) { // 根据节点类型获取对应的尺寸 let nodeWidth = 280; let nodeHeight = 180; if (node.type === 'note') { nodeWidth = 320; nodeHeight = 350; } 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, hiddenNodes, focusMode, focusedNodeId, 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]); // 生成树形文本结构 const generateTreeText = useCallback(() => { const lines = []; // 递归生成树形文本 const traverse = (nodes, prefix = '', isLast = true, depth = 0) => { nodes.forEach((node, index) => { const isLastNode = index === nodes.length - 1; const nodeData = initialNodes.find(n => n.id === node.id)?.data || {}; const nodeType = nodeData.nodeType || node.data?.nodeType || 'unknown'; const title = nodeData.title || node.data?.title || node.id; // 优先从node.data获取score,然后从nodeData获取 let score = null; if (node.data?.score !== undefined && node.data?.score !== null) { score = node.data.score; } else if (node.data?.relevance_score !== undefined && node.data?.relevance_score !== null) { score = node.data.relevance_score; } else if (nodeData.score !== undefined && nodeData.score !== null) { score = nodeData.score; } else if (nodeData.relevance_score !== undefined && nodeData.relevance_score !== null) { score = nodeData.relevance_score; } const strategy = nodeData.strategy || node.data?.strategy || ''; // 构建当前行 - score可能是数字或字符串,step/round节点不显示分数 const connector = isLastNode ? '└─' : '├─'; let scoreText = ''; if (nodeType !== 'step' && nodeType !== 'round' && score !== null && score !== undefined) { // score可能已经是字符串格式(如 "0.05"),也可能是数字 const scoreStr = typeof score === 'number' ? score.toFixed(2) : score; scoreText = \` (分数: \${scoreStr})\`; } const strategyText = strategy ? \` [\${strategy}]\` : ''; lines.push(\`\${prefix}\${connector} \${title}\${scoreText}\${strategyText}\`); // 递归处理子节点 if (node.children && node.children.length > 0) { const childPrefix = prefix + (isLastNode ? ' ' : '│ '); traverse(node.children, childPrefix, isLastNode, depth + 1); } }); }; // 添加标题 const rootNode = initialNodes.find(n => n.data?.level === 0); if (rootNode) { lines.push(\`📊 查询扩展树形结构\`); lines.push(\`原始问题: \${rootNode.data.title || rootNode.data.query}\`); lines.push(''); } traverse(treeRoots); return lines.join('\\n'); }, [treeRoots, initialNodes]); // 复制树形结构到剪贴板 const copyTreeToClipboard = useCallback(async () => { try { const treeText = generateTreeText(); await navigator.clipboard.writeText(treeText); alert('✅ 树形结构已复制到剪贴板!'); } catch (err) { console.error('复制失败:', err); alert('❌ 复制失败,请手动复制'); } }, [generateTreeText]); // 初始化树节点折叠状态 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; // 展开所有祖先节点 const ancestorIds = [nodeId]; const findAncestors = (id) => { initialEdges.forEach(edge => { if (edge.target === id && !ancestorIds.includes(edge.source)) { ancestorIds.push(edge.source); findAncestors(edge.source); } }); }; findAncestors(nodeId); // 如果节点或其祖先被隐藏,先恢复它们 setHiddenNodes(prev => { const newSet = new Set(prev); ancestorIds.forEach(id => newSet.delete(id)); return newSet; }); setSelectedNodeId(nodeId); // 获取选中节点的直接子节点 const childrenIds = []; initialEdges.forEach(edge => { if (edge.source === nodeId) { childrenIds.push(edge.target); } }); setCollapsedNodes(prev => { const newSet = new Set(prev); // 展开所有祖先节点 ancestorIds.forEach(id => newSet.delete(id)); // 展开选中节点本身 newSet.delete(nodeId); // 展开选中节点的直接子节点 childrenIds.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 (
{/* 顶部面包屑导航栏 */}
{selectedNodeId ? (
{/* 面包屑导航 - 显示所有路径 */} {(() => { const selectedNode = nodes.find(n => n.id === selectedNodeId); if (!selectedNode) return null; // 找到所有从根节点到当前节点的路径 const findAllPaths = (targetId) => { const paths = []; const buildPath = (nodeId, currentPath) => { const node = initialNodes.find(n => n.id === nodeId); if (!node) return; const newPath = [node, ...currentPath]; // 找到所有父节点 const parents = initialEdges.filter(e => e.target === nodeId).map(e => e.source); if (parents.length === 0) { // 到达根节点 paths.push(newPath); } else { // 递归处理所有父节点 parents.forEach(parentId => { buildPath(parentId, newPath); }); } }; buildPath(targetId, []); return paths; }; const allPaths = findAllPaths(selectedNodeId); // 去重:将路径转换为字符串进行比较 const uniquePaths = []; const pathStrings = new Set(); allPaths.forEach(path => { const pathString = path.map(n => n.id).join('->'); if (!pathStrings.has(pathString)) { pathStrings.add(pathString); uniquePaths.push(path); } }); return (
{uniquePaths.map((path, pathIndex) => (
{pathIndex > 0 && } {path.map((node, index) => { // 获取节点的 score、strategy 和 isSelected const nodeScore = node.data.score ? parseFloat(node.data.score) : 0; const nodeStrategy = node.data.strategy || ''; const strategyColor = getStrategyColor(nodeStrategy); const nodeIsSelected = node.type === 'note' ? node.data.matchLevel !== 'unsatisfied' : node.data.isSelected !== false; const nodeActualType = node.data.nodeType || node.type; // 获取实际节点类型 return ( { const nodeId = node.id; // 找到所有祖先节点 const ancestorIds = [nodeId]; const findAncestors = (id) => { initialEdges.forEach(edge => { if (edge.target === id && !ancestorIds.includes(edge.source)) { ancestorIds.push(edge.source); findAncestors(edge.source); } }); }; findAncestors(nodeId); // 如果节点或其祖先被隐藏,先恢复它们 setHiddenNodes(prev => { const newSet = new Set(prev); ancestorIds.forEach(id => newSet.delete(id)); return newSet; }); // 展开目录树中到达该节点的路径 // 需要找到所有包含该节点的树路径的 treeKey,并展开它们的父节点 setCollapsedTreeNodes(prev => { const newSet = new Set(prev); // 清空所有折叠状态,让目录树完全展开到选中节点 // 这样可以确保选中节点在目录中可见 return new Set(); }); setSelectedNodeId(nodeId); setTimeout(() => { fitView({ nodes: [{ id: nodeId }], duration: 800, padding: 0.3, }); }, 100); }} style={{ padding: '6px 8px', borderRadius: '4px', background: 'white', border: index === path.length - 1 ? '2px solid #3b82f6' : '1px solid #d1d5db', color: '#374151', fontWeight: index === path.length - 1 ? '600' : '400', width: '180px', cursor: 'pointer', transition: 'all 0.2s ease', position: 'relative', display: 'inline-flex', flexDirection: 'column', gap: '4px', }} onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.8'; }} onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }} title={\`\${node.data.title || node.id} (Score: \${nodeScore.toFixed(2)}, Strategy: \${nodeStrategy}, Selected: \${nodeIsSelected})\`} > {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
{/* 策略类型竖线 */}
{/* 节点文字 */} {truncateMiddle(node.data.title || node.id, 18)} {/* 分数显示 - 步骤和轮次节点不显示分数 */} {nodeActualType !== 'step' && nodeActualType !== 'round' && ( {nodeScore.toFixed(2)} )}
{/* 分数下划线 - 步骤和轮次节点不显示 */} {nodeActualType !== 'step' && nodeActualType !== 'round' && (
)} {index < path.length - 1 && } )})}
))}
); })()}
) : (
选择一个节点查看路径
)}
{/* 主内容区:目录 + 画布 */}
{/* 左侧目录树 */}
节点目录
{renderTree(treeRoots)}
{/* 画布区域 */}
{/* 右侧图例 */}

图例

{/* 画布节点展开/折叠控制 */}
节点控制
策略类型
初始分词
调用sug
同义改写
加词
抽象改写
基于部分匹配改进
结果分支-抽象改写
结果分支-同义改写
💡 点击节点左上角 × 隐藏节点
{/* 隐藏节点列表 - 在图例内部 */} {hiddenNodes.size > 0 && (

已隐藏节点

{Array.from(hiddenNodes).map(nodeId => { const node = initialNodes.find(n => n.id === nodeId); if (!node) return null; return (
{node.data.title || nodeId}
); })}
)}
{/* 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', // 强制所有 React 引用指向同一个位置,避免多副本 alias: { 'react': path.join(__dirname, 'node_modules/react'), 'react-dom': path.join(__dirname, 'node_modules/react-dom'), 'react/jsx-runtime': path.join(__dirname, 'node_modules/react/jsx-runtime'), 'react/jsx-dev-runtime': path.join(__dirname, 'node_modules/react/jsx-dev-runtime'), }, }).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); });