Преглед на файлове

异常搜索结果不到的兼容 可视化修改

刘立冬 преди 2 седмици
родител
ревизия
33d4b691b2
променени са 4 файла, в които са добавени 162 реда и са изтрити 78 реда
  1. 4 3
      src/analyzers/post_deconstruction_analyzer.py
  2. 2 6
      src/clients/xiaohongshu_search.py
  3. 7 2
      src/evaluators/llm_evaluator.py
  4. 149 67
      src/visualizers/deconstruction_visualizer.py

+ 4 - 3
src/analyzers/post_deconstruction_analyzer.py

@@ -118,11 +118,12 @@ class PostDeconstructionAnalyzer:
                     source_word = search_item.get('source_word', '')
                     evaluation = search_item.get('evaluation_with_filter', {})
 
-                    # 检查是否有搜索结果
-                    if 'search_result' not in search_item:
+                    # 检查是否有搜索结果(同时检查键存在且值不为None)
+                    search_result = search_item.get('search_result')
+                    if not search_result:
                         continue
 
-                    notes = search_item['search_result'].get('data', {}).get('data', [])
+                    notes = search_result.get('data', {}).get('data', [])
 
                     # 遍历评估结果
                     for note_eval in evaluation.get('notes_evaluation', []):

+ 2 - 6
src/clients/xiaohongshu_search.py

@@ -198,7 +198,6 @@ class XiaohongshuSearch:
     def _preprocess_response(self, result: Dict[str, Any]) -> None:
         """
         预处理搜索结果,将 image_list 中的字典格式转换为 URL 字符串列表
-        并限制返回的帖子数量为10个
 
         Args:
             result: API返回的原始结果字典(会直接修改)
@@ -207,11 +206,8 @@ class XiaohongshuSearch:
         data_wrapper = result.get("data", {})
         notes = data_wrapper.get("data", [])
 
-        # 限制为前10个帖子
-        if len(notes) > 10:
-            notes = notes[:10]
-            data_wrapper["data"] = notes
-            logger.info(f"  限制搜索结果为前10个帖子")
+        # 不再限制帖子数量,保留API返回的所有结果
+        # 数量控制由后续的评估阶段通过 evaluation_max_notes_per_query 参数处理
 
         for note in notes:
             note_card = note.get("note_card", {})

+ 7 - 2
src/evaluators/llm_evaluator.py

@@ -1479,8 +1479,10 @@ Query与目标特征的关系:
                         second_layer_result = future.result()
 
                         # 合并两层评估结果
+                        note_index = note_info["note_index"]
                         merged_result = {
-                            "note_index": note_info["note_index"],
+                            "note_index": note_index,
+                            "note_id": notes_to_eval[note_index].get('id'),  # 添加note_id用于关联解构数据
                             "Query相关性": "相关",
                             "综合得分": second_layer_result.get("综合得分", 0.0),  # 0-1分制
                             "匹配类型": second_layer_result.get("匹配类型", ""),
@@ -1493,8 +1495,10 @@ Query与目标特征的关系:
                     except Exception as e:
                         logger.error(f"      [第二层] 评估笔记 {note_info['note_index']} 失败: {e}")
                         # 失败的笔记也加入结果
+                        note_index = note_info["note_index"]
                         evaluated_notes.append({
-                            "note_index": note_info["note_index"],
+                            "note_index": note_index,
+                            "note_id": notes_to_eval[note_index].get('id'),  # 添加note_id用于关联解构数据
                             "Query相关性": "相关",
                             "综合得分": 0.0,
                             "匹配类型": "评估失败",
@@ -1513,6 +1517,7 @@ Query与目标特征的关系:
             if relevance == "不相关":
                 evaluated_notes.append({
                     "note_index": idx,
+                    "note_id": notes_to_eval[idx].get('id'),  # 添加note_id用于关联解构数据
                     "Query相关性": "不相关",
                     "综合得分": 0.0,
                     "匹配类型": "过滤",

+ 149 - 67
src/visualizers/deconstruction_visualizer.py

@@ -4098,6 +4098,7 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                 const matchType = noteEval['匹配类型'] || '未评估';
                 const explanation = noteEval['说明'] || noteEval['评分说明'] || '';
                 const firstLayerEval = noteEval['第一层评估'] || {{}};
+                const secondLayerEval = noteEval['第二层评估'] || {{}};
 
                 // 根据匹配类型确定样式
                 let matchClass = '';
@@ -4131,16 +4132,27 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
 
                 const evalDetailId = `eval-detail-${{featureIdx}}-${{baseWordIdx}}-${{swIdx}}-${{noteIdx}}`;
 
-                // 检查是否有解构数据,且仅当完全匹配时显示解构按钮
+                // 检查是否有解构数据和相似度数据
+                const hasSimilarityInfo = !!similarityData[noteId];
+                let maxSimilarityScore = 0;
+                if (hasSimilarityInfo) {{
+                    const features = similarityData[noteId].deconstructed_features || [];
+                    if (features.length > 0) {{
+                        maxSimilarityScore = Math.max(...features.map(f => f.similarity_score || 0));
+                    }}
+                }}
                 const hasDeconstruction = deconstructionData[noteId] != null && matchType.includes('完全匹配');
 
+                const cardId = `card-${{featureIdx}}-${{baseWordIdx}}-${{swIdx}}-${{noteIdx}}`;
                 html += `
-                    <div class="note-card ${{matchClass}}" style="border:2px solid #fbbf24;border-radius:12px;overflow:hidden;background:white;transition:all 0.2s;cursor:pointer;" onclick="openNoteImagesModal(${{featureIdx}}, ${{baseWordIdx}}, ${{swIdx}}, ${{noteIdx}})">
+                    <div class="note-card ${{matchClass}}" style="border:2px solid #fbbf24;border-radius:12px;overflow:hidden;background:white;transition:all 0.2s;cursor:pointer;" onclick="openAllNotesModal(` + featureIdx + `, ` + baseWordIdx + `, ` + swIdx + `, ` + noteIdx + `)">
                         <!-- 图片轮播区域 -->
-                        <div style="position:relative;width:100%;height:260px;background:#f3f4f6;">
-                            ${{cover ? `<img src="${{cover}}" style="width:100%;height:100%;object-fit:cover;">` : `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9ca3af;">${{typeIcon}}</div>`}}
+                        <div id="${{cardId}}" style="position:relative;width:100%;height:260px;background:#f3f4f6;" data-current-index="0" data-images='${{JSON.stringify(imageList)}}'>
+                            <img src="${{imageList[0] || ''}}" style="width:100%;height:100%;object-fit:cover;" onerror="this.parentElement.innerHTML='<div style=\\"width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9ca3af;\\">${{typeIcon}}</div>'">
+
+                            <!-- 图片计数器 -->
                             <div style="position:absolute;top:10px;right:10px;background:rgba(0,0,0,0.6);color:white;padding:4px 10px;border-radius:20px;font-size:12px;font-weight:600;">
-                                1/${{imageList.length || 1}}
+                                <span id="${{cardId}}-counter">1</span>/${{imageList.length || 1}}
                             </div>
                         </div>
 
@@ -4163,18 +4175,25 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                             </div>
 
                             <!-- 评分徽章 -->
-                            <div style="display:flex;align-items:center;justify-content:space-between;padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:8px;">
-                                <span style="font-size:12px;color:${{matchColor}};font-weight:600;">
-                                    ${{matchIcon}} ${{matchType}} (${{comprehensiveScore.toFixed(1)}}分)
-                                </span>
-                                <button onclick="event.stopPropagation(); toggleEvalDetail('${{evalDetailId}}')"
-                                        style="font-size:11px;color:#667eea;background:none;border:none;cursor:pointer;padding:2px 6px;">
-                                    详情 ▼
-                                </button>
+                            <div style="padding:8px;background:#f9fafb;border-radius:6px;margin-bottom:8px;">
+                                <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
+                                    <span style="font-size:12px;color:${{matchColor}};font-weight:600;">
+                                        ${{matchIcon}} 搜索结果与目标匹配: ${{comprehensiveScore.toFixed(2)}}
+                                    </span>
+                                    <button onclick="event.stopPropagation(); toggleEvalDetail('${{evalDetailId}}')"
+                                            style="font-size:11px;color:#667eea;background:none;border:none;cursor:pointer;padding:2px 6px;">
+                                        详情 ▼
+                                    </button>
+                                </div>
+                                ${{hasSimilarityInfo ? `
+                                    <div style="font-size:11px;color:#6b7280;margin-top:4px;">
+                                        解构结果与目标匹配: ${{maxSimilarityScore.toFixed(2)}}
+                                    </div>
+                                ` : ''}}
                             </div>
 
                             <!-- 评估详情(可展开) -->
-                            <div id="${{evalDetailId}}" style="display:none;font-size:11px;color:#6b7280;padding:8px;background:#fef3c7;border-radius:6px;margin-top:8px;max-height:150px;overflow-y:auto;">
+                            <div id="${{evalDetailId}}" style="display:none;font-size:11px;color:#6b7280;padding:8px;background:#fef3c7;border-radius:6px;margin-top:8px;max-height:200px;overflow-y:auto;">
                                 <div style="margin-bottom:6px;">
                                     <strong style="color:#92400e;">Query相关性:</strong> ${{queryRelevance}}
                                 </div>
@@ -4184,8 +4203,21 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
                                     </div>
                                 ` : ''}}
                                 ${{firstLayerEval['说明'] ? `
+                                    <div style="margin-bottom:6px;">
+                                        <strong style="color:#92400e;">第一层评估(与query比较):</strong> ${{firstLayerEval['说明']}}
+                                    </div>
+                                ` : ''}}
+                                ${{secondLayerEval['评分说明'] ? `
+                                    <div style="margin-bottom:6px;">
+                                        <strong style="color:#92400e;">第二层评估(与目标比较):</strong> ${{secondLayerEval['评分说明']}}
+                                    </div>
+                                ` : ''}}
+                                ${{secondLayerEval['关键匹配点'] && secondLayerEval['关键匹配点'].length > 0 ? `
                                     <div>
-                                        <strong style="color:#92400e;">第一层评估:</strong> ${{firstLayerEval['说明']}}
+                                        <strong style="color:#92400e;">关键匹配点:</strong>
+                                        <ul style="margin:4px 0 0 0;padding-left:20px;">
+                                            ${{secondLayerEval['关键匹配点'].map(point => `<li>${{point}}</li>`).join('')}}
+                                        </ul>
                                     </div>
                                 ` : ''}}
                             </div>
@@ -5158,104 +5190,154 @@ def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any],
             }});
         }}
 
-        // ========== 新增功能2: 帖子图片浮层模态窗口 ==========
-        function openNoteImagesModal(featureIdx, groupIdx, searchIdx, noteIdx) {{
-            console.log('🎯 [点击事件] 参数 - featureIdx:', featureIdx, 'groupIdx:', groupIdx, 'searchIdx:', searchIdx, 'noteIdx:', noteIdx);
+        // ========== 卡片图片切换功能 ==========
+        function changeCardImage(cardId, direction) {{
+            const card = document.getElementById(cardId);
+            if (!card) return;
 
-            const feature = data[featureIdx];
-            console.log('📊 [数据检查] feature:', feature);
+            const images = JSON.parse(card.getAttribute('data-images') || '[]');
+            if (images.length <= 1) return;
 
-            const group = feature['组合评估结果_分组'][groupIdx];
-            console.log('📊 [数据检查] group:', group);
+            let currentIndex = parseInt(card.getAttribute('data-current-index') || '0');
+            currentIndex = (currentIndex + direction + images.length) % images.length;
+
+            // 更新图片
+            const img = card.querySelector('img');
+            if (img) {{
+                img.src = images[currentIndex];
+            }}
+
+            // 更新索引
+            card.setAttribute('data-current-index', currentIndex);
+
+            // 更新计数器
+            const counter = document.getElementById(cardId + '-counter');
+            if (counter) {{
+                counter.textContent = currentIndex + 1;
+            }}
+        }}
 
+        // ========== 新增功能2: 帖子图片浮层模态窗口(显示当前帖子的所有图片平铺) ==========
+        function openAllNotesModal(featureIdx, groupIdx, searchIdx, initialNoteIdx) {{
+            const feature = data[featureIdx];
+            const group = feature['组合评估结果_分组'][groupIdx];
             const search = group['top10_searches'][searchIdx];
-            console.log('📊 [数据检查] search:', search);
+            const searchWord = search.search_word || '搜索词';
 
             const searchResult = search.search_result || {{}};
-            console.log('📊 [数据检查] searchResult:', searchResult);
-
             const notes = searchResult.data?.data || [];
-            console.log('📊 [数据检查] notes数组长度:', notes.length);
-
-            const note = notes[noteIdx];
-            console.log('📊 [数据检查] note:', note);
+            const note = notes[initialNoteIdx];
 
             if (!note) {{
-                console.log('❌ [浮层] 找不到帖子数据');
+                console.log('❌ 找不到帖子数据');
                 return;
             }}
 
             const card = note.note_card || {{}};
-            console.log('📊 [数据检查] note_card:', card);
-
             const images = card.image_list || [];
-            console.log('📊 [数据检查] image_list:', images);
-            console.log('📊 [数据检查] image_list类型:', typeof images, 'isArray:', Array.isArray(images));
-
             const title = card.display_title || '无标题';
             const noteId = note.id || '';
 
-            console.log('🔍 [帖子图片浮层] 帖子ID:', noteId);
-            console.log('🔍 [帖子图片浮层] 标题:', title);
-            console.log('🔍 [帖子图片浮层] 图片数量:', images.length);
-            console.log('🔍 [帖子图片浮层] 图片数组内容:', images);
-
             if (images.length === 0) {{
-                console.log('❌ [浮层] 该帖子没有图片');
-                console.log('❌ [浮层] card完整内容:', JSON.stringify(card, null, 2));
+                console.log('❌ 该帖子没有图片');
                 return;
             }}
 
+            // 显示模态窗口
             const modal = document.getElementById('notesModal');
             const modalContent = document.getElementById('notesModalContent');
 
-            // 更新模态窗口标题
-            const modalTitle = document.querySelector('.notes-modal-title');
-            if (modalTitle) {{
-                modalTitle.innerHTML = `
-                    <div>
-                        <div style="font-size: 18px; font-weight: 600;">📷 ${{title}}</div>
-                        <div style="font-size: 14px; opacity: 0.9; margin-top: 5px;">共 ${{images.length}} 张图片</div>
+            // 生成所有图片的HTML(网格布局,自适应高度)
+            let imagesHtml = '';
+            images.forEach((imageUrl, imgIdx) => {{
+                imagesHtml += `
+                    <div style="position: relative; background: #2a2a2a; border-radius: 8px; overflow: hidden;">
+                        <img src="${{imageUrl}}"
+                             alt="图片 ${{imgIdx + 1}}"
+                             style="width: 100%; height: auto; display: block; cursor: zoom-in;"
+                             onclick="openFullscreenImage('${{imageUrl}}'); event.stopPropagation();">
+                        <div style="position: absolute; bottom: 8px; left: 8px; padding: 4px 8px; background: rgba(0,0,0,0.7); color: white; font-size: 11px; border-radius: 4px;">
+                            ${{imgIdx + 1}}/${{images.length}}
+                        </div>
                     </div>
                 `;
-            }}
+            }});
 
-            // 构建图片网格HTML
-            let imagesHtml = '<div class="notes-grid-modal">';
+            modalContent.innerHTML = `
+                <div style="position: relative; width: 100%; height: 100%; display: flex; flex-direction: column;">
+                    <!-- 关闭按钮(右上角) -->
+                    <button onclick="closeNotesModal(); event.stopPropagation();"
+                            class="modal-close-btn"
+                            style="position: absolute; top: 20px; right: 20px; z-index: 1000;
+                                   width: 40px; height: 40px; background: rgba(0,0,0,0.7); color: white; border: none; border-radius: 50%;
+                                   cursor: pointer; font-size: 20px; display: flex; align-items: center; justify-content: center;
+                                   transition: all 0.3s;"
+                            onmouseover="this.style.background='rgba(0,0,0,0.9)'"
+                            onmouseout="this.style.background='rgba(0,0,0,0.7)'">
+                        ✕
+                    </button>
 
-            images.forEach((imageUrl, idx) => {{
-                imagesHtml += `
-                    <div class="note-card-modal" onclick="window.open('https://www.xiaohongshu.com/explore/${{noteId}}', '_blank'); event.stopPropagation();">
-                        <div class="note-image-wrapper" style="height: 300px;">
-                            <img src="${{imageUrl}}" alt="图片 ${{idx + 1}}" class="note-image" style="object-fit: contain; background: #000;"/>
-                        </div>
-                        <div class="note-content">
-                            <div style="text-align: center; color: #6b7280; font-size: 13px;">
-                                第 ${{idx + 1}} / ${{images.length}} 张
-                            </div>
+                    <!-- 标题栏 -->
+                    <div style="padding: 20px 70px 20px 20px; background: rgba(0,0,0,0.9); color: white; border-bottom: 1px solid rgba(255,255,255,0.1);">
+                        <div style="font-size: 14px; opacity: 0.7; margin-bottom: 8px;">🔍 ${{searchWord}}</div>
+                        <div style="font-size: 18px; font-weight: 600; margin-bottom: 5px;">📷 ${{title}}</div>
+                        <div style="font-size: 14px; opacity: 0.8;">
+                            共 ${{images.length}} 张图片
                         </div>
                     </div>
-                `;
-            }});
 
-            imagesHtml += '</div>';
+                    <!-- 图片网格容器 -->
+                    <div style="flex: 1; background: #1a1a1a; overflow-y: auto; padding: 20px; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; align-content: start;">
+                        ${{imagesHtml}}
+                    </div>
+                </div>
+            `;
 
-            modalContent.innerHTML = imagesHtml;
             modal.classList.add('active');
 
             // 点击背景关闭
             modal.onclick = function(e) {{
-                if (e.target === modal) {{
+                if (e.target === modal || e.target.classList.contains('modal-close-btn')) {{
                     closeNotesModal();
                 }}
             }};
         }}
 
+        // 全屏查看图片
+        function openFullscreenImage(imageUrl) {{
+            const fullscreenDiv = document.createElement('div');
+            fullscreenDiv.style.cssText = `
+                position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
+                background: rgba(0,0,0,0.95); z-index: 10000;
+                display: flex; align-items: center; justify-content: center;
+                cursor: zoom-out;
+            `;
+
+            const img = document.createElement('img');
+            img.src = imageUrl;
+            img.style.cssText = 'max-width: 95%; max-height: 95%; object-fit: contain;';
+
+            fullscreenDiv.appendChild(img);
+            document.body.appendChild(fullscreenDiv);
+
+            fullscreenDiv.onclick = function() {{
+                document.body.removeChild(fullscreenDiv);
+            }};
+        }}
+
         function closeNotesModal() {{
             const modal = document.getElementById('notesModal');
             modal.classList.remove('active');
         }}
 
+        // 键盘事件:ESC关闭浮层
+        document.addEventListener('keydown', function(e) {{
+            const modal = document.getElementById('notesModal');
+            if (modal.classList.contains('active') && e.key === 'Escape') {{
+                closeNotesModal();
+            }}
+        }});
+
         // ========== 新增功能3: 右侧滚动与左侧联动 ==========
         // 使用Intersection Observer监听右侧内容块的可见性
         const observerOptions = {{