Jelajahi Sumber

Remove old visualization script and cleanup temporary files

- Remove visualize_v2.js (replaced by visualization/sug_v6_1_2_5/index.js)
- Cleaned up temporary test files:
  - 37 HTML test files
  - 4 CSS bundle files
  - 5 old JS visualization scripts
  - 3 old Python visualization scripts
  - Root node_modules/ (29MB)
  - Root package.json and package-lock.json
- All visualization dependencies now in visualization/sug_v6_1_2_5/

🤖 Generated with Claude Code
yangxiaohui 1 bulan lalu
induk
melakukan
99d41b48f7
1 mengubah file dengan 0 tambahan dan 1951 penghapusan
  1. 0 1951
      visualize_v2.js

+ 0 - 1951
visualize_v2.js

@@ -1,1951 +0,0 @@
-#!/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 <path-to-query_graph.json> [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 (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: '#667eea', width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '12px',
-          borderRadius: '8px',
-          border: data.isHighlighted ? '3px solid #667eea' :
-                  data.isCollapsed ? '2px solid #667eea' :
-                  data.isSelected === false ? '2px dashed #d1d5db' :
-                  data.level === 0 ? '2px solid #8b5cf6' : '1px solid #e5e7eb',
-          background: data.isHighlighted ? '#eef2ff' :
-                      data.isSelected === false ? '#f9fafb' : 'white',
-          minWidth: '200px',
-          maxWidth: '280px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(102, 126, 234, 0.25), 0 4px 16px rgba(102, 126, 234, 0.4)' :
-                     data.isCollapsed ? '0 4px 12px rgba(102, 126, 234, 0.15)' :
-                     data.level === 0 ? '0 4px 12px rgba(139, 92, 246, 0.15)' : '0 2px 6px rgba(0, 0, 0, 0.06)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-          position: 'relative',
-          opacity: data.isSelected === false ? 0.6 : 1,
-        }}
-      >
-        {/* 折叠当前节点按钮 - 左边 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            left: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: '#f59e0b',
-            color: 'white',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            e.stopPropagation();
-            if (data.onHideSelf) {
-              data.onHideSelf();
-            }
-          }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.background = '#d97706';
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.background = '#f59e0b';
-          }}
-          title="隐藏当前节点"
-        >
-          ×
-        </div>
-
-        {/* 聚焦按钮 - 右上角 */}
-        <div
-          style={{
-            position: 'absolute',
-            top: '6px',
-            right: '6px',
-            width: '20px',
-            height: '20px',
-            borderRadius: '50%',
-            background: data.isFocused ? '#10b981' : '#e5e7eb',
-            color: data.isFocused ? 'white' : '#6b7280',
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            fontSize: '11px',
-            fontWeight: 'bold',
-            cursor: 'pointer',
-            transition: 'all 0.2s ease',
-            zIndex: 10,
-          }}
-          onClick={(e) => {
-            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 ? '取消聚焦' : '聚焦到此节点'}
-        >
-          🎯
-        </div>
-
-        {/* 折叠/展开子节点按钮 - 右边第二个位置 */}
-        {data.hasChildren && (
-          <div
-            style={{
-              position: 'absolute',
-              top: '6px',
-              right: '30px',
-              width: '20px',
-              height: '20px',
-              borderRadius: '50%',
-              background: data.isCollapsed ? '#667eea' : '#e5e7eb',
-              color: data.isCollapsed ? 'white' : '#6b7280',
-              display: 'flex',
-              alignItems: 'center',
-              justifyContent: 'center',
-              fontSize: '11px',
-              fontWeight: 'bold',
-              cursor: 'pointer',
-              transition: 'all 0.2s ease',
-              zIndex: 10,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              data.onToggleCollapse();
-            }}
-            title={data.isCollapsed ? '展开子节点' : '折叠子节点'}
-          >
-            {data.isCollapsed ? '+' : '−'}
-          </div>
-        )}
-
-        {/* 卡片内容 */}
-        <div>
-          {/* 标题行 */}
-          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px', paddingLeft: '24px', paddingRight: data.hasChildren ? '54px' : '28px' }}>
-            <div style={{ flex: 1 }}>
-              <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: '3px' }}>
-                <div style={{
-                  fontSize: '13px',
-                  fontWeight: data.level === 0 ? '700' : '600',
-                  color: data.level === 0 ? '#6b21a8' : '#1f2937',
-                  lineHeight: '1.3',
-                  flex: 1,
-                }}>
-                  {data.title}
-                </div>
-                {data.isSelected === false && (
-                  <div style={{
-                    fontSize: '9px',
-                    padding: '1px 4px',
-                    borderRadius: '3px',
-                    background: '#fee2e2',
-                    color: '#991b1b',
-                    fontWeight: '500',
-                    flexShrink: 0,
-                  }}>
-                    未选中
-                  </div>
-                )}
-              </div>
-            </div>
-          </div>
-
-        {/* 展开的详细信息 - 始终显示 */}
-        <div style={{ fontSize: '11px', lineHeight: 1.4 }}>
-            <div style={{ display: 'flex', gap: '4px', marginBottom: '6px', flexWrap: 'wrap' }}>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#eff6ff',
-                color: '#3b82f6',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                Lv.{data.level}
-              </span>
-              <span style={{
-                display: 'inline-block',
-                padding: '1px 6px',
-                borderRadius: '10px',
-                background: '#f0fdf4',
-                color: '#16a34a',
-                fontSize: '10px',
-                fontWeight: '500',
-              }}>
-                {data.score}
-              </span>
-              {data.strategy && data.strategy !== 'root' && (
-                <span style={{
-                  display: 'inline-block',
-                  padding: '1px 6px',
-                  borderRadius: '10px',
-                  background: '#fef3c7',
-                  color: '#92400e',
-                  fontSize: '10px',
-                  fontWeight: '500',
-                }}>
-                  {data.strategy}
-                </span>
-              )}
-            </div>
-
-            {data.parent && (
-              <div style={{ color: '#6b7280', fontSize: '10px', marginTop: '4px', paddingTop: '4px', borderTop: '1px solid #f3f4f6' }}>
-                <strong>Parent:</strong> {data.parent}
-              </div>
-            )}
-            {data.evaluationReason && (
-              <div style={{
-                marginTop: '6px',
-                paddingTop: '6px',
-                borderTop: '1px solid #f3f4f6',
-                fontSize: '10px',
-                color: '#6b7280',
-                lineHeight: '1.5',
-              }}>
-                <strong style={{ color: '#4b5563' }}>评估:</strong>
-                <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-              </div>
-            )}
-          </div>
-        </div>
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#667eea', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-// 笔记节点组件 - 卡片样式,带轮播图
-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 (
-    <div>
-      <Handle
-        type="target"
-        position={targetPosition || Position.Left}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-      <div
-        style={{
-          padding: '14px',
-          borderRadius: '20px',
-          border: data.isHighlighted ? '3px solid #ec4899' : '2px solid #fce7f3',
-          background: data.isHighlighted ? '#eef2ff' : 'white',
-          minWidth: '220px',
-          maxWidth: '300px',
-          boxShadow: data.isHighlighted ? '0 0 0 4px rgba(236, 72, 153, 0.25), 0 4px 16px rgba(236, 72, 153, 0.4)' : '0 4px 12px rgba(236, 72, 153, 0.15)',
-          transition: 'all 0.3s ease',
-          cursor: 'pointer',
-        }}
-      >
-        {/* 笔记图标和标题 */}
-        <div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: '8px' }}>
-          <span style={{ fontSize: '16px', marginRight: '8px' }}>📝</span>
-          <div style={{ flex: 1 }}>
-            <div style={{
-              fontSize: '13px',
-              fontWeight: '600',
-              color: '#831843',
-              lineHeight: '1.4',
-              marginBottom: '4px',
-            }}>
-              {data.title}
-            </div>
-          </div>
-        </div>
-
-        {/* 轮播图 */}
-        {hasImages && (
-          <div style={{
-            position: 'relative',
-            marginBottom: '8px',
-            borderRadius: '12px',
-            overflow: 'hidden',
-          }}>
-            <img
-              src={data.imageList[currentImageIndex].image_url}
-              alt={\`Image \${currentImageIndex + 1}\`}
-              style={{
-                width: '100%',
-                height: '160px',
-                objectFit: 'cover',
-                display: 'block',
-              }}
-              onError={(e) => {
-                e.target.style.display = 'none';
-              }}
-            />
-            {data.imageList.length > 1 && (
-              <>
-                {/* 左右切换按钮 */}
-                <button
-                  onClick={prevImage}
-                  style={{
-                    position: 'absolute',
-                    left: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ‹
-                </button>
-                <button
-                  onClick={nextImage}
-                  style={{
-                    position: 'absolute',
-                    right: '4px',
-                    top: '50%',
-                    transform: 'translateY(-50%)',
-                    background: 'rgba(0, 0, 0, 0.5)',
-                    color: 'white',
-                    border: 'none',
-                    borderRadius: '50%',
-                    width: '24px',
-                    height: '24px',
-                    cursor: 'pointer',
-                    display: 'flex',
-                    alignItems: 'center',
-                    justifyContent: 'center',
-                    fontSize: '14px',
-                  }}
-                >
-                  ›
-                </button>
-                {/* 图片计数 */}
-                <div style={{
-                  position: 'absolute',
-                  bottom: '4px',
-                  right: '4px',
-                  background: 'rgba(0, 0, 0, 0.6)',
-                  color: 'white',
-                  padding: '2px 6px',
-                  borderRadius: '10px',
-                  fontSize: '10px',
-                }}>
-                  {currentImageIndex + 1}/{data.imageList.length}
-                </div>
-              </>
-            )}
-          </div>
-        )}
-
-        {/* 标签 */}
-        <div style={{ display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' }}>
-          <span style={{
-            display: 'inline-block',
-            padding: '2px 8px',
-            borderRadius: '12px',
-            background: '#fff1f2',
-            color: '#be123c',
-            fontSize: '10px',
-            fontWeight: '500',
-          }}>
-            {data.matchLevel}
-          </span>
-          <span style={{
-            display: 'inline-block',
-            padding: '2px 8px',
-            borderRadius: '12px',
-            background: '#fff7ed',
-            color: '#c2410c',
-            fontSize: '10px',
-            fontWeight: '500',
-          }}>
-            Score: {data.score}
-          </span>
-        </div>
-
-        {/* 描述 */}
-        {expanded && data.description && (
-          <div style={{
-            fontSize: '11px',
-            color: '#9f1239',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            {data.description}
-          </div>
-        )}
-
-        {/* 评估理由 */}
-        {expanded && data.evaluationReason && (
-          <div style={{
-            fontSize: '10px',
-            color: '#831843',
-            lineHeight: '1.5',
-            paddingTop: '8px',
-            marginTop: '8px',
-            borderTop: '1px solid #fbcfe8',
-          }}>
-            <strong style={{ color: '#9f1239' }}>评估:</strong>
-            <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
-          </div>
-        )}
-      </div>
-      <Handle
-        type="source"
-        position={sourcePosition || Position.Right}
-        style={{ background: '#ec4899', width: 8, height: 8 }}
-      />
-    </div>
-  );
-}
-
-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',
-  };
-  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);
-
-  return (
-    <div style={{ marginLeft: level * 12 + 'px' }}>
-      <div
-        style={{
-          padding: '6px 8px',
-          borderRadius: '4px',
-          cursor: 'pointer',
-          background: 'transparent',
-          border: isSelected ? '1px solid #3b82f6' : '1px solid transparent',
-          display: 'flex',
-          alignItems: 'center',
-          gap: '6px',
-          transition: 'all 0.2s ease',
-          position: 'relative',
-          overflow: 'visible',
-        }}
-        onMouseEnter={(e) => {
-          if (!isSelected) e.currentTarget.style.background = '#f9fafb';
-        }}
-        onMouseLeave={(e) => {
-          if (!isSelected) e.currentTarget.style.background = 'transparent';
-        }}
-      >
-        {/* 策略类型竖线 */}
-        <div style={{
-          width: '3px',
-          height: '20px',
-          background: strategyColor,
-          borderRadius: '2px',
-          flexShrink: 0,
-          position: 'relative',
-          zIndex: 1,
-        }} />
-
-        {hasChildren && (
-          <span
-            style={{
-              fontSize: '10px',
-              color: '#6b7280',
-              cursor: 'pointer',
-              width: '16px',
-              textAlign: 'center',
-              position: 'relative',
-              zIndex: 1,
-            }}
-            onClick={(e) => {
-              e.stopPropagation();
-              onToggle();
-            }}
-          >
-            {isCollapsed ? '▶' : '▼'}
-          </span>
-        )}
-        {!hasChildren && <span style={{ width: '16px', position: 'relative', zIndex: 1 }}></span>}
-
-        <div
-          style={{
-            flex: 1,
-            fontSize: '12px',
-            color: '#374151',
-            position: 'relative',
-            zIndex: 1,
-            minWidth: 0,
-            display: 'flex',
-            flexDirection: 'column',
-            gap: '4px',
-          }}
-          onClick={onSelect}
-        >
-          <div style={{
-            display: 'flex',
-            alignItems: 'center',
-            gap: '8px',
-          }}>
-            {/* 节点类型图标 */}
-            <span style={{
-              fontSize: '12px',
-              flexShrink: 0,
-            }}>
-              {node.type === 'note' ? '📝' : '🔍'}
-            </span>
-
-            <div style={{
-              fontWeight: level === 0 ? '600' : '400',
-              maxWidth: '180px',
-              flex: 1,
-              minWidth: 0,
-              color: (node.type === 'note' ? node.data.matchLevel === 'unsatisfied' : node.data.isSelected === false) ? '#ef4444' : '#374151',
-            }}
-            title={node.data.title || node.id}
-            >
-              {truncateMiddle(node.data.title || node.id, 18)}
-            </div>
-
-            {/* 分数显示 */}
-            <span style={{
-              fontSize: '11px',
-              color: '#6b7280',
-              fontWeight: '500',
-              flexShrink: 0,
-            }}>
-              {score.toFixed(2)}
-            </span>
-          </div>
-
-          {/* 分数下划线 */}
-          <div style={{
-            width: (score * 100) + '%',
-            height: '2px',
-            background: getScoreColor(score),
-            borderRadius: '1px',
-          }} />
-        </div>
-      </div>
-
-      {hasChildren && !isCollapsed && (
-        <div>
-          {children}
-        </div>
-      )}
-    </div>
-  );
-}
-
-// 使用 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';
-
-      // 将 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]) => {
-    if (node.type === 'query') {
-      // 使用 query_level 作为唯一ID
-      const canvasId = node.query + '_' + node.level;
-      originalIdToCanvasId[originalId] = canvasId;
-
-      // 如果这个 canvasId 还没有创建过节点,则创建
-      if (!canvasIdToNodeData[canvasId]) {
-        canvasIdToNodeData[canvasId] = true;
-        nodes.push({
-          id: canvasId, // 使用 query_level 格式
-          originalId: originalId, // 保留原始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') {
-      // note节点直接使用原始ID
-      originalIdToCanvasId[originalId] = originalId;
-
-      if (!canvasIdToNodeData[originalId]) {
-        canvasIdToNodeData[originalId] = true;
-        nodes.push({
-          id: originalId,
-          originalId: originalId,
-          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,
-            imageList: node.image_list || [], // 添加图片列表
-            noteUrl: node.note_url || '', // 添加帖子链接
-            evaluationReason: node.evaluation_reason || '', // 添加评估理由
-          },
-          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]);
-
-  // 初始化树节点折叠状态
-  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 (
-        <TreeNode
-          key={node.treeKey}
-          node={node}
-          level={level}
-          isCollapsed={isCollapsed}
-          isSelected={isSelected}
-          onToggle={() => {
-            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)}
-        </TreeNode>
-      );
-    });
-  }, [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 (
-      <div style={{ padding: 50, color: 'red', fontSize: 20 }}>
-        ERROR: No nodes to display!
-      </div>
-    );
-  }
-
-  return (
-    <div style={{ width: '100vw', height: '100vh', background: '#f9fafb', display: 'flex', flexDirection: 'column' }}>
-      {/* 顶部面包屑导航栏 */}
-      <div style={{
-        minHeight: '48px',
-        maxHeight: '120px',
-        background: 'white',
-        borderBottom: '1px solid #e5e7eb',
-        display: 'flex',
-        alignItems: 'flex-start',
-        padding: '12px 24px',
-        zIndex: 1000,
-        boxShadow: '0 1px 3px rgba(0, 0, 0, 0.05)',
-        flexShrink: 0,
-        overflowY: 'auto',
-      }}>
-        <div style={{ width: '100%' }}>
-          {selectedNodeId ? (
-            <div style={{ fontSize: '12px', color: '#6b7280' }}>
-              {/* 面包屑导航 - 显示所有路径 */}
-              {(() => {
-                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 (
-                  <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
-                    {uniquePaths.map((path, pathIndex) => (
-                      <div key={pathIndex} style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
-                        {pathIndex > 0 && <span style={{ color: '#d1d5db', marginRight: '4px' }}>或</span>}
-                        {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;
-
-                          return (
-                          <React.Fragment key={node.id + '-' + index}>
-                            <span
-                              onClick={() => {
-                                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})\`}
-                            >
-                              {/* 上半部分:竖线 + 图标 + 文字 + 分数 */}
-                              <div style={{
-                                display: 'flex',
-                                alignItems: 'center',
-                                gap: '6px',
-                              }}>
-                                {/* 策略类型竖线 */}
-                                <div style={{
-                                  width: '3px',
-                                  height: '16px',
-                                  background: strategyColor,
-                                  borderRadius: '2px',
-                                  flexShrink: 0,
-                                }} />
-
-                                {/* 节点类型图标 */}
-                                <span style={{
-                                  fontSize: '11px',
-                                  flexShrink: 0,
-                                }}>
-                                  {node.type === 'note' ? '📝' : '🔍'}
-                                </span>
-
-                                {/* 节点文字 */}
-                                <span style={{
-                                  flex: 1,
-                                  fontSize: '12px',
-                                  color: nodeIsSelected ? '#374151' : '#ef4444',
-                                }}>
-                                  {truncateMiddle(node.data.title || node.id, 18)}
-                                </span>
-
-                                {/* 分数显示 */}
-                                <span style={{
-                                  fontSize: '10px',
-                                  color: '#6b7280',
-                                  fontWeight: '500',
-                                  flexShrink: 0,
-                                }}>
-                                  {nodeScore.toFixed(2)}
-                                </span>
-                              </div>
-
-                              {/* 分数下划线 */}
-                              <div style={{
-                                width: (nodeScore * 100) + '%',
-                                height: '2px',
-                                background: getScoreColor(nodeScore),
-                                borderRadius: '1px',
-                                marginLeft: '9px',
-                              }} />
-                            </span>
-                            {index < path.length - 1 && <span style={{ color: '#9ca3af' }}>›</span>}
-                          </React.Fragment>
-                        )})}
-                      </div>
-                    ))}
-                  </div>
-                );
-              })()}
-            </div>
-          ) : (
-            <div style={{ fontSize: '13px', color: '#9ca3af', textAlign: 'center' }}>
-              选择一个节点查看路径
-            </div>
-          )}
-        </div>
-      </div>
-
-      {/* 主内容区:目录 + 画布 */}
-      <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
-        {/* 左侧目录树 */}
-        <div style={{
-          width: '320px',
-          background: 'white',
-          borderRight: '1px solid #e5e7eb',
-          display: 'flex',
-          flexDirection: 'column',
-          flexShrink: 0,
-        }}>
-          <div style={{
-            padding: '12px 16px',
-            borderBottom: '1px solid #e5e7eb',
-            display: 'flex',
-            justifyContent: 'space-between',
-            alignItems: 'center',
-          }}>
-            <span style={{
-              fontWeight: '600',
-              fontSize: '14px',
-              color: '#111827',
-            }}>
-              节点目录
-            </span>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedTreeNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="展开全部节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const getAllTreeKeys = (nodes) => {
-                    const keys = new Set();
-                    const traverse = (node) => {
-                      if (node.children && node.children.length > 0) {
-                        keys.add(node.treeKey);
-                        node.children.forEach(traverse);
-                      }
-                    };
-                    nodes.forEach(traverse);
-                    return keys;
-                  };
-                  setCollapsedTreeNodes(getAllTreeKeys(treeRoots));
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                }}
-                title="折叠全部节点"
-              >
-                全部折叠
-              </button>
-            </div>
-          </div>
-          <div style={{
-            flex: 1,
-            overflowX: 'auto',
-            overflowY: 'auto',
-            padding: '8px',
-          }}>
-            <div style={{ minWidth: 'fit-content' }}>
-              {renderTree(treeRoots)}
-            </div>
-          </div>
-        </div>
-
-        {/* 画布区域 */}
-        <div style={{ flex: 1, position: 'relative' }}>
-
-          {/* 右侧图例 */}
-          <div style={{
-            position: 'absolute',
-            top: '20px',
-            right: '20px',
-            background: 'white',
-            padding: '16px',
-            borderRadius: '12px',
-            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.08)',
-            zIndex: 1000,
-            maxWidth: '260px',
-            border: '1px solid #e5e7eb',
-          }}>
-        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
-          <h3 style={{ fontSize: '14px', fontWeight: '600', color: '#111827', margin: 0 }}>图例</h3>
-          <button
-            onClick={() => setFocusMode(!focusMode)}
-            style={{
-              fontSize: '11px',
-              padding: '4px 8px',
-              borderRadius: '4px',
-              border: '1px solid',
-              borderColor: focusMode ? '#3b82f6' : '#d1d5db',
-              background: focusMode ? '#3b82f6' : 'white',
-              color: focusMode ? 'white' : '#6b7280',
-              cursor: 'pointer',
-              fontWeight: '500',
-            }}
-            title={focusMode ? '关闭聚焦模式' : '开启聚焦模式'}
-          >
-            {focusMode ? '🎯 聚焦' : '📊 全图'}
-          </button>
-        </div>
-
-        <div style={{ fontSize: '12px' }}>
-          {/* 画布节点展开/折叠控制 */}
-          <div style={{ marginBottom: '12px', paddingBottom: '12px', borderBottom: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>节点控制</div>
-            <div style={{ display: 'flex', gap: '6px' }}>
-              <button
-                onClick={() => {
-                  setCollapsedNodes(new Set());
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="展开画布中所有节点的子节点"
-              >
-                全部展开
-              </button>
-              <button
-                onClick={() => {
-                  const allNodeIds = new Set(initialNodes.map(n => n.id));
-                  setCollapsedNodes(allNodeIds);
-                }}
-                style={{
-                  fontSize: '11px',
-                  padding: '4px 8px',
-                  borderRadius: '4px',
-                  border: '1px solid #d1d5db',
-                  background: 'white',
-                  color: '#6b7280',
-                  cursor: 'pointer',
-                  fontWeight: '500',
-                  flex: 1,
-                }}
-                title="折叠画布中所有节点的子节点"
-              >
-                全部折叠
-              </button>
-            </div>
-          </div>
-
-          <div style={{ paddingTop: '12px', borderTop: '1px solid #f3f4f6' }}>
-            <div style={{ fontSize: '12px', fontWeight: '500', marginBottom: '8px', color: '#374151' }}>策略类型</div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#10b981', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>初始分词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#06b6d4', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>调用sug</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#f59e0b', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>同义改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#3b82f6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>加词</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#8b5cf6', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#ec4899', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>基于部分匹配改进</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#a855f7', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-抽象改写</span>
-            </div>
-            <div style={{ display: 'flex', alignItems: 'center', margin: '6px 0' }}>
-              <div style={{ width: '20px', height: '2px', marginRight: '8px', background: '#fb923c', opacity: 0.7 }}></div>
-              <span style={{ color: '#6b7280', fontSize: '11px' }}>结果分支-同义改写</span>
-            </div>
-          </div>
-
-          <div style={{
-            marginTop: '12px',
-            paddingTop: '12px',
-            borderTop: '1px solid #f3f4f6',
-            fontSize: '11px',
-            color: '#9ca3af',
-            lineHeight: '1.5',
-          }}>
-            💡 点击节点左上角 × 隐藏节点
-          </div>
-
-          {/* 隐藏节点列表 - 在图例内部 */}
-          {hiddenNodes.size > 0 && (
-            <div style={{
-              marginTop: '12px',
-              paddingTop: '12px',
-              borderTop: '1px solid #f3f4f6',
-            }}>
-              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
-                <h4 style={{ fontSize: '12px', fontWeight: '600', color: '#111827' }}>已隐藏节点</h4>
-                <button
-                  onClick={() => setHiddenNodes(new Set())}
-                  style={{
-                    fontSize: '10px',
-                    color: '#3b82f6',
-                    background: 'none',
-                    border: 'none',
-                    cursor: 'pointer',
-                    textDecoration: 'underline',
-                  }}
-                >
-                  全部恢复
-                </button>
-              </div>
-              <div style={{ fontSize: '12px', maxHeight: '200px', overflow: 'auto' }}>
-                {Array.from(hiddenNodes).map(nodeId => {
-                  const node = initialNodes.find(n => n.id === nodeId);
-                  if (!node) return null;
-                  return (
-                    <div
-                      key={nodeId}
-                      style={{
-                        display: 'flex',
-                        justifyContent: 'space-between',
-                        alignItems: 'center',
-                        padding: '6px 8px',
-                        margin: '4px 0',
-                        background: '#f9fafb',
-                        borderRadius: '6px',
-                        fontSize: '11px',
-                      }}
-                    >
-                      <span
-                        style={{
-                          flex: 1,
-                          overflow: 'hidden',
-                          textOverflow: 'ellipsis',
-                          whiteSpace: 'nowrap',
-                          color: '#374151',
-                        }}
-                        title={node.data.title || nodeId}
-                      >
-                        {node.data.title || nodeId}
-                      </span>
-                      <button
-                        onClick={() => {
-                          setHiddenNodes(prev => {
-                            const newSet = new Set(prev);
-                            newSet.delete(nodeId);
-                            return newSet;
-                          });
-                        }}
-                        style={{
-                          marginLeft: '8px',
-                          fontSize: '10px',
-                          color: '#10b981',
-                          background: 'none',
-                          border: 'none',
-                          cursor: 'pointer',
-                          flexShrink: 0,
-                        }}
-                      >
-                        恢复
-                      </button>
-                    </div>
-                  );
-                })}
-              </div>
-            </div>
-          )}
-          </div>
-          </div>
-
-          {/* React Flow 画布 */}
-          <ReactFlow
-            nodes={nodes}
-            edges={edges}
-            nodeTypes={nodeTypes}
-            fitView
-            fitViewOptions={{ padding: 0.2, duration: 500 }}
-            minZoom={0.1}
-            maxZoom={1.5}
-            nodesDraggable={true}
-            nodesConnectable={false}
-            elementsSelectable={true}
-            defaultEdgeOptions={{
-              type: 'smoothstep',
-            }}
-            proOptions={{ hideAttribution: true }}
-            onNodeClick={(event, clickedNode) => {
-              setSelectedNodeId(clickedNode.id);
-            }}
-          >
-            <Controls style={{ bottom: '20px', left: 'auto', right: '20px' }} />
-            <Background variant="dots" gap={20} size={1} color="#e5e7eb" />
-          </ReactFlow>
-        </div>
-      </div>
-    </div>
-  );
-}
-
-function App() {
-  return (
-    <ReactFlowProvider>
-      <FlowContent />
-    </ReactFlowProvider>
-  );
-}
-
-const root = createRoot(document.getElementById('root'));
-root.render(<App />);
-`;
-
-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 = `<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>查询图可视化</title>
-    <link rel="preconnect" href="https://fonts.googleapis.com">
-    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
-    <script src="https://unpkg.com/dagre@0.8.5/dist/dagre.min.js"></script>
-    <script>
-      // 过滤特定的 React 警告
-      const originalError = console.error;
-      console.error = (...args) => {
-        if (typeof args[0] === 'string' && args[0].includes('Each child in a list should have a unique "key" prop')) {
-          return;
-        }
-        originalError.apply(console, args);
-      };
-    </script>
-    <style>
-        * {
-            margin: 0;
-            padding: 0;
-            box-sizing: border-box;
-        }
-        body {
-            font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-            overflow: hidden;
-            -webkit-font-smoothing: antialiased;
-            -moz-osx-font-smoothing: grayscale;
-        }
-        #root {
-            width: 100vw;
-            height: 100vh;
-        }
-        ${css}
-
-        /* 自定义样式覆盖 */
-        .react-flow__edge-path {
-            stroke-linecap: round;
-        }
-        .react-flow__controls {
-            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-            border: 1px solid #e5e7eb;
-            border-radius: 8px;
-        }
-        .react-flow__controls-button {
-            border: none;
-            border-bottom: 1px solid #e5e7eb;
-        }
-        .react-flow__controls-button:hover {
-            background: #f9fafb;
-        }
-    </style>
-</head>
-<body>
-    <div id="root"></div>
-    <script>${bundleJs}</script>
-</body>
-</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);
-});