刘立冬 3 settimane fa
parent
commit
815438eb08
2 ha cambiato i file con 1609 aggiunte e 7 eliminazioni
  1. 222 7
      enhanced_search_v2.py
  2. 1387 0
      visualize_stage6_results.py

+ 222 - 7
enhanced_search_v2.py

@@ -46,7 +46,12 @@ class EnhancedSearchV2:
         output_dir: str = "output_v2",
         top_n: int = 10,
         max_total_searches: Optional[int] = None,
-        search_max_workers: int = 3
+        search_max_workers: int = 3,
+        max_searches_per_feature: Optional[int] = None,
+        max_searches_per_base_word: Optional[int] = None,
+        enable_stage6: bool = False,
+        stage6_max_workers: int = 10,
+        stage6_max_notes: int = 20
     ):
         """
         初始化系统
@@ -60,6 +65,11 @@ class EnhancedSearchV2:
             top_n: 每个原始特征取评分最高的N个搜索词(默认10)
             max_total_searches: 全局最大搜索次数限制(默认None不限制)
             search_max_workers: 搜索并发数(默认3)
+            max_searches_per_feature: 每个原始特征的最大搜索次数(默认None不限制)
+            max_searches_per_base_word: 每个base_word的最大搜索次数(默认None不限制)
+            enable_stage6: 是否启用Stage 6评估(默认False)
+            stage6_max_workers: Stage 6并发评估数(默认10)
+            stage6_max_notes: 每个搜索结果评估的最大帖子数(默认20)
         """
         self.how_json_path = how_json_path
         self.dimension_associations_path = dimension_associations_path
@@ -68,6 +78,11 @@ class EnhancedSearchV2:
         self.top_n = top_n
         self.max_total_searches = max_total_searches
         self.search_max_workers = search_max_workers
+        self.max_searches_per_feature = max_searches_per_feature
+        self.max_searches_per_base_word = max_searches_per_base_word
+        self.enable_stage6 = enable_stage6
+        self.stage6_max_workers = stage6_max_workers
+        self.stage6_max_notes = stage6_max_notes
 
         # 创建输出目录
         os.makedirs(output_dir, exist_ok=True)
@@ -1085,6 +1100,7 @@ class EnhancedSearchV2:
                     base_word = group.get('base_word', '')
                     base_similarity = group.get('base_word_similarity', 0)
 
+                    base_word_searches = []
                     for eval_item in group.get('top10_searches', []):
                         sw = eval_item.get('search_word')
                         if not sw:
@@ -1092,13 +1108,20 @@ class EnhancedSearchV2:
 
                         score = eval_item.get('score', 0.0)
 
-                        feature_search_groups[original_feature].append({
+                        base_word_searches.append({
                             'search_word': sw,
                             'score': score,
                             'base_word': base_word,
                             'base_word_similarity': base_similarity,
                             'feature_ref': eval_item  # 引用评估项,用于写入搜索结果
                         })
+
+                    # 应用每个base_word的搜索次数限制
+                    if self.max_searches_per_base_word and len(base_word_searches) > self.max_searches_per_base_word:
+                        logger.info(f"  应用base_word限制: {base_word} 从 {len(base_word_searches)} 减少到 {self.max_searches_per_base_word}")
+                        base_word_searches = base_word_searches[:self.max_searches_per_base_word]
+
+                    feature_search_groups[original_feature].extend(base_word_searches)
             else:
                 # 兼容旧结构(组合评估结果)
                 for eval_item in feature_result.get('组合评估结果', []):
@@ -1114,6 +1137,11 @@ class EnhancedSearchV2:
                         'feature_ref': eval_item
                     })
 
+            # 应用每个原始特征的搜索次数限制
+            if self.max_searches_per_feature and len(feature_search_groups[original_feature]) > self.max_searches_per_feature:
+                logger.info(f"  应用特征限制: {original_feature} 从 {len(feature_search_groups[original_feature])} 减少到 {self.max_searches_per_feature}")
+                feature_search_groups[original_feature] = feature_search_groups[original_feature][:self.max_searches_per_feature]
+
         # 收集所有搜索任务(分组结构下执行所有base_word的top10,不再过滤)
         all_searches = []
         total_count = 0
@@ -1257,6 +1285,146 @@ class EnhancedSearchV2:
             max_workers=20  # 20个并发评估每个帖子
         )
 
+    def stage6_evaluate_search_results_with_filter(
+        self,
+        features_data: List[Dict[str, Any]]
+    ) -> List[Dict[str, Any]]:
+        """
+        阶段6:用LLM评估搜索结果(使用两层过滤评估)
+
+        遍历所有搜索结果,使用两层评估机制:
+        1. 第一层:过滤与搜索Query无关的结果
+        2. 第二层:评估与目标特征的匹配度(8-10/6-7/5-6/≤4)
+
+        Args:
+            features_data: 阶段5的数据
+
+        Returns:
+            带评估结果的数据
+        """
+        logger.info("=" * 60)
+        logger.info("阶段6:LLM评估搜索结果(两层过滤评估)")
+        logger.info(f"  并发数: {self.stage6_max_workers}")
+        logger.info(f"  每个搜索最多评估: {self.stage6_max_notes} 个帖子")
+        logger.info("=" * 60)
+
+        # 收集所有需要评估的搜索项
+        search_items_to_evaluate = []
+
+        for feature_result in features_data:
+            original_feature = feature_result['原始特征名称']
+
+            # 从组合评估结果_分组中读取搜索结果
+            grouped_results = feature_result.get('组合评估结果_分组', [])
+
+            if grouped_results:
+                for group in grouped_results:
+                    for eval_item in group.get('top10_searches', []):
+                        # 检查是否有搜索结果
+                        if eval_item.get('search_result') and eval_item.get('search_metadata', {}).get('status') == 'success':
+                            search_items_to_evaluate.append({
+                                'original_feature': original_feature,
+                                'search_item': eval_item,
+                                'base_word': group.get('base_word', '')
+                            })
+            else:
+                # 兼容旧结构
+                for eval_item in feature_result.get('组合评估结果', []):
+                    if eval_item.get('search_result') and eval_item.get('search_metadata', {}).get('status') == 'success':
+                        search_items_to_evaluate.append({
+                            'original_feature': original_feature,
+                            'search_item': eval_item,
+                            'base_word': ''
+                        })
+
+        logger.info(f"共 {len(search_items_to_evaluate)} 个搜索结果需要评估")
+
+        # 并行评估所有搜索结果
+        with ThreadPoolExecutor(max_workers=self.stage6_max_workers) as executor:
+            futures = []
+            for idx, item in enumerate(search_items_to_evaluate, 1):
+                future = executor.submit(
+                    self._evaluate_single_search_with_filter,
+                    idx,
+                    len(search_items_to_evaluate),
+                    item['original_feature'],
+                    item['search_item'],
+                    item['base_word']
+                )
+                futures.append((future, item))
+
+            # 收集结果
+            success_count = 0
+            failed_count = 0
+
+            for future, item in futures:
+                try:
+                    evaluation = future.result()
+                    item['search_item']['evaluation_with_filter'] = evaluation
+                    success_count += 1
+                except Exception as e:
+                    logger.error(f"  评估失败: {item['search_item'].get('search_word', 'unknown')}, 错误: {e}")
+                    item['search_item']['evaluation_with_filter'] = None
+                    failed_count += 1
+
+        logger.info(f"\n评估完成: 成功 {success_count}, 失败 {failed_count}")
+
+        # 保存结果
+        output_path = os.path.join(self.output_dir, "stage6_with_evaluations.json")
+        self._save_json(features_data, output_path)
+
+        logger.info(f"\n" + "=" * 60)
+        logger.info(f"阶段6完成")
+        logger.info("=" * 60)
+
+        return features_data
+
+    def _evaluate_single_search_with_filter(
+        self,
+        idx: int,
+        total: int,
+        original_feature: str,
+        search_item: Dict[str, Any],
+        base_word: str
+    ) -> Dict[str, Any]:
+        """
+        评估单个搜索结果(使用两层过滤)
+
+        Args:
+            idx: 索引
+            total: 总数
+            original_feature: 原始特征
+            search_item: 搜索项(包含search_word和search_result)
+            base_word: 基础词
+
+        Returns:
+            评估结果
+        """
+        search_word = search_item.get('search_word', '')
+        notes = search_item['search_result'].get('data', {}).get('data', [])
+
+        logger.info(f"[{idx}/{total}] 评估: {search_word} (帖子数: {len(notes)})")
+
+        # 调用LLM评估器的批量评估方法
+        evaluation = self.llm_evaluator.batch_evaluate_notes_with_filter(
+            search_query=search_word,
+            target_feature=original_feature,
+            notes=notes,
+            max_notes=self.stage6_max_notes,
+            max_workers=self.stage6_max_workers
+        )
+
+        # 统计信息
+        filtered_count = evaluation.get('filtered_count', 0)
+        evaluated_count = evaluation.get('evaluated_count', 0)
+        match_dist = evaluation.get('match_distribution', {})
+
+        logger.info(f"  ✓ 完成: 过滤 {filtered_count}, 评估 {evaluated_count}, "
+                   f"完全匹配 {match_dist.get('完全匹配(8-10)', 0)}, "
+                   f"相似匹配 {match_dist.get('相似匹配(6-7)', 0)}")
+
+        return evaluation
+
     # ========== 阶段7:扩展搜索 ==========
 
     def stage7_extended_searches(
@@ -1398,14 +1566,23 @@ class EnhancedSearchV2:
             # 阶段5
             stage5_results = self.stage5_execute_searches(stage4_results, search_delay=2.0, top_n=self.top_n)
 
-            # 阶段6 - 暂时切断执行(代码保留)
-            # stage6_results = self.stage6_evaluate_search_results(stage5_results)
+            # 阶段6 - 条件执行(使用新的两层过滤评估)
+            if self.enable_stage6:
+                stage6_results = self.stage6_evaluate_search_results_with_filter(stage5_results)
+            else:
+                stage6_results = stage5_results
+                logger.info("\n" + "=" * 60)
+                logger.info("阶段6:跳过(未启用)")
+                logger.info("=" * 60)
 
             # 阶段7 - 暂时切断执行(代码保留)
             # final_results = self.stage7_extended_searches(stage6_results, search_delay=2.0)
 
             logger.info("\n" + "=" * 60)
-            logger.info("✓ 完整流程执行完成(Stage1-5)")
+            if self.enable_stage6:
+                logger.info("✓ 完整流程执行完成(Stage1-6)")
+            else:
+                logger.info("✓ 完整流程执行完成(Stage1-5)")
             logger.info("=" * 60)
 
             # 自动执行可视化
@@ -1414,8 +1591,12 @@ class EnhancedSearchV2:
             logger.info("=" * 60)
 
             try:
+                # 根据是否启用stage6选择不同的可视化脚本
+                viz_script = 'visualize_stage6_results.py' if self.enable_stage6 else 'visualize_stage5_results.py'
+                logger.info(f"  使用可视化脚本: {viz_script}")
+
                 result = subprocess.run(
-                    ['python3', 'visualize_stage5_results.py'],
+                    ['python3', viz_script],
                     capture_output=True,
                     text=True,
                     timeout=60
@@ -1484,6 +1665,35 @@ def main():
         default=3,
         help='搜索并发数(默认3)'
     )
+    parser.add_argument(
+        '--max-searches-per-feature',
+        type=int,
+        default=None,
+        help='每个原始特征的最大搜索次数(默认None不限制)'
+    )
+    parser.add_argument(
+        '--max-searches-per-base-word',
+        type=int,
+        default=None,
+        help='每个base_word的最大搜索次数(默认None不限制)'
+    )
+    parser.add_argument(
+        '--enable-stage6',
+        action='store_true',
+        help='启用Stage 6评估(默认False)'
+    )
+    parser.add_argument(
+        '--stage6-max-workers',
+        type=int,
+        default=10,
+        help='Stage 6并发评估数(默认10)'
+    )
+    parser.add_argument(
+        '--stage6-max-notes',
+        type=int,
+        default=20,
+        help='每个搜索结果评估的最大帖子数(默认20)'
+    )
 
     args = parser.parse_args()
 
@@ -1496,7 +1706,12 @@ def main():
         output_dir=args.output_dir,
         top_n=args.top_n,
         max_total_searches=args.max_total_searches,
-        search_max_workers=args.search_workers
+        search_max_workers=args.search_workers,
+        max_searches_per_feature=args.max_searches_per_feature,
+        max_searches_per_base_word=args.max_searches_per_base_word,
+        enable_stage6=args.enable_stage6,
+        stage6_max_workers=args.stage6_max_workers,
+        stage6_max_notes=args.stage6_max_notes
     )
 
     # 执行完整流程

+ 1387 - 0
visualize_stage6_results.py

@@ -0,0 +1,1387 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Stage6评估结果可视化工具
+整合两层评估结果的交互式HTML页面
+"""
+
+import json
+import os
+from datetime import datetime
+from typing import List, Dict, Any
+
+
+def load_data(json_path: str) -> List[Dict[str, Any]]:
+    """加载JSON数据"""
+    with open(json_path, 'r', encoding='utf-8') as f:
+        return json.load(f)
+
+
+def calculate_statistics(data: List[Dict[str, Any]]) -> Dict[str, Any]:
+    """计算统计数据(包括评估结果)"""
+    total_features = len(data)
+    total_search_words = 0
+    total_notes = 0
+    video_count = 0
+    normal_count = 0
+
+    # 评估统计
+    total_evaluated_notes = 0
+    total_filtered = 0
+    match_complete = 0  # 8-10分
+    match_similar = 0   # 6-7分
+    match_weak = 0      # 5-6分
+    match_none = 0      # ≤4分
+
+    for feature in data:
+        grouped_results = feature.get('组合评估结果_分组', [])
+
+        for group in grouped_results:
+            search_items = group.get('top10_searches', [])
+            total_search_words += len(search_items)
+
+            for search_item in search_items:
+                search_result = search_item.get('search_result', {})
+                notes = search_result.get('data', {}).get('data', [])
+                total_notes += len(notes)
+
+                # 统计视频/图文类型
+                for note in notes:
+                    note_type = note.get('note_card', {}).get('type', '')
+                    if note_type == 'video':
+                        video_count += 1
+                    else:
+                        normal_count += 1
+
+                # 统计评估结果
+                evaluation = search_item.get('evaluation_with_filter')
+                if evaluation:
+                    total_evaluated_notes += evaluation.get('total_notes', 0)
+                    total_filtered += evaluation.get('filtered_count', 0)
+
+                    stats = evaluation.get('statistics', {})
+                    match_complete += stats.get('完全匹配(8-10)', 0)
+                    match_similar += stats.get('相似匹配(6-7)', 0)
+                    match_weak += stats.get('弱相似(5-6)', 0)
+                    match_none += stats.get('无匹配(≤4)', 0)
+
+    # 计算百分比
+    total_remaining = total_evaluated_notes - total_filtered if total_evaluated_notes > 0 else 0
+
+    return {
+        'total_features': total_features,
+        'total_search_words': total_search_words,
+        'total_notes': total_notes,
+        'video_count': video_count,
+        'normal_count': normal_count,
+        'video_percentage': round(video_count / total_notes * 100, 1) if total_notes > 0 else 0,
+        'normal_percentage': round(normal_count / total_notes * 100, 1) if total_notes > 0 else 0,
+
+        # 评估统计
+        'total_evaluated': total_evaluated_notes,
+        'total_filtered': total_filtered,
+        'total_remaining': total_remaining,
+        'filter_rate': round(total_filtered / total_evaluated_notes * 100, 1) if total_evaluated_notes > 0 else 0,
+        'match_complete': match_complete,
+        'match_similar': match_similar,
+        'match_weak': match_weak,
+        'match_none': match_none,
+        'complete_rate': round(match_complete / total_remaining * 100, 1) if total_remaining > 0 else 0,
+        'similar_rate': round(match_similar / total_remaining * 100, 1) if total_remaining > 0 else 0,
+    }
+
+
+def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any], output_path: str):
+    """生成HTML可视化页面"""
+
+    # 准备数据JSON(用于JavaScript)
+    data_json = json.dumps(data, ensure_ascii=False, indent=2)
+
+    html_content = f'''<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Stage6 评估结果可视化</title>
+    <style>
+        * {{
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }}
+
+        body {{
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background: #f5f7fa;
+            color: #333;
+            overflow-x: hidden;
+        }}
+
+        /* 顶部统计面板 */
+        .stats-panel {{
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 20px;
+            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+        }}
+
+        .stats-container {{
+            max-width: 1400px;
+            margin: 0 auto;
+        }}
+
+        .stats-row {{
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+            flex-wrap: wrap;
+            gap: 15px;
+            margin-bottom: 15px;
+        }}
+
+        .stats-row:last-child {{
+            margin-bottom: 0;
+            padding-top: 15px;
+            border-top: 1px solid rgba(255,255,255,0.2);
+        }}
+
+        .stat-item {{
+            text-align: center;
+        }}
+
+        .stat-value {{
+            font-size: 28px;
+            font-weight: bold;
+            margin-bottom: 5px;
+        }}
+
+        .stat-label {{
+            font-size: 12px;
+            opacity: 0.9;
+        }}
+
+        .stat-item.small .stat-value {{
+            font-size: 22px;
+        }}
+
+        /* 过滤控制面板 */
+        .filter-panel {{
+            background: white;
+            max-width: 1400px;
+            margin: 20px auto;
+            padding: 15px 20px;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            display: flex;
+            align-items: center;
+            gap: 20px;
+            flex-wrap: wrap;
+        }}
+
+        .filter-label {{
+            font-weight: 600;
+            color: #374151;
+        }}
+
+        .filter-buttons {{
+            display: flex;
+            gap: 10px;
+            flex-wrap: wrap;
+        }}
+
+        .filter-btn {{
+            padding: 6px 12px;
+            border: 2px solid #e5e7eb;
+            background: white;
+            border-radius: 6px;
+            cursor: pointer;
+            font-size: 13px;
+            font-weight: 500;
+            transition: all 0.2s;
+        }}
+
+        .filter-btn:hover {{
+            border-color: #667eea;
+            background: #f9fafb;
+        }}
+
+        .filter-btn.active {{
+            border-color: #667eea;
+            background: #667eea;
+            color: white;
+        }}
+
+        .filter-btn.complete {{
+            border-color: #10b981;
+        }}
+        .filter-btn.complete.active {{
+            background: #10b981;
+            border-color: #10b981;
+        }}
+
+        .filter-btn.similar {{
+            border-color: #f59e0b;
+        }}
+        .filter-btn.similar.active {{
+            background: #f59e0b;
+            border-color: #f59e0b;
+        }}
+
+        .filter-btn.weak {{
+            border-color: #f97316;
+        }}
+        .filter-btn.weak.active {{
+            background: #f97316;
+            border-color: #f97316;
+        }}
+
+        .filter-btn.none {{
+            border-color: #ef4444;
+        }}
+        .filter-btn.none.active {{
+            background: #ef4444;
+            border-color: #ef4444;
+        }}
+
+        .filter-btn.filtered {{
+            border-color: #6b7280;
+        }}
+        .filter-btn.filtered.active {{
+            background: #6b7280;
+            border-color: #6b7280;
+        }}
+
+        /* 主容器 */
+        .main-container {{
+            display: flex;
+            max-width: 1400px;
+            margin: 0 auto 20px;
+            gap: 20px;
+            padding: 0 20px;
+            height: calc(100vh - 260px);
+        }}
+
+        /* 左侧导航 */
+        .left-sidebar {{
+            width: 30%;
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            overflow-y: auto;
+            position: sticky;
+            top: 20px;
+            height: fit-content;
+            max-height: calc(100vh - 280px);
+        }}
+
+        .feature-group {{
+            border-bottom: 1px solid #e5e7eb;
+        }}
+
+        .feature-header {{
+            padding: 15px 20px;
+            background: #f9fafb;
+            cursor: pointer;
+            user-select: none;
+            transition: background 0.2s;
+        }}
+
+        .feature-header:hover {{
+            background: #f3f4f6;
+        }}
+
+        .feature-header.active {{
+            background: #667eea;
+            color: white;
+        }}
+
+        .feature-title {{
+            font-size: 16px;
+            font-weight: 600;
+            margin-bottom: 5px;
+        }}
+
+        .feature-meta {{
+            font-size: 12px;
+            color: #6b7280;
+        }}
+
+        .feature-header.active .feature-meta {{
+            color: rgba(255,255,255,0.8);
+        }}
+
+        .search-words-list {{
+            display: none;
+            padding: 0;
+        }}
+
+        .search-words-list.expanded {{
+            display: block;
+        }}
+
+        /* Base word分组层 */
+        .base-word-group {{
+            border-bottom: 1px solid #f3f4f6;
+        }}
+
+        .base-word-header {{
+            padding: 12px 20px 12px 30px;
+            background: #fafbfc;
+            cursor: pointer;
+            user-select: none;
+            transition: all 0.2s;
+            border-left: 3px solid transparent;
+        }}
+
+        .base-word-header:hover {{
+            background: #f3f4f6;
+            border-left-color: #a78bfa;
+        }}
+
+        .base-word-header.active {{
+            background: #f3f4f6;
+            border-left-color: #7c3aed;
+        }}
+
+        .base-word-title {{
+            font-size: 15px;
+            font-weight: 600;
+            color: #7c3aed;
+            margin-bottom: 4px;
+        }}
+
+        .base-word-meta {{
+            font-size: 11px;
+            color: #6b7280;
+        }}
+
+        .base-word-desc {{
+            padding: 8px 20px 8px 30px;
+            background: #fefce8;
+            font-size: 12px;
+            color: #854d0e;
+            line-height: 1.5;
+            border-left: 3px solid #fbbf24;
+            display: none;
+        }}
+
+        .base-word-desc.expanded {{
+            display: block;
+        }}
+
+        .search-words-sublist {{
+            display: none;
+        }}
+
+        .search-words-sublist.expanded {{
+            display: block;
+        }}
+
+        .search-word-item {{
+            padding: 12px 20px 12px 50px;
+            cursor: pointer;
+            border-left: 3px solid transparent;
+            transition: all 0.2s;
+        }}
+
+        .search-word-item:hover {{
+            background: #f9fafb;
+            border-left-color: #667eea;
+        }}
+
+        .search-word-item.active {{
+            background: #ede9fe;
+            border-left-color: #7c3aed;
+        }}
+
+        .search-word-text {{
+            font-size: 14px;
+            font-weight: 500;
+            color: #374151;
+            margin-bottom: 4px;
+        }}
+
+        .search-word-score {{
+            display: inline-block;
+            padding: 2px 8px;
+            border-radius: 12px;
+            font-size: 11px;
+            font-weight: 600;
+            margin-left: 8px;
+        }}
+
+        .score-high {{
+            background: #d1fae5;
+            color: #065f46;
+        }}
+
+        .score-medium {{
+            background: #fef3c7;
+            color: #92400e;
+        }}
+
+        .score-low {{
+            background: #fee2e2;
+            color: #991b1b;
+        }}
+
+        /* 评估徽章 */
+        .eval-badge {{
+            display: inline-block;
+            padding: 2px 6px;
+            border-radius: 10px;
+            font-size: 11px;
+            font-weight: 600;
+            margin-left: 6px;
+        }}
+
+        .eval-complete {{
+            background: #d1fae5;
+            color: #065f46;
+            border: 1px solid #10b981;
+        }}
+
+        .eval-similar {{
+            background: #fef3c7;
+            color: #92400e;
+            border: 1px solid #f59e0b;
+        }}
+
+        .eval-weak {{
+            background: #fed7aa;
+            color: #9a3412;
+            border: 1px solid #f97316;
+        }}
+
+        .eval-none {{
+            background: #fee2e2;
+            color: #991b1b;
+            border: 1px solid #ef4444;
+        }}
+
+        .eval-filtered {{
+            background: #e5e7eb;
+            color: #4b5563;
+            border: 1px solid #6b7280;
+        }}
+
+        .search-word-eval {{
+            font-size: 11px;
+            color: #6b7280;
+            margin-top: 4px;
+        }}
+
+        /* 右侧结果区 */
+        .right-content {{
+            flex: 1;
+            overflow-y: auto;
+            padding-bottom: 40px;
+        }}
+
+        .result-block {{
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+            margin-bottom: 30px;
+            padding: 20px;
+            scroll-margin-top: 20px;
+        }}
+
+        .result-header {{
+            margin-bottom: 20px;
+            padding-bottom: 15px;
+            border-bottom: 2px solid #e5e7eb;
+        }}
+
+        .result-title {{
+            font-size: 20px;
+            font-weight: 600;
+            color: #111827;
+            margin-bottom: 10px;
+        }}
+
+        .result-stats {{
+            display: flex;
+            gap: 10px;
+            font-size: 12px;
+            color: #6b7280;
+            flex-wrap: wrap;
+        }}
+
+        .stat-badge {{
+            background: #f3f4f6;
+            padding: 4px 10px;
+            border-radius: 4px;
+        }}
+
+        .stat-badge.eval {{
+            font-weight: 600;
+        }}
+
+        .stat-badge.eval.complete {{
+            background: #d1fae5;
+            color: #065f46;
+        }}
+
+        .stat-badge.eval.similar {{
+            background: #fef3c7;
+            color: #92400e;
+        }}
+
+        .stat-badge.eval.weak {{
+            background: #fed7aa;
+            color: #9a3412;
+        }}
+
+        .stat-badge.eval.none {{
+            background: #fee2e2;
+            color: #991b1b;
+        }}
+
+        .stat-badge.eval.filtered {{
+            background: #e5e7eb;
+            color: #4b5563;
+        }}
+
+        /* 帖子网格 */
+        .notes-grid {{
+            display: grid;
+            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+            gap: 20px;
+        }}
+
+        .note-card {{
+            border: 3px solid #e5e7eb;
+            border-radius: 8px;
+            overflow: hidden;
+            cursor: pointer;
+            transition: all 0.3s;
+            background: white;
+        }}
+
+        .note-card:hover {{
+            transform: translateY(-4px);
+            box-shadow: 0 10px 25px rgba(0,0,0,0.15);
+        }}
+
+        /* 根据评估分数设置边框颜色 */
+        .note-card.eval-complete {{
+            border-color: #10b981;
+        }}
+
+        .note-card.eval-similar {{
+            border-color: #f59e0b;
+        }}
+
+        .note-card.eval-weak {{
+            border-color: #f97316;
+        }}
+
+        .note-card.eval-none {{
+            border-color: #ef4444;
+        }}
+
+        .note-card.eval-filtered {{
+            border-color: #6b7280;
+            opacity: 0.6;
+        }}
+
+        /* 图片轮播 */
+        .image-carousel {{
+            position: relative;
+            width: 100%;
+            height: 280px;
+            background: #f3f4f6;
+            overflow: hidden;
+        }}
+
+        .carousel-images {{
+            display: flex;
+            height: 100%;
+            transition: transform 0.3s ease;
+        }}
+
+        .carousel-image {{
+            min-width: 100%;
+            height: 100%;
+            object-fit: cover;
+        }}
+
+        .carousel-btn {{
+            position: absolute;
+            top: 50%;
+            transform: translateY(-50%);
+            background: rgba(0,0,0,0.5);
+            color: white;
+            border: none;
+            width: 32px;
+            height: 32px;
+            border-radius: 50%;
+            cursor: pointer;
+            font-size: 16px;
+            display: none;
+            align-items: center;
+            justify-content: center;
+            transition: background 0.2s;
+            z-index: 10;
+        }}
+
+        .carousel-btn:hover {{
+            background: rgba(0,0,0,0.7);
+        }}
+
+        .carousel-btn.prev {{
+            left: 8px;
+        }}
+
+        .carousel-btn.next {{
+            right: 8px;
+        }}
+
+        .note-card:hover .carousel-btn {{
+            display: flex;
+        }}
+
+        .carousel-indicators {{
+            position: absolute;
+            bottom: 10px;
+            left: 50%;
+            transform: translateX(-50%);
+            display: flex;
+            gap: 6px;
+            z-index: 10;
+        }}
+
+        .dot {{
+            width: 8px;
+            height: 8px;
+            border-radius: 50%;
+            background: rgba(255,255,255,0.5);
+            cursor: pointer;
+            transition: all 0.2s;
+        }}
+
+        .dot.active {{
+            background: white;
+            width: 24px;
+            border-radius: 4px;
+        }}
+
+        .image-counter {{
+            position: absolute;
+            top: 10px;
+            right: 10px;
+            background: rgba(0,0,0,0.6);
+            color: white;
+            padding: 4px 8px;
+            border-radius: 4px;
+            font-size: 12px;
+            z-index: 10;
+        }}
+
+        /* 帖子信息 */
+        .note-info {{
+            padding: 12px;
+        }}
+
+        .note-title {{
+            font-size: 14px;
+            font-weight: 500;
+            color: #111827;
+            margin-bottom: 8px;
+            display: -webkit-box;
+            -webkit-line-clamp: 2;
+            -webkit-box-orient: vertical;
+            overflow: hidden;
+            line-height: 1.4;
+        }}
+
+        .note-meta {{
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            font-size: 12px;
+            color: #6b7280;
+            margin-bottom: 8px;
+        }}
+
+        .note-type {{
+            padding: 3px 8px;
+            border-radius: 4px;
+            font-weight: 500;
+        }}
+
+        .type-video {{
+            background: #dbeafe;
+            color: #1e40af;
+        }}
+
+        .type-normal {{
+            background: #d1fae5;
+            color: #065f46;
+        }}
+
+        .note-author {{
+            display: flex;
+            align-items: center;
+            gap: 6px;
+        }}
+
+        .author-avatar {{
+            width: 24px;
+            height: 24px;
+            border-radius: 50%;
+        }}
+
+        /* 评估信息 */
+        .note-eval {{
+            padding: 8px 12px;
+            background: #f9fafb;
+            border-top: 1px solid #e5e7eb;
+            font-size: 12px;
+        }}
+
+        .note-eval-header {{
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            cursor: pointer;
+            user-select: none;
+        }}
+
+        .note-eval-score {{
+            font-weight: 600;
+        }}
+
+        .note-eval-toggle {{
+            color: #6b7280;
+            font-size: 10px;
+        }}
+
+        .note-eval-details {{
+            margin-top: 8px;
+            padding-top: 8px;
+            border-top: 1px solid #e5e7eb;
+            display: none;
+            line-height: 1.5;
+        }}
+
+        .note-eval-details.expanded {{
+            display: block;
+        }}
+
+        .eval-detail-label {{
+            font-weight: 600;
+            color: #374151;
+            margin-top: 6px;
+            margin-bottom: 2px;
+        }}
+
+        .eval-detail-label:first-child {{
+            margin-top: 0;
+        }}
+
+        .eval-detail-text {{
+            color: #6b7280;
+        }}
+
+        /* 滚动条样式 */
+        ::-webkit-scrollbar {{
+            width: 8px;
+            height: 8px;
+        }}
+
+        ::-webkit-scrollbar-track {{
+            background: #f1f1f1;
+        }}
+
+        ::-webkit-scrollbar-thumb {{
+            background: #888;
+            border-radius: 4px;
+        }}
+
+        ::-webkit-scrollbar-thumb:hover {{
+            background: #555;
+        }}
+
+        /* 隐藏类 */
+        .hidden {{
+            display: none !important;
+        }}
+    </style>
+</head>
+<body>
+    <!-- 统计面板 -->
+    <div class="stats-panel">
+        <div class="stats-container">
+            <div class="stats-row">
+                <div class="stat-item">
+                    <div class="stat-value">📊 {stats['total_features']}</div>
+                    <div class="stat-label">原始特征数</div>
+                </div>
+                <div class="stat-item">
+                    <div class="stat-value">🔍 {stats['total_search_words']}</div>
+                    <div class="stat-label">搜索词数</div>
+                </div>
+                <div class="stat-item">
+                    <div class="stat-value">📝 {stats['total_notes']}</div>
+                    <div class="stat-label">帖子总数</div>
+                </div>
+                <div class="stat-item">
+                    <div class="stat-value">🎬 {stats['video_count']}</div>
+                    <div class="stat-label">视频类型 ({stats['video_percentage']}%)</div>
+                </div>
+                <div class="stat-item">
+                    <div class="stat-value">📷 {stats['normal_count']}</div>
+                    <div class="stat-label">图文类型 ({stats['normal_percentage']}%)</div>
+                </div>
+            </div>
+            <div class="stats-row">
+                <div class="stat-item small">
+                    <div class="stat-value">⚡ {stats['total_evaluated']}</div>
+                    <div class="stat-label">已评估</div>
+                </div>
+                <div class="stat-item small">
+                    <div class="stat-value">⚫ {stats['total_filtered']}</div>
+                    <div class="stat-label">已过滤 ({stats['filter_rate']}%)</div>
+                </div>
+                <div class="stat-item small">
+                    <div class="stat-value">🟢 {stats['match_complete']}</div>
+                    <div class="stat-label">完全匹配 ({stats['complete_rate']}%)</div>
+                </div>
+                <div class="stat-item small">
+                    <div class="stat-value">🟡 {stats['match_similar']}</div>
+                    <div class="stat-label">相似匹配 ({stats['similar_rate']}%)</div>
+                </div>
+                <div class="stat-item small">
+                    <div class="stat-value">🟠 {stats['match_weak']}</div>
+                    <div class="stat-label">弱相似</div>
+                </div>
+                <div class="stat-item small">
+                    <div class="stat-value">🔴 {stats['match_none']}</div>
+                    <div class="stat-label">无匹配</div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 过滤控制面板 -->
+    <div class="filter-panel">
+        <span class="filter-label">🔍 筛选显示:</span>
+        <div class="filter-buttons">
+            <button class="filter-btn active" onclick="filterNotes('all')">全部</button>
+            <button class="filter-btn complete" onclick="filterNotes('complete')">🟢 完全匹配</button>
+            <button class="filter-btn similar" onclick="filterNotes('similar')">🟡 相似匹配</button>
+            <button class="filter-btn weak" onclick="filterNotes('weak')">🟠 弱相似</button>
+            <button class="filter-btn none" onclick="filterNotes('none')">🔴 无匹配</button>
+            <button class="filter-btn filtered" onclick="filterNotes('filtered')">⚫ 已过滤</button>
+        </div>
+    </div>
+
+    <!-- 主容器 -->
+    <div class="main-container">
+        <!-- 左侧导航 -->
+        <div class="left-sidebar" id="leftSidebar">
+            <!-- 通过JavaScript动态生成 -->
+        </div>
+
+        <!-- 右侧结果区 -->
+        <div class="right-content" id="rightContent">
+            <!-- 通过JavaScript动态生成 -->
+        </div>
+    </div>
+
+    <script>
+        // 数据
+        const data = {data_json};
+        let currentFilter = 'all';
+
+        // 创建评估映射(使用索引: "featureIdx-groupIdx-swIdx-noteIdx" -> evaluation)
+        const noteEvaluations = {{}};
+        data.forEach((feature, fIdx) => {{
+            const groups = feature['组合评估结果_分组'] || [];
+            groups.forEach((group, gIdx) => {{
+                const searches = group['top10_searches'] || [];
+                searches.forEach((search, sIdx) => {{
+                    const evaluation = search['evaluation_with_filter'];
+                    if (evaluation && evaluation.notes_evaluation) {{
+                        evaluation.notes_evaluation.forEach(noteEval => {{
+                            const key = `${{fIdx}}-${{gIdx}}-${{sIdx}}-${{noteEval.note_index}}`;
+                            noteEvaluations[key] = noteEval;
+                        }});
+                    }}
+                }});
+            }});
+        }});
+
+        // 获取评估类别
+        function getEvalCategory(noteEval) {{
+            if (!noteEval || noteEval['Query相关性'] !== '相关') {{
+                return 'filtered';
+            }}
+            const score = noteEval['综合得分'];
+            if (score >= 8) return 'complete';
+            if (score >= 6) return 'similar';
+            if (score >= 5) return 'weak';
+            return 'none';
+        }}
+
+        // 渲染左侧导航
+        function renderLeftSidebar() {{
+            const sidebar = document.getElementById('leftSidebar');
+            let html = '';
+
+            data.forEach((feature, featureIdx) => {{
+                const groups = feature['组合评估结果_分组'] || [];
+                let totalSearches = 0;
+                groups.forEach(group => {{
+                    totalSearches += (group['top10_searches'] || []).length;
+                }});
+
+                // 层级1: 原始特征
+                html += `
+                    <div class="feature-group">
+                        <div class="feature-header" onclick="toggleFeature(${{featureIdx}})" id="feature-header-${{featureIdx}}">
+                            <div class="feature-title">${{feature['原始特征名称']}}</div>
+                            <div class="feature-meta">
+                                ${{feature['来源层级']}} · 权重: ${{feature['权重'].toFixed(2)}} · ${{totalSearches}}个搜索词
+                            </div>
+                        </div>
+                        <div class="search-words-list" id="search-words-${{featureIdx}}">
+                `;
+
+                // 层级2: Base word分组
+                groups.forEach((group, groupIdx) => {{
+                    const baseWord = group['base_word'] || '';
+                    const baseSimilarity = group['base_word_similarity'] || 0;
+                    const searches = group['top10_searches'] || [];
+
+                    // 获取相关词汇
+                    const relatedWords = feature['高相似度候选_按base_word']?.[baseWord] || [];
+                    const relatedWordNames = relatedWords.map(w => w['人设特征名称']).slice(0, 10).join('、');
+
+                    html += `
+                        <div class="base-word-group">
+                            <div class="base-word-header" onclick="toggleBaseWord(${{featureIdx}}, ${{groupIdx}})"
+                                 id="base-word-header-${{featureIdx}}-${{groupIdx}}">
+                                <div class="base-word-title">🎯 ${{baseWord}}</div>
+                                <div class="base-word-meta">相似度: ${{baseSimilarity.toFixed(2)}} · ${{searches.length}}个搜索词</div>
+                            </div>
+                            <div class="base-word-desc" id="base-word-desc-${{featureIdx}}-${{groupIdx}}">
+                                ${{relatedWordNames || '无相关词汇'}}
+                            </div>
+                            <div class="search-words-sublist" id="search-words-sublist-${{featureIdx}}-${{groupIdx}}">
+                    `;
+
+                    // 层级3: 搜索词列表
+                    searches.forEach((sw, swIdx) => {{
+                        const score = sw.score || 0;
+                        const scoreClass = score >= 0.9 ? 'score-high' : score >= 0.7 ? 'score-medium' : 'score-low';
+                        const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
+                        const sourceWord = sw.source_word || '';
+
+                        // 获取评估统计
+                        const evaluation = sw['evaluation_with_filter'];
+                        let evalBadges = '';
+                        if (evaluation) {{
+                            const stats = evaluation.statistics || {{}};
+                            const complete = stats['完全匹配(8-10)'] || 0;
+                            const similar = stats['相似匹配(6-7)'] || 0;
+                            const weak = stats['弱相似(5-6)'] || 0;
+                            const none = stats['无匹配(≤4)'] || 0;
+                            const filtered = evaluation.filtered_count || 0;
+
+                            if (complete > 0) evalBadges += `<span class="eval-badge eval-complete">🟢${{complete}}</span>`;
+                            if (similar > 0) evalBadges += `<span class="eval-badge eval-similar">🟡${{similar}}</span>`;
+                            if (weak > 0) evalBadges += `<span class="eval-badge eval-weak">🟠${{weak}}</span>`;
+                            if (none > 0) evalBadges += `<span class="eval-badge eval-none">🔴${{none}}</span>`;
+                            if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
+                        }}
+
+                        html += `
+                            <div class="search-word-item" onclick="scrollToBlock('${{blockId}}')"
+                                 id="sw-${{featureIdx}}-${{groupIdx}}-${{swIdx}}"
+                                 data-block-id="${{blockId}}">
+                                <div class="search-word-text">
+                                    🔍 ${{sw.search_word}}
+                                    <span class="search-word-score ${{scoreClass}}">${{score.toFixed(2)}}</span>
+                                </div>
+                                <div class="search-word-meta" style="font-size:11px;color:#9ca3af;margin-top:2px">
+                                    来源: ${{sourceWord}}
+                                </div>
+                                <div class="search-word-eval">${{evalBadges}}</div>
+                            </div>
+                        `;
+                    }});
+
+                    html += `
+                            </div>
+                        </div>
+                    `;
+                }});
+
+                html += `
+                        </div>
+                    </div>
+                `;
+            }});
+
+            sidebar.innerHTML = html;
+        }}
+
+        // 渲染右侧结果区
+        function renderRightContent() {{
+            const content = document.getElementById('rightContent');
+            let html = '';
+
+            data.forEach((feature, featureIdx) => {{
+                const groups = feature['组合评估结果_分组'] || [];
+
+                groups.forEach((group, groupIdx) => {{
+                    const searches = group['top10_searches'] || [];
+
+                    searches.forEach((sw, swIdx) => {{
+                        const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
+                        const searchResult = sw.search_result || {{}};
+                        const notes = searchResult.data?.data || [];
+
+                        const videoCount = notes.filter(n => n.note_card?.type === 'video').length;
+                        const normalCount = notes.length - videoCount;
+
+                        // 获取评估统计
+                        const evaluation = sw['evaluation_with_filter'];
+                        let evalStats = '';
+                        if (evaluation) {{
+                            const stats = evaluation.statistics || {{}};
+                            const complete = stats['完全匹配(8-10)'] || 0;
+                            const similar = stats['相似匹配(6-7)'] || 0;
+                            const weak = stats['弱相似(5-6)'] || 0;
+                            const none = stats['无匹配(≤4)'] || 0;
+                            const filtered = evaluation.filtered_count || 0;
+
+                            if (complete > 0) evalStats += `<span class="stat-badge eval complete">🟢 完全:${{complete}}</span>`;
+                            if (similar > 0) evalStats += `<span class="stat-badge eval similar">🟡 相似:${{similar}}</span>`;
+                            if (weak > 0) evalStats += `<span class="stat-badge eval weak">🟠 弱:${{weak}}</span>`;
+                            if (none > 0) evalStats += `<span class="stat-badge eval none">🔴 无:${{none}}</span>`;
+                            if (filtered > 0) evalStats += `<span class="stat-badge eval filtered">⚫ 过滤:${{filtered}}</span>`;
+                        }}
+
+                        html += `
+                            <div class="result-block" id="${{blockId}}">
+                                <div class="result-header">
+                                    <div class="result-title">${{sw.search_word}}</div>
+                                    <div class="result-stats">
+                                        <span class="stat-badge">📝 ${{notes.length}} 条帖子</span>
+                                        <span class="stat-badge">🎬 ${{videoCount}} 视频</span>
+                                        <span class="stat-badge">📷 ${{normalCount}} 图文</span>
+                                        ${{evalStats}}
+                                    </div>
+                                </div>
+                                <div class="notes-grid">
+                                    ${{notes.map((note, noteIdx) => renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx)).join('')}}
+                                </div>
+                            </div>
+                        `;
+                    }});
+                }});
+            }});
+
+            content.innerHTML = html;
+        }}
+
+        // 渲染单个帖子卡片
+        function renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx) {{
+            const card = note.note_card || {{}};
+            const images = card.image_list || [];
+            const title = card.display_title || '无标题';
+            const noteType = card.type || 'normal';
+            const noteId = note.id || '';
+            const user = card.user || {{}};
+            const userName = user.nick_name || '未知用户';
+            const userAvatar = user.avatar || '';
+
+            const carouselId = `carousel-${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
+
+            // 获取评估结果(使用索引key)
+            const evalKey = `${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
+            const noteEval = noteEvaluations[evalKey];
+            const evalCategory = getEvalCategory(noteEval);
+            const evalClass = `eval-${{evalCategory}}`;
+
+            let evalSection = '';
+            if (noteEval) {{
+                const score = noteEval['综合得分'];
+                const scoreEmoji = score >= 8 ? '🟢' : score >= 6 ? '🟡' : score >= 5 ? '🟠' : '🔴';
+                const scoreText = score >= 8 ? '完全匹配' : score >= 6 ? '相似匹配' : score >= 5 ? '弱相似' : '无匹配';
+                const reasoning = noteEval['评分说明'] || '无';
+                const matchingPoints = (noteEval['关键匹配点'] || []).join('、') || '无';
+
+                evalSection = `
+                    <div class="note-eval">
+                        <div class="note-eval-header" onclick="event.stopPropagation(); toggleEvalDetails('${{carouselId}}')">
+                            <span class="note-eval-score">${{scoreEmoji}} ${{scoreText}} (${{score}}分)</span>
+                            <span class="note-eval-toggle" id="${{carouselId}}-toggle">▼ 详情</span>
+                        </div>
+                        <div class="note-eval-details" id="${{carouselId}}-details">
+                            <div class="eval-detail-label">评估理由:</div>
+                            <div class="eval-detail-text">${{reasoning}}</div>
+                            <div class="eval-detail-label">匹配要点:</div>
+                            <div class="eval-detail-text">${{matchingPoints}}</div>
+                        </div>
+                    </div>
+                `;
+            }} else if (evalCategory === 'filtered') {{
+                evalSection = `
+                    <div class="note-eval">
+                        <div class="note-eval-score">⚫ 已过滤(与搜索无关)</div>
+                    </div>
+                `;
+            }}
+
+            return `
+                <div class="note-card ${{evalClass}}" data-eval-category="${{evalCategory}}" onclick="openNote('${{noteId}}')">
+                    <div class="image-carousel" id="${{carouselId}}">
+                        <div class="carousel-images">
+                            ${{images.map(img => `<img class="carousel-image" src="${{img}}" alt="帖子图片" loading="lazy">`).join('')}}
+                        </div>
+                        ${{images.length > 1 ? `
+                            <button class="carousel-btn prev" onclick="event.stopPropagation(); changeImage('${{carouselId}}', -1)">←</button>
+                            <button class="carousel-btn next" onclick="event.stopPropagation(); changeImage('${{carouselId}}', 1)">→</button>
+                            <div class="carousel-indicators">
+                                ${{images.map((_, i) => `<span class="dot ${{i === 0 ? 'active' : ''}}" onclick="event.stopPropagation(); goToImage('${{carouselId}}', ${{i}})"></span>`).join('')}}
+                            </div>
+                            <span class="image-counter">1/${{images.length}}</span>
+                        ` : ''}}
+                    </div>
+                    <div class="note-info">
+                        <div class="note-title">${{title}}</div>
+                        <div class="note-meta">
+                            <span class="note-type type-${{noteType}}">
+                                ${{noteType === 'video' ? '🎬 视频' : '📷 图文'}}
+                            </span>
+                            <div class="note-author">
+                                ${{userAvatar ? `<img class="author-avatar" src="${{userAvatar}}" alt="${{userName}}">` : ''}}
+                                <span>${{userName}}</span>
+                            </div>
+                        </div>
+                    </div>
+                    ${{evalSection}}
+                </div>
+            `;
+        }}
+
+        // 图片轮播逻辑
+        const carouselStates = {{}};
+
+        function changeImage(carouselId, direction) {{
+            if (!carouselStates[carouselId]) {{
+                carouselStates[carouselId] = {{ currentIndex: 0 }};
+            }}
+
+            const carousel = document.getElementById(carouselId);
+            const imagesContainer = carousel.querySelector('.carousel-images');
+            const images = carousel.querySelectorAll('.carousel-image');
+            const dots = carousel.querySelectorAll('.dot');
+            const counter = carousel.querySelector('.image-counter');
+
+            let newIndex = carouselStates[carouselId].currentIndex + direction;
+            if (newIndex < 0) newIndex = images.length - 1;
+            if (newIndex >= images.length) newIndex = 0;
+
+            carouselStates[carouselId].currentIndex = newIndex;
+            imagesContainer.style.transform = `translateX(-${{newIndex * 100}}%)`;
+
+            // 更新指示器
+            dots.forEach((dot, i) => {{
+                dot.classList.toggle('active', i === newIndex);
+            }});
+
+            // 更新计数器
+            if (counter) {{
+                counter.textContent = `${{newIndex + 1}}/${{images.length}}`;
+            }}
+        }}
+
+        function goToImage(carouselId, index) {{
+            if (!carouselStates[carouselId]) {{
+                carouselStates[carouselId] = {{ currentIndex: 0 }};
+            }}
+
+            const carousel = document.getElementById(carouselId);
+            const imagesContainer = carousel.querySelector('.carousel-images');
+            const dots = carousel.querySelectorAll('.dot');
+            const counter = carousel.querySelector('.image-counter');
+
+            carouselStates[carouselId].currentIndex = index;
+            imagesContainer.style.transform = `translateX(-${{index * 100}}%)`;
+
+            // 更新指示器
+            dots.forEach((dot, i) => {{
+                dot.classList.toggle('active', i === index);
+            }});
+
+            // 更新计数器
+            if (counter) {{
+                counter.textContent = `${{index + 1}}/${{dots.length}}`;
+            }}
+        }}
+
+        // 展开/折叠特征组
+        function toggleFeature(featureIdx) {{
+            const searchWordsList = document.getElementById(`search-words-${{featureIdx}}`);
+            const featureHeader = document.getElementById(`feature-header-${{featureIdx}}`);
+
+            searchWordsList.classList.toggle('expanded');
+            featureHeader.classList.toggle('active');
+        }}
+
+        // 展开/折叠base word分组
+        function toggleBaseWord(featureIdx, groupIdx) {{
+            const baseWordHeader = document.getElementById(`base-word-header-${{featureIdx}}-${{groupIdx}}`);
+            const baseWordDesc = document.getElementById(`base-word-desc-${{featureIdx}}-${{groupIdx}}`);
+            const searchWordsSublist = document.getElementById(`search-words-sublist-${{featureIdx}}-${{groupIdx}}`);
+
+            baseWordHeader.classList.toggle('active');
+            baseWordDesc.classList.toggle('expanded');
+            searchWordsSublist.classList.toggle('expanded');
+        }}
+
+        // 滚动到指定结果块
+        function scrollToBlock(blockId) {{
+            const block = document.getElementById(blockId);
+            if (block) {{
+                block.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
+
+                // 高亮对应的搜索词
+                document.querySelectorAll('.search-word-item').forEach(item => {{
+                    item.classList.remove('active');
+                }});
+
+                document.querySelectorAll(`[data-block-id="${{blockId}}"]`).forEach(item => {{
+                    item.classList.add('active');
+                }});
+            }}
+        }}
+
+        // 切换评估详情
+        function toggleEvalDetails(carouselId) {{
+            const details = document.getElementById(`${{carouselId}}-details`);
+            const toggle = document.getElementById(`${{carouselId}}-toggle`);
+
+            if (details && toggle) {{
+                details.classList.toggle('expanded');
+                toggle.textContent = details.classList.contains('expanded') ? '▲ 收起' : '▼ 详情';
+            }}
+        }}
+
+        // 过滤帖子
+        function filterNotes(category) {{
+            currentFilter = category;
+
+            // 更新按钮状态
+            document.querySelectorAll('.filter-btn').forEach(btn => {{
+                btn.classList.remove('active');
+            }});
+            event.target.classList.add('active');
+
+            // 过滤帖子卡片
+            document.querySelectorAll('.note-card').forEach(card => {{
+                const evalCategory = card.getAttribute('data-eval-category');
+                if (category === 'all' || evalCategory === category) {{
+                    card.classList.remove('hidden');
+                }} else {{
+                    card.classList.add('hidden');
+                }}
+            }});
+
+            // 隐藏空的结果块
+            document.querySelectorAll('.result-block').forEach(block => {{
+                const visibleCards = block.querySelectorAll('.note-card:not(.hidden)');
+                if (visibleCards.length === 0) {{
+                    block.classList.add('hidden');
+                }} else {{
+                    block.classList.remove('hidden');
+                }}
+            }});
+        }}
+
+        // 打开小红书帖子
+        function openNote(noteId) {{
+            if (noteId) {{
+                window.open(`https://www.xiaohongshu.com/explore/${{noteId}}`, '_blank');
+            }}
+        }}
+
+        // 初始化
+        document.addEventListener('DOMContentLoaded', () => {{
+            renderLeftSidebar();
+            renderRightContent();
+
+            // 默认展开第一个特征组和第一个base_word
+            if (data.length > 0) {{
+                toggleFeature(0);
+
+                // 展开第一个base_word分组
+                const firstGroups = data[0]['组合评估结果_分组'];
+                if (firstGroups && firstGroups.length > 0) {{
+                    toggleBaseWord(0, 0);
+                }}
+            }}
+        }});
+    </script>
+</body>
+</html>
+'''
+
+    # 写入文件
+    with open(output_path, 'w', encoding='utf-8') as f:
+        f.write(html_content)
+
+
+def main():
+    """主函数"""
+    # 配置路径
+    script_dir = os.path.dirname(os.path.abspath(__file__))
+    json_path = os.path.join(script_dir, 'output_v2', 'stage6_with_evaluations.json')
+    output_dir = os.path.join(script_dir, 'visualization')
+    os.makedirs(output_dir, exist_ok=True)
+
+    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
+    output_path = os.path.join(output_dir, f'stage6_interactive_{timestamp}.html')
+
+    # 加载数据
+    print(f"📖 加载数据: {json_path}")
+    data = load_data(json_path)
+    print(f"✓ 加载了 {len(data)} 个原始特征")
+
+    # 计算统计
+    print("📊 计算统计数据...")
+    stats = calculate_statistics(data)
+    print(f"✓ 统计完成:")
+    print(f"  - 原始特征: {stats['total_features']}")
+    print(f"  - 搜索词: {stats['total_search_words']}")
+    print(f"  - 帖子总数: {stats['total_notes']}")
+    print(f"  - 视频: {stats['video_count']} ({stats['video_percentage']}%)")
+    print(f"  - 图文: {stats['normal_count']} ({stats['normal_percentage']}%)")
+    print(f"\n  评估结果:")
+    print(f"  - 已评估: {stats['total_evaluated']}")
+    print(f"  - 已过滤: {stats['total_filtered']} ({stats['filter_rate']}%)")
+    print(f"  - 完全匹配: {stats['match_complete']} ({stats['complete_rate']}%)")
+    print(f"  - 相似匹配: {stats['match_similar']} ({stats['similar_rate']}%)")
+    print(f"  - 弱相似: {stats['match_weak']}")
+    print(f"  - 无匹配: {stats['match_none']}")
+
+    # 生成HTML
+    print(f"\n🎨 生成可视化页面...")
+    generate_html(data, stats, output_path)
+    print(f"✓ 生成完成: {output_path}")
+
+    # 打印访问提示
+    print(f"\n🌐 在浏览器中打开查看:")
+    print(f"   file://{output_path}")
+
+    return output_path
+
+
+if __name__ == '__main__':
+    main()