刘立冬 2 周之前
父節點
當前提交
d579b9910c
共有 1 個文件被更改,包括 241 次插入18 次删除
  1. 241 18
      src/visualizers/deconstruction_visualizer.py

+ 241 - 18
src/visualizers/deconstruction_visualizer.py

@@ -802,6 +802,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             overflow-y: auto;
             height: fit-content;
             max-height: calc(100vh - 280px);
+            /* 隐藏滚动条 */
+            scrollbar-width: none; /* Firefox */
+            -ms-overflow-style: none; /* IE/Edge */
+        }}
+
+        .left-sidebar::-webkit-scrollbar {{
+            display: none; /* Chrome/Safari */
         }}
 
         /* 中间栏 - base_word列表 */
@@ -815,6 +822,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             height: fit-content;
             max-height: calc(100vh - 280px);
             display: none;
+            /* 隐藏滚动条 */
+            scrollbar-width: none; /* Firefox */
+            -ms-overflow-style: none; /* IE/Edge */
+        }}
+
+        .middle-sidebar::-webkit-scrollbar {{
+            display: none; /* Chrome/Safari */
         }}
 
         .middle-sidebar.active {{
@@ -832,6 +846,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             height: fit-content;
             max-height: calc(100vh - 280px);
             display: none;
+            /* 隐藏滚动条 */
+            scrollbar-width: none; /* Firefox */
+            -ms-overflow-style: none; /* IE/Edge */
+        }}
+
+        .right-sidebar::-webkit-scrollbar {{
+            display: none; /* Chrome/Safari */
         }}
 
         .right-sidebar.active {{
@@ -846,6 +867,13 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             box-shadow: 0 2px 8px rgba(0,0,0,0.1);
             overflow-y: auto;
             max-height: calc(100vh - 280px);
+            /* 隐藏滚动条 */
+            scrollbar-width: none; /* Firefox */
+            -ms-overflow-style: none; /* IE/Edge */
+        }}
+
+        .detail-area::-webkit-scrollbar {{
+            display: none; /* Chrome/Safari */
         }}
 
         /* 侧边栏标题 */
@@ -2856,6 +2884,23 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             margin-top: 4px;
         }}
 
+        .partial-feature-score {{
+            display: inline-block;
+            font-size: 12px;
+            font-weight: 600;
+            color: #ca8a04;
+            background: #fef9c3;
+            padding: 2px 8px;
+            border-radius: 4px;
+            margin-right: 6px;
+        }}
+
+        .partial-feature-meta {{
+            font-size: 11px;
+            color: #ca8a04;
+            margin-top: 4px;
+        }}
+
         /* ========== 部分匹配特征样式(橘黄色)========== */
         .partial-similarity-section {{
             margin-top: 20px;
@@ -3122,6 +3167,28 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
 
     <!-- 主容器 - 级联布局 -->
     <div class="main-container">
+        <!-- SVG连接线层 -->
+        <svg id="connectionLines" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1;">
+            <!-- 选题点到人设的连接线 -->
+            <path id="line-feature-to-base" d=""
+                  stroke="#3b82f6"
+                  stroke-width="1.5"
+                  fill="none"
+                  opacity="0.6"/>
+            <!-- 人设到搜索词的连接线 -->
+            <path id="line-base-to-search" d=""
+                  stroke="#3b82f6"
+                  stroke-width="1.5"
+                  fill="none"
+                  opacity="0.6"/>
+            <!-- 搜索词到搜索结果的连接线 -->
+            <path id="line-search-to-detail" d=""
+                  stroke="#3b82f6"
+                  stroke-width="1.5"
+                  fill="none"
+                  opacity="0.6"/>
+        </svg>
+
         <!-- 左侧栏:原始特征列表 -->
         <div class="left-sidebar" id="leftSidebar">
             <div class="sidebar-header">选题点</div>
@@ -3357,7 +3424,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             let html = `
                 <div class="candidate-library">
                     <div class="candidate-header" onclick="toggleCandidateLibrary()">
-                        <div class="candidate-title">📋 候选词库(区分人设/帖子来源)</div>
+                        <div class="candidate-title">📋 候选分类、标签(用于组合生成搜索query)</div>
                         <div class="candidate-toggle" id="candidate-toggle">▼</div>
                     </div>
                     <div class="candidate-content" id="candidate-content">
@@ -3367,7 +3434,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             if (personaCount > 0) {{
                 html += `
                     <div class="candidate-section">
-                        <div class="candidate-section-title">【人设候选词】(${{personaCount}}个)</div>
+                        <div class="candidate-section-title">【来源人设】(${{personaCount}}个)  与帖子相似度≥0.8</div>
                         <div class="candidate-list">
                 `;
 
@@ -3392,7 +3459,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             if (postCount > 0) {{
                 html += `
                     <div class="candidate-section">
-                        <div class="candidate-section-title">【帖子候选词】(${{postCount}}个)</div>
+                        <div class="candidate-section-title">【来源帖子】(${{postCount}}个) 与人设相似度≥0.8</div>
                         <div class="candidate-list">
                 `;
 
@@ -3490,13 +3557,34 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                         sourcePath = top3Matches[0]['所属分类路径'] || '';
                     }}
 
+                    // 检查是否有完全匹配的帖子
+                    let hasCompleteMatch = false;
+                    groups.forEach(group => {{
+                        const searches = group['top10_searches'] || [];
+                        searches.forEach(search => {{
+                            const evaluation = search['evaluation_with_filter'];
+                            if (evaluation && evaluation.notes_evaluation) {{
+                                evaluation.notes_evaluation.forEach(noteEval => {{
+                                    if (noteEval['匹配类型'] === '完全匹配') {{
+                                        hasCompleteMatch = true;
+                                    }}
+                                }});
+                            }}
+                        }});
+                    }});
+
                     const isActive = selectedFeatureIdx === featureIdx;
+                    const postIcon = hasCompleteMatch ? ' 📝' : '';
 
                     html += `
                         <div class="feature-item-left ${{isActive ? 'active' : ''}}"
                              onclick="selectFeature(${{featureIdx}})"
                              id="feature-left-${{featureIdx}}">
-                            <div class="feature-name">🎯 ${{featureName}}</div>
+                            <div class="feature-name">🎯 ${{featureName}}${{postIcon}}</div>
+                            <div class="cascade-item-meta">
+                                <span class="high-feature-score">相似度: ${{similarity.toFixed(2)}}</span>
+                                <span class="high-feature-meta">${{dimension}}</span>
+                            </div>
                         </div>
                     `;
                 }});
@@ -3536,6 +3624,117 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             leftContent.innerHTML = html;
         }}
 
+        // ========== 连接线绘制函数 ==========
+
+        // 绘制两个元素之间的连接线
+        function drawConnectionLine(fromId, toId, lineId) {{
+            const fromEl = document.getElementById(fromId);
+            const toEl = document.getElementById(toId);
+            const lineEl = document.getElementById(lineId);
+
+            if (!fromEl || !toEl || !lineEl) {{
+                // 如果任一元素不存在,隐藏连接线
+                if (lineEl) lineEl.setAttribute('d', '');
+                return;
+            }}
+
+            // 获取容器位置
+            const container = document.querySelector('.main-container');
+            const containerRect = container.getBoundingClientRect();
+
+            // 获取元素位置(相对于容器)
+            const fromRect = fromEl.getBoundingClientRect();
+            const toRect = toEl.getBoundingClientRect();
+
+            // 计算起点(从元素右侧中心点出发)
+            const startX = fromRect.right - containerRect.left;
+            const startY = fromRect.top + fromRect.height / 2 - containerRect.top;
+
+            // 计算终点
+            let endX, endY;
+            if (toId === 'detailArea') {{
+                // 如果目标是detailArea,连接到其左上角区域
+                endX = toRect.left - containerRect.left;
+                endY = toRect.top + 80 - containerRect.top; // 80px是header高度
+            }} else {{
+                // 其他情况连接到左侧中心点
+                endX = toRect.left - containerRect.left;
+                endY = toRect.top + toRect.height / 2 - containerRect.top;
+            }}
+
+            // 使用贝塞尔曲线绘制连接线
+            const controlPoint1X = startX + (endX - startX) * 0.5;
+            const controlPoint1Y = startY;
+            const controlPoint2X = startX + (endX - startX) * 0.5;
+            const controlPoint2Y = endY;
+
+            const path = `M ${{startX}} ${{startY}} C ${{controlPoint1X}} ${{controlPoint1Y}}, ${{controlPoint2X}} ${{controlPoint2Y}}, ${{endX}} ${{endY}}`;
+
+            lineEl.setAttribute('d', path);
+        }}
+
+        // 更新所有连接线
+        function updateConnectionLines() {{
+            // 1. 选题点 → 人设
+            if (selectedFeatureIdx !== null && selectedBaseWordIdx !== null) {{
+                const fromId = `feature-left-${{selectedFeatureIdx}}`;
+                const toId = `baseword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}`;
+                drawConnectionLine(fromId, toId, 'line-feature-to-base');
+            }} else {{
+                // 清空线条
+                document.getElementById('line-feature-to-base').setAttribute('d', '');
+            }}
+
+            // 2. 人设 → 搜索词
+            if (selectedBaseWordIdx !== null && selectedSearchWordIdx !== null) {{
+                const fromId = `baseword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}`;
+                const toId = `searchword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}-${{selectedSearchWordIdx}}`;
+                drawConnectionLine(fromId, toId, 'line-base-to-search');
+            }} else {{
+                // 清空线条
+                document.getElementById('line-base-to-search').setAttribute('d', '');
+            }}
+
+            // 3. 搜索词 → 搜索结果
+            if (selectedSearchWordIdx !== null) {{
+                const fromId = `searchword-${{selectedFeatureIdx}}-${{selectedBaseWordIdx}}-${{selectedSearchWordIdx}}`;
+                const toId = 'detailArea';
+                drawConnectionLine(fromId, toId, 'line-search-to-detail');
+            }} else {{
+                // 清空线条
+                document.getElementById('line-search-to-detail').setAttribute('d', '');
+            }}
+        }}
+
+        // 监听滚动和resize事件,更新连接线位置
+        function setupScrollListeners() {{
+            // 监听所有可能滚动的容器
+            const scrollContainers = [
+                document.getElementById('leftSidebar'),
+                document.getElementById('middleSidebar'),
+                document.getElementById('rightSidebar'),
+                document.getElementById('detailArea')
+            ];
+
+            scrollContainers.forEach(container => {{
+                if (container) {{
+                    container.addEventListener('scroll', () => {{
+                        updateConnectionLines();
+                    }}, {{ passive: true }});
+                }}
+            }});
+
+            // 监听窗口大小变化
+            window.addEventListener('resize', () => {{
+                updateConnectionLines();
+            }});
+
+            // 监听窗口滚动
+            window.addEventListener('scroll', () => {{
+                updateConnectionLines();
+            }}, {{ passive: true }});
+        }}
+
         // ========== 级联交互函数 ==========
 
         // 选择特征 - 显示middle sidebar
@@ -3560,6 +3759,18 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             document.getElementById('rightSidebar').classList.remove('active');
             document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
             document.getElementById('detailContent').innerHTML = '';
+
+            // 自动选择第一个base_word
+            const feature = data[featureIdx];
+            const groups = feature['组合评估结果_分组'] || [];
+            if (groups.length > 0) {{
+                // 延迟执行,确保DOM渲染完成
+                setTimeout(() => {{
+                    selectBaseWord(featureIdx, 0);
+                }}, 50);
+            }} else {{
+                updateConnectionLines();
+            }}
         }}
 
         // 渲染middle sidebar - 显示base_words
@@ -3618,6 +3829,21 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             // 隐藏detail content
             document.getElementById('detailArea').querySelector('.detail-placeholder').style.display = 'flex';
             document.getElementById('detailContent').innerHTML = '';
+
+            // 更新连接线:选题点 → 人设
+            updateConnectionLines();
+
+            // 自动选择第一个搜索词
+            const feature = data[featureIdx];
+            const groups = feature['组合评估结果_分组'] || [];
+            const group = groups[baseWordIdx];
+            const searches = group['top10_searches'] || [];
+            if (searches.length > 0) {{
+                // 延迟执行,确保DOM渲染完成
+                setTimeout(() => {{
+                    selectSearchWord(featureIdx, baseWordIdx, 0);
+                }}, 50);
+            }}
         }}
 
         // 渲染right sidebar - 显示search words
@@ -3734,6 +3960,11 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
 
             // 渲染detail area
             renderDetailArea(featureIdx, baseWordIdx, swIdx);
+
+            // 更新连接线:人设 → 搜索词
+            setTimeout(() => {{
+                updateConnectionLines();
+            }}, 50);
         }}
 
         // 渲染detail area - 显示搜索结果
@@ -4801,22 +5032,14 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                 renderLeftSidebar();
                 console.log('✅ [系统] 左侧导航(级联模式)渲染完成');
 
-                // 级联模式:自动选择第一个特征
+                // 设置滚动监听器
+                setupScrollListeners();
+                console.log('✅ [系统] 滚动监听器已设置');
+
+                // 级联模式:自动选择第一个特征(会自动级联选择base_word和search_word)
                 if (data.length > 0) {{
                     selectFeature(0);
-                    console.log('✅ [系统] 自动选择第一个特征');
-
-                    const firstGroups = data[0]['组合评估结果_分组'];
-                    if (firstGroups && firstGroups.length > 0) {{
-                        selectBaseWord(0, 0);
-                        console.log('✅ [系统] 自动选择第一个base_word');
-
-                        const firstSearches = firstGroups[0]['top10_searches'];
-                        if (firstSearches && firstSearches.length > 0) {{
-                            selectSearchWord(0, 0, 0);
-                            console.log('✅ [系统] 自动选择第一个搜索词');
-                        }}
-                    }}
+                    console.log('✅ [系统] 自动选择第一个特征(级联选择)');
                 }}
 
                 console.log('✅ [系统] 页面初始化完成');