浏览代码

feat: 优化How解构结果可视化页面

主要改进:
1. 相似度分级优化
   - 相同 (≥0.9) - 绿色
   - 相似 (0.8-0.9) - 橙色
   - 无关 (<0.8) - 灰色

2. 多层级结论展示
   - 目录顶部:整体结论(找到/部分找到/都找不到)
   - 灵感点详情:每个灵感点的结论徽章
   - 特征标签:显示状态(相同/相似/无关)
   - 匹配结果:每个特征的状态标签

3. 匹配结果分组展示
   - 匹配标签组(可折叠)
   - 匹配分类组(可折叠)
   - 紧凑卡片流式布局,提升浏览效率

4. 交互式特征详情
   - 点击特征卡片弹出模态框
   - 显示详细匹配说明
   - 展示完整历史帖子卡片(带图片、统计数据)
   - 模态框层级管理(帖子详情在最上层)

5. 目录导航增强
   - 特征条目显示状态标签
   - 彩色左侧边框标识状态
   - 整体结论卡片置顶

6. 执行脚本
   - 新增 run_all.sh 自动按顺序运行所有数据处理脚本
   - 显示每步的输入输出路径
yangxiaohui 1 周之前
父节点
当前提交
6c26a01a23
共有 1 个文件被更改,包括 748 次插入114 次删除
  1. 748 114
      script/data_processing/visualize_how_results.py

+ 748 - 114
script/data_processing/visualize_how_results.py

@@ -182,22 +182,75 @@ def generate_post_detail_html(post_data: Dict, post_idx: int) -> str:
     return html
 
 
-def generate_inspiration_detail_html(inspiration_point: Dict) -> str:
-    """生成灵感点详情HTML"""
+def generate_inspiration_detail_html(inspiration_point: Dict, feature_status_map: Dict[str, str] = None) -> str:
+    """生成灵感点详情HTML
+
+    Args:
+        inspiration_point: 灵感点数据
+        feature_status_map: 特征名称到状态的映射 {特征名称: "相同"|"相似"|"无关"}
+    """
     name = inspiration_point.get("名称", "")
     desc = inspiration_point.get("描述", "")
     features = inspiration_point.get("特征列表", [])
 
-    features_html = "".join([
-        f'<span class="feature-tag">{html_module.escape(f if isinstance(f, str) else f.get("特征名称", ""))} <span class="feature-weight">({f.get("权重", 1.0) if isinstance(f, dict) else 1.0})</span></span>'
-        for f in features
-    ])
+    if feature_status_map is None:
+        feature_status_map = {}
+
+    # 计算该灵感点的整体结论
+    feature_statuses = []
+    features_html_list = []
+    for f in features:
+        feature_name = f if isinstance(f, str) else f.get("特征名称", "")
+        weight = f.get("权重", 1.0) if isinstance(f, dict) else 1.0
+
+        # 获取该特征的状态
+        status = feature_status_map.get(feature_name, "无关")
+        feature_statuses.append(status)
+
+        if status == "相同":
+            status_class = "feature-same"
+            status_label = "相同"
+        elif status == "相似":
+            status_class = "feature-similar"
+            status_label = "相似"
+        else:
+            status_class = "feature-unrelated"
+            status_label = "无关"
+
+        features_html_list.append(
+            f'<span class="feature-tag {status_class}">'
+            f'<span class="feature-status-label">{status_label}</span> '
+            f'{html_module.escape(feature_name)} '
+            f'<span class="feature-weight">({weight})</span>'
+            f'</span>'
+        )
+
+    features_html = "".join(features_html_list)
+
+    # 计算灵感点结论
+    has_same = "相同" in feature_statuses
+    has_similar = "相似" in feature_statuses
+    has_unrelated = "无关" in feature_statuses
+
+    if not has_unrelated:
+        # 没有无关的 -> 找到
+        insp_conclusion = "找到"
+        insp_conclusion_class = "insp-conclusion-found"
+    elif has_same or has_similar:
+        # 有相同或相似,但也有无关 -> 部分找到
+        insp_conclusion = "部分找到"
+        insp_conclusion_class = "insp-conclusion-partial"
+    else:
+        # 都是无关 -> 都找不到
+        insp_conclusion = "都找不到"
+        insp_conclusion_class = "insp-conclusion-not-found"
 
     html = f'''
     <div class="inspiration-detail-card">
         <div class="inspiration-header">
             <span class="inspiration-type-badge">灵感点</span>
             <h3 class="inspiration-name">{html_module.escape(name)}</h3>
+            <span class="inspiration-conclusion {insp_conclusion_class}">{insp_conclusion}</span>
         </div>
         <div class="inspiration-description">
             <div class="desc-label">描述:</div>
@@ -249,6 +302,123 @@ def load_feature_source_mapping() -> Dict:
         return {}
 
 
+def generate_single_match_html(match: Dict, match_idx: int, post_idx: int, insp_idx: int, feature_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
+    """生成单个匹配项的HTML
+
+    Args:
+        match: 单个匹配数据
+        match_idx: 匹配项索引
+        post_idx: 帖子索引
+        insp_idx: 灵感点索引
+        feature_idx: 特征索引
+        category_mapping: 特征分类映射
+        source_mapping: 特征来源映射
+    """
+    persona_name = match.get("人设特征名称", "")
+    feature_type = match.get("特征类型", "")
+    feature_categories = match.get("特征分类", [])
+    match_result = match.get("匹配结果", {})
+    similarity = match_result.get("相似度", 0.0)
+    explanation = match_result.get("说明", "")
+
+    # 根据相似度确定颜色和标签
+    if similarity >= 0.9:
+        color = "#10b981"  # 绿色 - 相同
+        label = "相同"
+    elif similarity >= 0.8:
+        color = "#f59e0b"  # 橙色 - 相似
+        label = "相似"
+    else:
+        color = "#9ca3af"  # 灰色 - 无关
+        label = "无关"
+
+    match_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-match-{match_idx}"
+
+    # 生成特征类型和分类标签
+    type_badge_html = ""
+    if feature_type:
+        type_badge_html = f'<span class="feature-type-badge">{html_module.escape(feature_type)}</span>'
+
+    categories_badge_html = ""
+    if feature_categories:
+        categories_text = " / ".join(feature_categories)
+        categories_badge_html = f'<span class="feature-category-badge">{html_module.escape(categories_text)}</span>'
+
+    # 获取该人设特征的分类信息
+    categories_html = ""
+    if category_mapping and persona_name:
+        found_categories = None
+        # 依次在灵感点、关键点、目的点中查找
+        for persona_type in ["灵感点", "关键点", "目的点"]:
+            if persona_type in category_mapping:
+                type_mapping = category_mapping[persona_type]
+                if persona_name in type_mapping:
+                    found_categories = type_mapping[persona_name].get("所属分类", [])
+                    break
+
+        if found_categories:
+            # 简洁样式:[大类/中类/小类]
+            categories_reversed = list(reversed(found_categories))
+            categories_text = "/".join(categories_reversed)
+            categories_html = f'<span class="category-simple">[{html_module.escape(categories_text)}]</span>'
+
+    # 获取该人设特征的历史帖子来源
+    historical_posts_html = ""
+    if source_mapping and persona_name and persona_name in source_mapping:
+        source_list = source_mapping[persona_name]
+        if source_list:
+            historical_cards = []
+            for source_item in source_list:
+                post_detail = source_item.get("帖子详情", {})
+                if post_detail:
+                    card_html = generate_historical_post_card_html(post_detail, source_item)
+                    historical_cards.append(card_html)
+
+            if historical_cards:
+                historical_posts_html = f'''
+                <div class="historical-posts-section">
+                    <h4 class="historical-posts-title">历史帖子来源</h4>
+                    <div class="historical-posts-grid">
+                        {"".join(historical_cards)}
+                    </div>
+                </div>
+                '''
+
+    # 生成历史帖子HTML
+    historical_posts_html = ""
+    if source_mapping and persona_name and persona_name in source_mapping:
+        source_list = source_mapping[persona_name]
+        if source_list:
+            for source_item in source_list[:5]:  # 最多5个
+                post_detail = source_item.get("帖子详情", {})
+                if post_detail:
+                    card_html = generate_historical_post_card_html(post_detail, source_item)
+                    historical_posts_html += card_html
+
+    # 将数据编码到data属性中
+    import html as html_encode
+    data_explanation = html_encode.escape(explanation)
+    data_historical = html_encode.escape(historical_posts_html)
+
+    # 生成紧凑的匹配项HTML(可点击,弹出模态框)
+    html = f'''
+    <div class="match-item-compact"
+         data-persona-name="{html_module.escape(persona_name)}"
+         data-feature-type="{html_module.escape(feature_type)}"
+         data-similarity="{similarity}"
+         data-label="{label}"
+         data-explanation="{data_explanation}"
+         data-historical-posts="{data_historical}"
+         onclick="showMatchDetail(this)">
+        {type_badge_html}
+        <span class="persona-name">{html_module.escape(persona_name)}</span>
+        <span class="score-badge">相似度: {similarity:.2f}</span>
+        <span class="relation-badge" style="background: {color};">{label}</span>
+    </div>
+    '''
+    return html
+
+
 def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_idx: int, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
     """生成可折叠的匹配结果HTML"""
     if not how_steps or len(how_steps) == 0:
@@ -271,27 +441,45 @@ def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_id
     # 按相似度排序
     sorted_matches = sorted(match_results, key=lambda x: x.get("匹配结果", {}).get("相似度", 0), reverse=True)
 
-    # 统计相似度分布(按区间统计)
+    # 找出最高相似度,确定状态
+    max_similarity = 0.0
+    if match_results:
+        max_similarity = max(match.get("匹配结果", {}).get("相似度", 0) for match in match_results)
+
+    # 根据最高相似度确定状态
+    if max_similarity >= 0.9:
+        status = "相同"
+        status_class = "status-same"
+    elif max_similarity >= 0.8:
+        status = "相似"
+        status_class = "status-similar"
+    else:
+        status = "无关"
+        status_class = "status-unrelated"
+
+    found_status_html = f'<span class="feature-match-status {status_class}">{status}</span>'
+
+    # 统计相似度分布
     similarity_ranges = {
-        "高相似 (≥0.7)": 0,
-        "中相似 (0.4-0.7)": 0,
-        "低相似 (<0.4)": 0
+        "相同 (≥0.9)": 0,
+        "相似 (0.8-0.9)": 0,
+        "无关 (<0.8)": 0
     }
     for match in match_results:
         similarity = match.get("匹配结果", {}).get("相似度", 0)
-        if similarity >= 0.7:
-            similarity_ranges["高相似 (≥0.7)"] += 1
-        elif similarity >= 0.4:
-            similarity_ranges["中相似 (0.4-0.7)"] += 1
+        if similarity >= 0.9:
+            similarity_ranges["相同 (≥0.9)"] += 1
+        elif similarity >= 0.8:
+            similarity_ranges["相似 (0.8-0.9)"] += 1
         else:
-            similarity_ranges["低相似 (<0.4)"] += 1
+            similarity_ranges["无关 (<0.8)"] += 1
 
     # 生成统计信息
     stats_items = []
     range_colors = {
-        "高相似 (≥0.7)": "#10b981",
-        "中相似 (0.4-0.7)": "#f59e0b",
-        "低相似 (<0.4)": "#9ca3af"
+        "相同 (≥0.9)": "#10b981",
+        "相似 (0.8-0.9)": "#f59e0b",
+        "无关 (<0.8)": "#9ca3af"
     }
     for range_name, count in similarity_ranges.items():
         if count > 0:
@@ -299,95 +487,62 @@ def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_id
             stats_items.append(f'<span class="stat-badge" style="background: {color};">{range_name}: {count}</span>')
     stats_html = "".join(stats_items)
 
-    # 生成匹配项
-    matches_html = ""
+    # 按特征类型分组匹配项
+    match_groups = {
+        "标签": [],
+        "分类": []
+    }
+
     for i, match in enumerate(sorted_matches):
-        persona_name = match.get("人设特征名称", "")
         feature_type = match.get("特征类型", "")
-        feature_categories = match.get("特征分类", [])
-        match_result = match.get("匹配结果", {})
-        similarity = match_result.get("相似度", 0.0)
-        explanation = match_result.get("说明", "")
-
-        # 根据相似度确定颜色
-        if similarity >= 0.7:
-            color = "#10b981"  # 绿色 - 高相似
-            label = "高相似"
-        elif similarity >= 0.4:
-            color = "#f59e0b"  # 橙色 - 中相似
-            label = "中相似"
-        else:
-            color = "#9ca3af"  # 灰色 - 低相似
-            label = "低相似"
-
-        match_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-match-{i}"
-
-        # 生成特征类型和分类标签
-        type_badge_html = ""
-        if feature_type:
-            type_badge_html = f'<span class="feature-type-badge">{html_module.escape(feature_type)}</span>'
-
-        categories_badge_html = ""
-        if feature_categories:
-            categories_text = " / ".join(feature_categories)
-            categories_badge_html = f'<span class="feature-category-badge">{html_module.escape(categories_text)}</span>'
-
-        # 获取该人设特征的分类信息
-        # 需要在三个类型中查找该特征
-        categories_html = ""
-        if category_mapping and persona_name:
-            found_categories = None
-            # 依次在灵感点、关键点、目的点中查找
-            for persona_type in ["灵感点", "关键点", "目的点"]:
-                if persona_type in category_mapping:
-                    type_mapping = category_mapping[persona_type]
-                    if persona_name in type_mapping:
-                        found_categories = type_mapping[persona_name].get("所属分类", [])
-                        break
-
-            if found_categories:
-                # 简洁样式:[大类/中类/小类]
-                categories_reversed = list(reversed(found_categories))
-                categories_text = "/".join(categories_reversed)
-                categories_html = f'<span class="category-simple">[{html_module.escape(categories_text)}]</span>'
-
-        # 获取该人设特征的历史帖子来源
-        historical_posts_html = ""
-        if source_mapping and persona_name and persona_name in source_mapping:
-            source_list = source_mapping[persona_name]
-            if source_list:
-                historical_cards = []
-                for source_item in source_list:
-                    post_detail = source_item.get("帖子详情", {})
-                    if post_detail:
-                        card_html = generate_historical_post_card_html(post_detail, source_item)
-                        historical_cards.append(card_html)
-
-                if historical_cards:
-                    historical_posts_html = f'''
-                    <div class="historical-posts-section">
-                        <h4 class="historical-posts-title">历史帖子来源</h4>
-                        <div class="historical-posts-grid">
-                            {"".join(historical_cards)}
-                        </div>
-                    </div>
-                    '''
+        if feature_type in match_groups:
+            match_groups[feature_type].append((i, match))
+
+    # 生成分组的匹配项HTML
+    matches_html = ""
+
+    # 先显示"标签"匹配结果
+    if match_groups["标签"]:
+        group_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-group-label"
+        group_matches_html = ""
+        for i, match in match_groups["标签"]:
+            match_html = generate_single_match_html(
+                match, i, post_idx, insp_idx, feature_idx,
+                category_mapping, source_mapping
+            )
+            group_matches_html += match_html
 
         matches_html += f'''
-        <div class="match-item-collapsible">
-            <div class="match-header" onclick="toggleMatch('{match_id}')">
-                <div class="match-header-left">
-                    <span class="expand-icon" id="{match_id}-icon">▶</span>
-                    <span class="persona-name">{categories_html} {html_module.escape(persona_name)}</span>
-                    {type_badge_html}
-                    {categories_badge_html}
-                    <span class="relation-badge" style="background: {color};">{label}</span>
-                    <span class="score-badge">相似度: {similarity:.2f}</span>
-                </div>
+        <div class="match-group-section">
+            <div class="match-group-header" onclick="toggleMatchGroup('{group_id}')">
+                <span class="expand-icon" id="{group_id}-icon">▼</span>
+                <h4 class="match-group-title">匹配标签 ({len(match_groups["标签"])})</h4>
             </div>
-            <div class="match-content" id="{match_id}-content" style="display: none;">
-                <div class="match-explanation">{html_module.escape(explanation)}</div>
-                {historical_posts_html}
+            <div class="match-group-content" id="{group_id}-content">
+                {group_matches_html}
+            </div>
+        </div>
+        '''
+
+    # 再显示"分类"匹配结果
+    if match_groups["分类"]:
+        group_id = f"post-{post_idx}-insp-{insp_idx}-feat-{feature_idx}-group-category"
+        group_matches_html = ""
+        for i, match in match_groups["分类"]:
+            match_html = generate_single_match_html(
+                match, i, post_idx, insp_idx, feature_idx,
+                category_mapping, source_mapping
+            )
+            group_matches_html += match_html
+
+        matches_html += f'''
+        <div class="match-group-section">
+            <div class="match-group-header" onclick="toggleMatchGroup('{group_id}')">
+                <span class="expand-icon" id="{group_id}-icon">▼</span>
+                <h4 class="match-group-title">匹配分类 ({len(match_groups["分类"])})</h4>
+            </div>
+            <div class="match-group-content" id="{group_id}-content">
+                {group_matches_html}
             </div>
         </div>
         '''
@@ -400,6 +555,7 @@ def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_id
             <div class="header-left">
                 <span class="expand-icon" id="{section_id}-icon">▼</span>
                 <h4>匹配结果: {html_module.escape(feature_name)} <span class="feature-weight-display">(权重: {feature_weight})</span></h4>
+                {found_status_html}
             </div>
             <div class="match-stats">{stats_html}</div>
         </div>
@@ -411,11 +567,21 @@ def generate_match_results_html(how_steps: List[Dict], feature_idx: int, insp_id
     return html
 
 
-def generate_toc_html(post_data: Dict, post_idx: int) -> str:
-    """生成目录导航HTML"""
+def generate_toc_html(post_data: Dict, post_idx: int, feature_status_map: Dict[str, str] = None, overall_conclusion: str = "") -> str:
+    """生成目录导航HTML
+
+    Args:
+        post_data: 帖子数据
+        post_idx: 帖子索引
+        feature_status_map: 特征名称到状态的映射 {特征名称: "相同"|"相似"|"无关"}
+        overall_conclusion: 整体结论
+    """
     how_result = post_data.get("how解构结果", {})
     inspiration_list = how_result.get("灵感点列表", [])
 
+    if feature_status_map is None:
+        feature_status_map = {}
+
     toc_items = []
 
     # 帖子详情
@@ -434,11 +600,45 @@ def generate_toc_html(post_data: Dict, post_idx: int) -> str:
             features = how_steps[0].get("特征列表", [])
             for feat_idx, feature_data in enumerate(features):
                 feature_name = feature_data.get("特征名称", f"特征 {feat_idx + 1}")
-                toc_items.append(f'<div class="toc-item toc-level-2" onclick="scrollToSection(\'post-{post_idx}-feat-{insp_idx}-{feat_idx}\')"><span class="toc-badge toc-badge-feature">特征</span> {html_module.escape(feature_name)}</div>')
+
+                # 获取状态
+                status = feature_status_map.get(feature_name, "无关")
+                if status == "相同":
+                    status_class = "toc-feature-same"
+                    status_label = "相同"
+                elif status == "相似":
+                    status_class = "toc-feature-similar"
+                    status_label = "相似"
+                else:
+                    status_class = "toc-feature-unrelated"
+                    status_label = "无关"
+
+                toc_items.append(f'<div class="toc-item toc-level-2 {status_class}" onclick="scrollToSection(\'post-{post_idx}-feat-{insp_idx}-{feat_idx}\')"><span class="toc-badge toc-badge-feature">特征</span> {html_module.escape(feature_name)} <span class="toc-feature-status">{status_label}</span></div>')
+
+    # 整体结论HTML
+    conclusion_html = ""
+    if overall_conclusion:
+        if overall_conclusion == "找到":
+            conclusion_class = "conclusion-found"
+            conclusion_icon = "✓"
+        elif overall_conclusion == "部分找到":
+            conclusion_class = "conclusion-partial"
+            conclusion_icon = "~"
+        else:  # 都找不到
+            conclusion_class = "conclusion-not-found"
+            conclusion_icon = "✗"
+
+        conclusion_html = f'''
+        <div class="toc-conclusion {conclusion_class}">
+            <span class="conclusion-icon">{conclusion_icon}</span>
+            <span class="conclusion-text">{overall_conclusion}</span>
+        </div>
+        '''
 
     return f'''
     <div class="toc-container">
         <div class="toc-header">目录导航</div>
+        {conclusion_html}
         <div class="toc-content">
             {"".join(toc_items)}
         </div>
@@ -448,20 +648,59 @@ def generate_toc_html(post_data: Dict, post_idx: int) -> str:
 
 def generate_post_content_html(post_data: Dict, post_idx: int, category_mapping: Dict = None, source_mapping: Dict = None) -> str:
     """生成单个帖子的完整内容HTML"""
-    # 生成目录
-    toc_html = generate_toc_html(post_data, post_idx)
-
-    # 1. 帖子详情
-    post_detail_html = generate_post_detail_html(post_data, post_idx)
-
     # 2. 灵感点详情和匹配结果
     how_result = post_data.get("how解构结果", {})
     inspiration_list = how_result.get("灵感点列表", [])
 
-    # 生成所有灵感点的详情HTML(只包含灵感点详情,不包含匹配结果)
+    # 先计算所有特征的状态(基于最高相似度)
+    feature_status_map = {}  # {特征名称: "相同"|"相似"|"无关"}
+    for inspiration_point in inspiration_list:
+        how_steps = inspiration_point.get("how步骤列表", [])
+        if how_steps:
+            features = how_steps[0].get("特征列表", [])
+            for feature_data in features:
+                feature_name = feature_data.get("特征名称", "")
+                match_results = feature_data.get("匹配结果", [])
+
+                # 找出最高相似度
+                max_similarity = 0.0
+                if match_results:
+                    max_similarity = max(match.get("匹配结果", {}).get("相似度", 0) for match in match_results)
+
+                # 根据最高相似度确定状态
+                if max_similarity >= 0.9:
+                    feature_status_map[feature_name] = "相同"
+                elif max_similarity >= 0.8:
+                    feature_status_map[feature_name] = "相似"
+                else:
+                    feature_status_map[feature_name] = "无关"
+
+    # 计算整体结论
+    status_values = list(feature_status_map.values())
+    has_same = "相同" in status_values
+    has_similar = "相似" in status_values
+    has_unrelated = "无关" in status_values
+
+    if not has_unrelated:
+        # 没有无关的 -> 找到
+        overall_conclusion = "找到"
+    elif has_same or has_similar:
+        # 有相同或相似,但也有无关 -> 部分找到
+        overall_conclusion = "部分找到"
+    else:
+        # 都是无关 -> 都找不到
+        overall_conclusion = "都找不到"
+
+    # 生成目录(传入状态映射和整体结论)
+    toc_html = generate_toc_html(post_data, post_idx, feature_status_map, overall_conclusion)
+
+    # 1. 帖子详情
+    post_detail_html = generate_post_detail_html(post_data, post_idx)
+
+    # 生成所有灵感点的详情HTML(传入状态映射)
     inspirations_detail_html = ""
     for insp_idx, inspiration_point in enumerate(inspiration_list):
-        inspiration_detail = generate_inspiration_detail_html(inspiration_point)
+        inspiration_detail = generate_inspiration_detail_html(inspiration_point, feature_status_map)
         inspirations_detail_html += f'''
         <div id="post-{post_idx}-insp-{insp_idx}" class="inspiration-detail-item content-section">
             {inspiration_detail}
@@ -721,6 +960,80 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 padding-left: 30px;
             }}
 
+            .toc-level-2.toc-feature-same {{
+                border-left: 3px solid #10b981;
+            }}
+
+            .toc-level-2.toc-feature-similar {{
+                border-left: 3px solid #f59e0b;
+            }}
+
+            .toc-level-2.toc-feature-unrelated {{
+                border-left: 3px solid #9ca3af;
+            }}
+
+            .toc-feature-status {{
+                font-weight: 600;
+                font-size: 11px;
+                padding: 2px 6px;
+                border-radius: 4px;
+                margin-left: 8px;
+            }}
+
+            .toc-feature-same .toc-feature-status {{
+                background: #d1fae5;
+                color: #065f46;
+            }}
+
+            .toc-feature-similar .toc-feature-status {{
+                background: #fed7aa;
+                color: #92400e;
+            }}
+
+            .toc-feature-unrelated .toc-feature-status {{
+                background: #e5e7eb;
+                color: #4b5563;
+            }}
+
+            /* 目录整体结论 */
+            .toc-conclusion {{
+                padding: 15px 20px;
+                margin: 10px;
+                border-radius: 8px;
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                font-weight: 600;
+                font-size: 14px;
+            }}
+
+            .toc-conclusion.conclusion-found {{
+                background: #d1fae5;
+                color: #065f46;
+                border: 2px solid #10b981;
+            }}
+
+            .toc-conclusion.conclusion-partial {{
+                background: #fed7aa;
+                color: #92400e;
+                border: 2px solid #f59e0b;
+            }}
+
+            .toc-conclusion.conclusion-not-found {{
+                background: #fee2e2;
+                color: #991b1b;
+                border: 2px solid #ef4444;
+            }}
+
+            .conclusion-icon {{
+                font-size: 18px;
+                font-weight: 700;
+            }}
+
+            .conclusion-text {{
+                flex: 1;
+            }}
+
             .toc-badge {{
                 display: inline-block;
                 padding: 2px 8px;
@@ -964,6 +1277,33 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 display: flex;
                 align-items: center;
                 gap: 10px;
+                flex-wrap: wrap;
+            }}
+
+            .inspiration-conclusion {{
+                padding: 6px 14px;
+                border-radius: 16px;
+                font-size: 13px;
+                font-weight: 700;
+                margin-left: auto;
+            }}
+
+            .inspiration-conclusion.insp-conclusion-found {{
+                background: #d1fae5;
+                color: #065f46;
+                border: 2px solid #10b981;
+            }}
+
+            .inspiration-conclusion.insp-conclusion-partial {{
+                background: #fed7aa;
+                color: #92400e;
+                border: 2px solid #f59e0b;
+            }}
+
+            .inspiration-conclusion.insp-conclusion-not-found {{
+                background: #fee2e2;
+                color: #991b1b;
+                border: 2px solid #ef4444;
             }}
 
             .inspiration-type-badge {{
@@ -1024,6 +1364,29 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 border-radius: 16px;
                 font-size: 13px;
                 font-weight: 500;
+                display: inline-flex;
+                align-items: center;
+                gap: 4px;
+            }}
+
+            .feature-tag.feature-same {{
+                background: #10b981;
+            }}
+
+            .feature-tag.feature-similar {{
+                background: #f59e0b;
+            }}
+
+            .feature-tag.feature-unrelated {{
+                background: #9ca3af;
+            }}
+
+            .feature-status-label {{
+                font-weight: 700;
+                font-size: 11px;
+                padding: 2px 6px;
+                background: rgba(255, 255, 255, 0.3);
+                border-radius: 4px;
             }}
 
             /* 匹配结果部分 */
@@ -1090,6 +1453,151 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 gap: 8px;
             }}
 
+            .match-group-section {{
+                margin-bottom: 20px;
+                background: white;
+                border: 1px solid #e5e7eb;
+                border-radius: 8px;
+                overflow: hidden;
+            }}
+
+            .match-group-header {{
+                padding: 12px 18px;
+                background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
+                border-bottom: 2px solid #d1d5db;
+                cursor: pointer;
+                user-select: none;
+                display: flex;
+                align-items: center;
+                gap: 10px;
+                transition: background 0.2s;
+            }}
+
+            .match-group-header:hover {{
+                background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
+            }}
+
+            .match-group-title {{
+                font-size: 15px;
+                font-weight: 600;
+                color: #374151;
+                margin: 0;
+            }}
+
+            .match-group-content {{
+                padding: 15px;
+                display: flex;
+                flex-direction: row;
+                flex-wrap: wrap;
+                gap: 10px;
+            }}
+
+            .match-item-compact {{
+                display: inline-flex;
+                align-items: center;
+                gap: 6px;
+                padding: 6px 12px;
+                background: white;
+                border: 1px solid #e5e7eb;
+                border-radius: 6px;
+                font-size: 13px;
+                transition: all 0.2s;
+                cursor: pointer;
+            }}
+
+            .match-item-compact:hover {{
+                border-color: #667eea;
+                box-shadow: 0 2px 4px rgba(102, 126, 234, 0.1);
+                transform: translateY(-1px);
+            }}
+
+            /* 匹配详情模态框 */
+            .match-modal {{
+                display: none;
+                position: fixed;
+                z-index: 3000;
+                left: 0;
+                top: 0;
+                width: 100%;
+                height: 100%;
+                background: rgba(0, 0, 0, 0.5);
+                animation: fadeIn 0.2s;
+            }}
+
+            .match-modal-content {{
+                position: relative;
+                background: white;
+                margin: 5% auto;
+                padding: 0;
+                width: 80%;
+                max-width: 900px;
+                max-height: 80vh;
+                border-radius: 12px;
+                box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
+                overflow: hidden;
+                animation: slideDown 0.3s;
+            }}
+
+            .match-modal-header {{
+                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+                color: white;
+                padding: 20px 25px;
+                display: flex;
+                align-items: center;
+                justify-content: space-between;
+            }}
+
+            .match-modal-title {{
+                font-size: 18px;
+                font-weight: 600;
+                margin: 0;
+            }}
+
+            .match-modal-close {{
+                color: white;
+                font-size: 32px;
+                font-weight: 300;
+                cursor: pointer;
+                background: none;
+                border: none;
+                padding: 0;
+                line-height: 1;
+                transition: transform 0.2s;
+            }}
+
+            .match-modal-close:hover {{
+                transform: scale(1.2);
+            }}
+
+            .match-modal-body {{
+                padding: 25px;
+                max-height: calc(80vh - 80px);
+                overflow-y: auto;
+            }}
+
+            .match-detail-section {{
+                margin-bottom: 25px;
+            }}
+
+            .match-detail-section h3 {{
+                font-size: 16px;
+                font-weight: 600;
+                color: #374151;
+                margin-bottom: 12px;
+                padding-bottom: 8px;
+                border-bottom: 2px solid #e5e7eb;
+            }}
+
+            .match-explanation-text {{
+                line-height: 1.8;
+                color: #4b5563;
+                padding: 15px;
+                background: #f9fafb;
+                border-radius: 8px;
+                border-left: 4px solid #667eea;
+            }}
+
+
             .match-item-collapsible {{
                 border: 1px solid #e5e7eb;
                 border-radius: 8px;
@@ -1135,6 +1643,12 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 color: #111827;
             }}
 
+            .match-item-compact .persona-name {{
+                font-weight: 500;
+                font-size: 13px;
+                color: #374151;
+            }}
+
             .relation-badge {{
                 padding: 3px 10px;
                 border-radius: 12px;
@@ -1172,6 +1686,52 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 border: 1px solid #93c5fd;
             }}
 
+            .level-badge {{
+                padding: 4px 10px;
+                border-radius: 12px;
+                font-size: 11px;
+                font-weight: 600;
+                margin-right: 6px;
+            }}
+
+            .level-badge-match {{
+                background: #e0e7ff;
+                color: #4338ca;
+                border: 1px solid #a5b4fc;
+            }}
+
+            .level-badge-category {{
+                background: #fef3c7;
+                color: #92400e;
+                border: 1px solid #fcd34d;
+            }}
+
+            .feature-match-status {{
+                padding: 5px 12px;
+                border-radius: 12px;
+                font-size: 12px;
+                font-weight: 700;
+                margin-left: 12px;
+            }}
+
+            .feature-match-status.status-same {{
+                background: #d1fae5;
+                color: #065f46;
+                border: 2px solid #10b981;
+            }}
+
+            .feature-match-status.status-similar {{
+                background: #fed7aa;
+                color: #92400e;
+                border: 2px solid #f59e0b;
+            }}
+
+            .feature-match-status.status-unrelated {{
+                background: #e5e7eb;
+                color: #4b5563;
+                border: 2px solid #9ca3af;
+            }}
+
             .match-content {{
                 padding: 16px;
                 background: #f9fafb;
@@ -1322,7 +1882,7 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
             .post-detail-modal {{
                 display: none;
                 position: fixed;
-                z-index: 1000;
+                z-index: 4000;
                 left: 0;
                 top: 0;
                 width: 100%;
@@ -1548,6 +2108,17 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
             {contents_html}
         </div>
 
+        <!-- 匹配详情模态框 -->
+        <div id="matchModal" class="match-modal" onclick="closeMatchModal(event)">
+            <div class="match-modal-content" onclick="event.stopPropagation()">
+                <div class="match-modal-header">
+                    <h2 class="match-modal-title" id="matchModalTitle"></h2>
+                    <button class="match-modal-close" onclick="closeMatchModal()">&times;</button>
+                </div>
+                <div class="match-modal-body" id="matchModalBody"></div>
+            </div>
+        </div>
+
         <!-- 帖子详情模态框 -->
         <div id="postDetailModal" class="post-detail-modal" onclick="closePostDetail(event)"></div>
 
@@ -1620,6 +2191,69 @@ def generate_combined_html(posts_data: List[Dict], category_mapping: Dict = None
                 }}
             }}
 
+            function toggleMatchGroup(groupId) {{
+                var content = document.getElementById(groupId + '-content');
+                var icon = document.getElementById(groupId + '-icon');
+
+                if (content.style.display === 'none') {{
+                    content.style.display = 'flex';
+                    icon.textContent = '▼';
+                }} else {{
+                    content.style.display = 'none';
+                    icon.textContent = '▶';
+                }}
+            }}
+
+            function showMatchDetail(element) {{
+                var modal = document.getElementById('matchModal');
+                var title = document.getElementById('matchModalTitle');
+                var body = document.getElementById('matchModalBody');
+
+                // 从data属性读取数据
+                var personaName = element.dataset.personaName;
+                var featureType = element.dataset.featureType;
+                var similarity = parseFloat(element.dataset.similarity);
+                var label = element.dataset.label;
+                var explanation = element.dataset.explanation;
+                var historicalPosts = element.dataset.historicalPosts;
+
+                // 设置标题
+                title.textContent = '[' + featureType + '] ' + personaName + ' (相似度: ' + similarity.toFixed(2) + ' - ' + label + ')';
+
+                // 生成内容
+                var bodyHTML = '<div class="match-detail-section">';
+                bodyHTML += '<h3>匹配说明</h3>';
+                bodyHTML += '<div class="match-explanation-text">' + explanation + '</div>';
+                bodyHTML += '</div>';
+
+                // 如果有历史帖子
+                if (historicalPosts && historicalPosts.trim().length > 0) {{
+                    bodyHTML += '<div class="match-detail-section">';
+                    bodyHTML += '<h3>历史帖子来源</h3>';
+                    bodyHTML += '<div class="historical-posts-grid">';
+                    bodyHTML += historicalPosts;
+                    bodyHTML += '</div>';
+                    bodyHTML += '</div>';
+                }}
+
+                body.innerHTML = bodyHTML;
+                modal.style.display = 'block';
+            }}
+
+            function closeMatchModal(event) {{
+                var modal = document.getElementById('matchModal');
+                if (!event || event.target === modal) {{
+                    modal.style.display = 'none';
+                }}
+            }}
+
+            // ESC键关闭模态框
+            document.addEventListener('keydown', function(event) {{
+                if (event.key === 'Escape') {{
+                    closeMatchModal();
+                }}
+            }});
+
             function showPostDetail(element) {{
                 const postDataStr = element.dataset.postData;
                 if (!postDataStr) return;