Просмотр исходного кода

Add simplified visualization with node merging

- Add convertV8ToGraphSimplified function to merge duplicate nodes
  - Merge all query nodes by text (ignoring seg/sug/add_word source)
  - Merge post nodes by note_id
  - Hide Round/Step nodes, show strategy info on edges
  - Reduce nodes by ~47% (752 -> 399)

- Enhance node display in simplified view
  - Show evolution history when node appears multiple times
  - Display search results count for queries that found posts
  - Show which queries found each post
  - Display rounds where posts appeared

- Support both detailed and simplified views
  - Use --simplified flag to enable simplified view
  - Simplified: focus on query and post evolution
  - Detailed: show complete Round/Step structure

- Generate both datasets in simplified mode
  - Full data prepared for directory tree (not yet implemented)
  - Simplified data used for canvas display

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 4 недель назад
Родитель
Сommit
898a46e9d5

+ 282 - 1
visualization/sug_v6_1_2_8/convert_v8_to_graph_v2.js

@@ -558,4 +558,285 @@ function convertV8ToGraphV2(runContext, searchResults) {
   };
 }
 
-module.exports = { convertV8ToGraphV2 };
+/**
+ * 简化版转换:专注于query和post的演化
+ * - 合并所有query节点(不区分seg/sug/add_word)
+ * - 合并相同的帖子节点
+ * - 步骤信息放在边上
+ * - 隐藏Round/Step节点
+ */
+function convertV8ToGraphSimplified(runContext, searchResults) {
+  const mergedNodes = {};
+  const edges = [];
+  const iterations = {};
+
+  const o = runContext.o || '原始问题';
+  const rounds = runContext.rounds || [];
+
+  // 添加原始问题根节点
+  const rootId = 'root_o';
+  mergedNodes[rootId] = {
+    type: 'root',
+    query: o,
+    level: 0,
+    relevance_score: 1.0,
+    strategy: '原始问题',
+    iteration: 0,
+    is_selected: true,
+    occurrences: [{round: 0, role: 'root', score: 1.0}]
+  };
+  iterations[0] = [rootId];
+
+  // 用于记录节点之间的演化关系
+  const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}}
+  const postMap = {}; // {note_id: {...}}
+
+  // 第一遍:收集所有query和post
+  rounds.forEach((round, roundIndex) => {
+    const roundNum = round.round_num || roundIndex;
+
+    if (round.type === 'initialization') {
+      // Round 0: 收集分词结果
+      (round.q_list_1 || []).forEach(q => {
+        if (!queryEvolution[q.text]) {
+          queryEvolution[q.text] = {
+            occurrences: [],
+            parentTexts: new Set([o]), // 来自原始问题
+            childTexts: new Set()
+          };
+        }
+        queryEvolution[q.text].occurrences.push({
+          round: roundNum,
+          role: 'segmentation',
+          strategy: '分词',
+          score: q.score,
+          reason: q.reason
+        });
+      });
+    } else {
+      // Round 1+
+
+      // 收集sug_details (推荐词)
+      Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => {
+        sugs.forEach(sug => {
+          if (!queryEvolution[sug.text]) {
+            queryEvolution[sug.text] = {
+              occurrences: [],
+              parentTexts: new Set(),
+              childTexts: new Set()
+            };
+          }
+          queryEvolution[sug.text].occurrences.push({
+            round: roundNum,
+            role: 'sug',
+            strategy: '调用sug',
+            score: sug.score,
+            reason: sug.reason
+          });
+          queryEvolution[sug.text].parentTexts.add(parentText);
+          if (queryEvolution[parentText]) {
+            queryEvolution[parentText].childTexts.add(sug.text);
+          }
+        });
+      });
+
+      // 收集add_word_details (加词结果)
+      Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => {
+        words.forEach(word => {
+          if (!queryEvolution[word.text]) {
+            queryEvolution[word.text] = {
+              occurrences: [],
+              parentTexts: new Set(),
+              childTexts: new Set()
+            };
+          }
+          queryEvolution[word.text].occurrences.push({
+            round: roundNum,
+            role: 'add_word',
+            strategy: '加词',
+            score: word.score,
+            reason: word.reason,
+            selectedWord: word.selected_word
+          });
+          queryEvolution[word.text].parentTexts.add(seedText);
+          if (queryEvolution[seedText]) {
+            queryEvolution[seedText].childTexts.add(word.text);
+          }
+        });
+      });
+
+      // 收集搜索结果和帖子
+      const roundSearchResults = round.search_results || searchResults;
+      if (roundSearchResults && Array.isArray(roundSearchResults)) {
+        roundSearchResults.forEach(search => {
+          const searchText = search.text;
+
+          // 标记这个query被用于搜索
+          if (queryEvolution[searchText]) {
+            queryEvolution[searchText].occurrences.push({
+              round: roundNum,
+              role: 'search',
+              strategy: '执行搜索',
+              score: search.score_with_o,
+              postCount: search.post_list ? search.post_list.length : 0
+            });
+          }
+
+          // 收集帖子
+          if (search.post_list && search.post_list.length > 0) {
+            search.post_list.forEach(post => {
+              if (!postMap[post.note_id]) {
+                postMap[post.note_id] = {
+                  ...post,
+                  foundByQueries: new Set(),
+                  foundInRounds: new Set()
+                };
+              }
+              postMap[post.note_id].foundByQueries.add(searchText);
+              postMap[post.note_id].foundInRounds.add(roundNum);
+
+              // 建立query到post的关系
+              if (!queryEvolution[searchText].posts) {
+                queryEvolution[searchText].posts = new Set();
+              }
+              queryEvolution[searchText].posts.add(post.note_id);
+            });
+          }
+        });
+      }
+    }
+  });
+
+  // 第二遍:创建合并后的节点
+  Object.entries(queryEvolution).forEach(([text, data]) => {
+    const nodeId = `query_${text}`;
+
+    // 获取最新的分数
+    const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {};
+    const hasSearchResults = data.posts && data.posts.size > 0;
+
+    mergedNodes[nodeId] = {
+      type: 'query',
+      query: text,
+      level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2,
+      relevance_score: latestOccurrence.score || 0,
+      evaluationReason: latestOccurrence.reason || '',
+      strategy: data.occurrences.map(o => o.strategy).join(' + '),
+      iteration: Math.max(...data.occurrences.map(o => o.round), 0),
+      is_selected: true,
+      occurrences: data.occurrences,
+      hasSearchResults: hasSearchResults,
+      postCount: data.posts ? data.posts.size : 0,
+      selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || ''
+    };
+
+    // 添加到对应的轮次
+    const maxRound = Math.max(...data.occurrences.map(o => o.round), 0);
+    const iterKey = maxRound * 10 + 2;
+    if (!iterations[iterKey]) iterations[iterKey] = [];
+    iterations[iterKey].push(nodeId);
+  });
+
+  // 创建帖子节点
+  Object.entries(postMap).forEach(([noteId, post]) => {
+    const postId = `post_${noteId}`;
+
+    const imageList = (post.images || []).map(url => ({
+      image_url: url
+    }));
+
+    mergedNodes[postId] = {
+      type: 'post',
+      query: post.title,
+      level: 100, // 放在最后
+      relevance_score: 0,
+      strategy: '帖子',
+      iteration: Math.max(...Array.from(post.foundInRounds)),
+      is_selected: true,
+      note_id: post.note_id,
+      note_url: post.note_url,
+      body_text: post.body_text || '',
+      images: post.images || [],
+      image_list: imageList,
+      interact_info: post.interact_info || {},
+      foundByQueries: Array.from(post.foundByQueries),
+      foundInRounds: Array.from(post.foundInRounds)
+    };
+
+    if (!iterations[100]) iterations[100] = [];
+    iterations[100].push(postId);
+  });
+
+  // 第三遍:创建边
+  // 1. 原始问题 -> 分词结果
+  Object.entries(queryEvolution).forEach(([text, data]) => {
+    const nodeId = `query_${text}`;
+    const segOccurrence = data.occurrences.find(o => o.role === 'segmentation');
+
+    if (segOccurrence && data.parentTexts.has(o)) {
+      edges.push({
+        from: rootId,
+        to: nodeId,
+        edge_type: 'segmentation',
+        strategy: '分词',
+        label: '分词',
+        round: 0
+      });
+    }
+  });
+
+  // 2. Query演化关系
+  Object.entries(queryEvolution).forEach(([text, data]) => {
+    const nodeId = `query_${text}`;
+
+    data.parentTexts.forEach(parentText => {
+      if (parentText === o) return; // 跳过原始问题(已处理)
+
+      const parentNodeId = `query_${parentText}`;
+      if (!mergedNodes[parentNodeId]) return;
+
+      // 找到这个演化的策略和轮次
+      const occurrence = data.occurrences.find(o =>
+        o.role === 'sug' || o.role === 'add_word'
+      );
+
+      edges.push({
+        from: parentNodeId,
+        to: nodeId,
+        edge_type: occurrence?.role || 'evolution',
+        strategy: occurrence?.strategy || '演化',
+        label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`,
+        round: occurrence?.round || 0
+      });
+    });
+  });
+
+  // 3. Query -> Post (搜索关系)
+  Object.entries(queryEvolution).forEach(([text, data]) => {
+    const nodeId = `query_${text}`;
+
+    if (data.posts && data.posts.size > 0) {
+      const searchOccurrence = data.occurrences.find(o => o.role === 'search');
+
+      data.posts.forEach(noteId => {
+        const postId = `post_${noteId}`;
+        edges.push({
+          from: nodeId,
+          to: postId,
+          edge_type: 'search',
+          strategy: '搜索',
+          label: `搜索 (${data.posts.size}个帖子)`,
+          round: searchOccurrence?.round || 0
+        });
+      });
+    }
+  });
+
+  return {
+    nodes: mergedNodes,
+    edges,
+    iterations
+  };
+}
+
+module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };

+ 100 - 6
visualization/sug_v6_1_2_8/index.js

@@ -4,17 +4,18 @@ 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 { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v2');
 
 // 读取命令行参数
 const args = process.argv.slice(2);
 if (args.length === 0) {
-  console.error('Usage: node index.js <path-to-run_context.json> [output.html]');
+  console.error('Usage: node index.js <path-to-run_context.json> [output.html] [--simplified]');
   process.exit(1);
 }
 
 const inputFile = args[0];
 const outputFile = args[1] || 'query_graph_output.html';
+const useSimplified = args.includes('--simplified');
 
 // 读取输入数据
 const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf-8'));
@@ -35,14 +36,35 @@ if (inputData.rounds && inputData.o) {
     console.log('✅ 使用 run_context.json 中的内嵌搜索结果');
   }
 
-  // 使用新的转换函数(按 Round > 步骤 > 数据 组织)
-  const graphData = convertV8ToGraphV2(inputData, searchResults);
+  // 选择转换函数
+  let graphData;
+  let fullData = null; // 用于目录的完整数据
+
+  if (useSimplified) {
+    console.log('🎨 使用简化视图(合并query节点)');
+    // 生成简化版用于画布
+    graphData = convertV8ToGraphSimplified(inputData, searchResults);
+    // 生成完整版用于目录
+    const fullGraphData = convertV8ToGraphV2(inputData, searchResults);
+    fullData = {
+      nodes: fullGraphData.nodes,
+      edges: fullGraphData.edges,
+      iterations: fullGraphData.iterations
+    };
+    console.log(`✅ 简化版: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
+    console.log(`📋 完整版(用于目录): ${Object.keys(fullData.nodes).length} 个节点`);
+  } else {
+    console.log('📊 使用详细视图(完整流程)');
+    graphData = convertV8ToGraphV2(inputData, searchResults);
+    console.log(`✅ 转换完成: ${Object.keys(graphData.nodes).length} 个节点, ${graphData.edges.length} 条边`);
+  }
+
   data = {
     nodes: graphData.nodes,
     edges: graphData.edges,
-    iterations: graphData.iterations
+    iterations: graphData.iterations,
+    fullData: fullData // 传递完整数据
   };
-  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 格式,直接使用');
@@ -344,6 +366,46 @@ function QueryNode({ id, data, sourcePosition, targetPosition }) {
                 <div style={{ marginTop: '2px' }}>{data.evaluationReason}</div>
               </div>
             )}
+            {data.occurrences && data.occurrences.length > 1 && (
+              <div style={{
+                marginTop: '6px',
+                paddingTop: '6px',
+                borderTop: '1px solid #f3f4f6',
+                fontSize: '10px',
+                color: '#6b7280',
+              }}>
+                <strong style={{ color: '#4b5563' }}>演化历史 ({data.occurrences.length}次):</strong>
+                <div style={{ marginTop: '4px' }}>
+                  {data.occurrences.map((occ, idx) => (
+                    <div key={idx} style={{ marginTop: '2px', paddingLeft: '8px' }}>
+                      <span style={{ color: '#3b82f6', fontWeight: '500' }}>R{occ.round}</span>
+                      {' · '}
+                      <span>{occ.strategy}</span>
+                      {occ.score !== undefined && (
+                        <span style={{ color: '#16a34a', marginLeft: '4px' }}>
+                          ({typeof occ.score === 'number' ? occ.score.toFixed(2) : occ.score})
+                        </span>
+                      )}
+                    </div>
+                  ))}
+                </div>
+              </div>
+            )}
+            {data.hasSearchResults && (
+              <div style={{
+                marginTop: '6px',
+                paddingTop: '6px',
+                borderTop: '1px solid #f3f4f6',
+                fontSize: '10px',
+                background: '#fef3c7',
+                padding: '4px 6px',
+                borderRadius: '4px',
+                color: '#92400e',
+                fontWeight: '500',
+              }}>
+                🔍 找到 {data.postCount} 个帖子
+              </div>
+            )}
           </div>
         </div>
       </div>
@@ -530,6 +592,38 @@ function NoteNode({ id, data, sourcePosition, targetPosition }) {
           </div>
         )}
 
+        {/* 被哪些query找到 */}
+        {data.foundByQueries && data.foundByQueries.length > 0 && (
+          <div style={{
+            marginBottom: '8px',
+            padding: '6px 8px',
+            background: '#f0fdf4',
+            borderRadius: '6px',
+            fontSize: '10px',
+          }}>
+            <strong style={{ color: '#16a34a' }}>🔍 被找到:</strong>
+            <div style={{ marginTop: '4px', display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
+              {data.foundByQueries.map((query, idx) => (
+                <span key={idx} style={{
+                  display: 'inline-block',
+                  padding: '2px 6px',
+                  background: '#dcfce7',
+                  color: '#166534',
+                  borderRadius: '4px',
+                  fontSize: '9px',
+                }}>
+                  {query}
+                </span>
+              ))}
+            </div>
+            {data.foundInRounds && data.foundInRounds.length > 0 && (
+              <div style={{ marginTop: '4px', color: '#6b7280' }}>
+                出现在: Round {data.foundInRounds.join(', ')}
+              </div>
+            )}
+          </div>
+        )}
+
         {/* 标签 */}
         {(data.matchLevel || data.score) && (
           <div style={{ display: 'flex', gap: '6px', marginBottom: '8px', flexWrap: 'wrap' }}>