""" 灵感点分析结果可视化脚本 读取 how/灵感点 目录下的分析结果,结合作者历史帖子详情,生成可视化HTML页面 """ import json from pathlib import Path from typing import Dict, Any, List, Optional from datetime import datetime import html as html_module def load_inspiration_points_data(inspiration_dir: str) -> List[Dict[str, Any]]: """ 加载所有灵感点的分析结果 Args: inspiration_dir: 灵感点目录路径 Returns: 灵感点分析结果列表 """ inspiration_path = Path(inspiration_dir) results = [] # 遍历所有子目录 for subdir in inspiration_path.iterdir(): if subdir.is_dir(): # 查找 all_summary 文件 summary_files = list(subdir.glob("all_summary_*.json")) if summary_files: summary_file = summary_files[0] try: with open(summary_file, 'r', encoding='utf-8') as f: data = json.load(f) # 加载完整的 step1 和 step2 数据 step1_data = None step2_data = None # 直接从当前子目录查找 step1 和 step2 文件 step1_files = list(subdir.glob("all_step1_*.json")) step2_files = list(subdir.glob("all_step2_*.json")) if step1_files: try: with open(step1_files[0], 'r', encoding='utf-8') as f: step1_data = json.load(f) except Exception as e: print(f"警告: 读取 {step1_files[0]} 失败: {e}") if step2_files: try: with open(step2_files[0], 'r', encoding='utf-8') as f: step2_data = json.load(f) except Exception as e: print(f"警告: 读取 {step2_files[0]} 失败: {e}") # 加载搜索结果和匹配分数 search_results = {} search_dir = subdir / "search" if search_dir.exists() and search_dir.is_dir(): search_files = list(search_dir.glob("all_search_*.json")) for search_file in search_files: try: with open(search_file, 'r', encoding='utf-8') as f: search_data = json.load(f) # 从JSON内容中读取真实的keyword,而不是从文件名提取 keyword = search_data.get("search_params", {}).get("keyword", "") if keyword: # 尝试加载对应的匹配结果文件 match_file = search_dir / "all_step4_搜索结果匹配_gemini-2.5-pro.json" match_data = None if match_file.exists(): try: with open(match_file, 'r', encoding='utf-8') as mf: match_data = json.load(mf) except Exception as e: print(f"警告: 读取匹配文件 {match_file} 失败: {e}") search_results[keyword] = { "search_data": search_data, "match_data": match_data } else: # 如果JSON中没有keyword,则从文件名提取 keyword = search_file.stem.replace("all_search_", "") search_results[keyword] = { "search_data": search_data, "match_data": None } except Exception as e: print(f"警告: 读取 {search_file} 失败: {e}") results.append({ "summary": data, "step1": step1_data, "step2": step2_data, "search_results": search_results, "inspiration_name": subdir.name }) except Exception as e: print(f"警告: 读取 {summary_file} 失败: {e}") return results def load_posts_data(posts_dir: str) -> Dict[str, Dict[str, Any]]: """ 加载所有帖子详情数据 Args: posts_dir: 帖子目录路径 Returns: 帖子ID到帖子详情的映射 """ posts_path = Path(posts_dir) posts_map = {} for post_file in posts_path.glob("*.json"): try: with open(post_file, 'r', encoding='utf-8') as f: post_data = json.load(f) post_id = post_data.get("channel_content_id") if post_id: posts_map[post_id] = post_data except Exception as e: print(f"警告: 读取 {post_file} 失败: {e}") return posts_map def generate_post_card_html(post: Dict[str, Any], note_id_prefix: str = "info-post", post_to_mapping_data: Dict[str, Any] = None) -> str: """ 生成单个帖子卡片HTML(与搜索结果样式一致) Args: post: 帖子数据 note_id_prefix: 帖子ID前缀,用于区分不同区域的帖子 post_to_mapping_data: 帖子到分类和点映射数据 Returns: HTML字符串 """ import html as html_module import random title = post.get("title", "") desc = post.get("body_text", "") images = post.get("images", []) like_count = post.get("like_count", 0) comment_count = post.get("comment_count", 0) link = post.get("link", "") author = post.get("channel_account_name", "") post_id = post.get("channel_content_id", "") publish_time = post.get("publish_time", "") # 生成唯一的note_id note_id = f"{note_id_prefix}-{random.randint(10000, 99999)}" # 生成图片轮播HTML images_html = "" if images and len(images) > 0: images_track = "".join([f'图片{i+1}' for i, img in enumerate(images)]) # 图片导航按钮 nav_buttons = "" if len(images) > 1: nav_buttons = f''' ''' images_html = f''' ''' else: # 无图片时显示占位符 images_html = f''' ''' # 准备详情数据 note_data = { "title": title, "desc": desc, "images": images, "link": link, "author": author, "like_count": like_count, "comment_count": comment_count } # 添加灵感点、关键点、目的点到note_data if post_to_mapping_data and post_id and post_id in post_to_mapping_data: mapping = post_to_mapping_data[post_id] note_data["inspiration_points"] = mapping.get("灵感点列表", []) note_data["key_points"] = mapping.get("关键点列表", []) note_data["purpose_points"] = mapping.get("目的点列表", []) import json note_data_json = json.dumps(note_data, ensure_ascii=False) note_data_json_escaped = html_module.escape(note_data_json) # 获取帖子的灵感点、关键点、目的点 points_html = "" if post_to_mapping_data and post_id and post_id in post_to_mapping_data: mapping = post_to_mapping_data[post_id] inspiration_points = mapping.get("灵感点列表", []) key_points = mapping.get("关键点列表", []) purpose_points = mapping.get("目的点列表", []) points_sections = [] # 灵感点 if inspiration_points: insp_items = "".join([ f'
{html_module.escape(p.get("灵感点", ""))}
' for p in inspiration_points[:3] # 最多显示3个 ]) points_sections.append(f'
灵感点
{insp_items}
') # 关键点 if key_points: key_items = "".join([ f'
{html_module.escape(k.get("关键点", ""))}
' for k in key_points[:3] # 最多显示3个 ]) points_sections.append(f'
关键点
{key_items}
') # 目的点 if purpose_points: purpose_items = "".join([ f'
{html_module.escape(p.get("目的点", ""))}
' for p in purpose_points[:3] # 最多显示3个 ]) points_sections.append(f'
目的点
{purpose_items}
') if points_sections: points_html = f'
{"".join(points_sections)}
' # 生成发布日期HTML publish_date_html = "" if publish_time: publish_date_html = f'
📅 {html_module.escape(publish_time)}
' card_html = f'''
{images_html}
{html_module.escape(title) if title else "无标题"}
{html_module.escape(desc) if desc else "暂无描述"}
{publish_date_html}
{f'
{points_html}
' if points_html else ''}
''' return card_html def generate_inspiration_card_html( inspiration_data: Dict[str, Any], inspiration_to_post_data: Dict[str, Any] = None, category_index_data: Dict[str, Any] = None, post_to_mapping_data: Dict[str, Any] = None ) -> str: """ 生成单个灵感点的卡片HTML Args: inspiration_data: 灵感点数据 inspiration_to_post_data: 点到帖子映射数据 category_index_data: 分类索引数据 Returns: HTML字符串 """ summary = inspiration_data.get("summary", {}) step1 = inspiration_data.get("step1", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") search_results = inspiration_data.get("search_results", {}) # 提取关键指标 metrics = summary.get("关键指标", {}) step1_score = metrics.get("step1_top1_score", 0) step1_match_element = metrics.get("step1_top1_匹配要素", "") # 确定卡片颜色(基于Step1分数) if step1_score >= 0.7: border_color = "#10b981" step1_color = "#10b981" elif step1_score >= 0.5: border_color = "#f59e0b" step1_color = "#f59e0b" elif step1_score >= 0.3: border_color = "#3b82f6" step1_color = "#3b82f6" else: border_color = "#ef4444" step1_color = "#ef4444" # 转义HTML inspiration_name_escaped = html_module.escape(inspiration_name) step1_match_element_escaped = html_module.escape(step1_match_element) # 获取Step1匹配结果(简要展示) step1_matches = step1.get("匹配结果列表", []) if step1 else [] step1_match_preview = "" if step1_matches: top_match = step1_matches[0] # 从新的数据结构中提取信息 input_info = top_match.get("输入信息", {}) match_result = top_match.get("匹配结果", {}) element_name = input_info.get("A", "") match_score = match_result.get("score", 0) same_parts = match_result.get("相同部分", {}) or {} increment_parts = match_result.get("增量部分", {}) or {} # 生成相同部分和增量部分的HTML parts_html = "" if same_parts: same_items = [f"{html_module.escape(k)}" for k in same_parts.keys()] parts_html += f'
相同: {", ".join(same_items)}
' if increment_parts: inc_items = [f"{html_module.escape(k)}" for k in increment_parts.keys()] parts_html += f'
增量: {", ".join(inc_items)}
' step1_match_preview = f'''
🎯 Step1 Top1匹配
{html_module.escape(element_name)} {match_score:.2f}
{parts_html}
''' # 提取 top3 匹配信息 top3_matches = [] if step1: matches = step1.get("匹配结果列表", []) for i, match in enumerate(matches[:3]): input_info = match.get("输入信息", {}) match_result = match.get("匹配结果", {}) element_name = input_info.get("A", "") match_score = match_result.get("score", 0) context = input_info.get("A_Context", "") # 解析层级关系 hierarchy = [] if context: lines = context.split("\n") for line in lines: if ":" in line: key, value = line.split(":", 1) key = key.strip() value = value.strip() if key in ["所属视角", "一级分类", "二级分类"]: hierarchy.append(value) top3_matches.append({ "rank": i + 1, "name": element_name, "score": match_score, "hierarchy": hierarchy }) # 准备详细数据用于弹窗 detail_data_json = json.dumps(inspiration_data, ensure_ascii=False) detail_data_json_escaped = html_module.escape(detail_data_json) # top3 匹配数据 top3_json = json.dumps(top3_matches, ensure_ascii=False) top3_json_escaped = html_module.escape(top3_json) # 生成详细HTML并进行HTML转义 detail_html = generate_detail_html(inspiration_data) detail_html_escaped = html_module.escape(detail_html) # 生成匹配列表HTML matches_html = "" if step1: step1_matches = step1.get("匹配结果列表", []) for idx, match in enumerate(step1_matches): input_info = match.get("输入信息", {}) match_result = match.get("匹配结果", {}) element_name = input_info.get("A", "") context = input_info.get("A_Context", "") score = match_result.get("score", 0) score_explain = match_result.get("score说明", "") or "" same_parts = match_result.get("相同部分", {}) or {} increment_parts = match_result.get("增量部分", {}) or {} # 为搜索结果容器生成唯一ID safe_insp_name = ''.join(c if c.isalnum() else '_' for c in inspiration_name) unique_match_id = f"{safe_insp_name}-match-{idx}" # 解析层级 hierarchy = [] if context: lines = context.split("\n") for line in lines: if ":" in line: key, value = line.split(":", 1) key = key.strip() value = value.strip() if key in ["所属视角", "一级分类", "二级分类"]: hierarchy.append(value) hierarchy_html = " › ".join([html_module.escape(h) for h in hierarchy]) if hierarchy else "" rank_class = f"rank-{idx + 1}" if idx < 3 else "" # 相同部分HTML same_parts_html = "" if same_parts: same_items = "".join([f'
{html_module.escape(k)}:{html_module.escape(v)}
' for k, v in same_parts.items()]) same_parts_html = f'''
✅ 相同部分
{same_items}
''' # 增量部分HTML increment_parts_html = "" if increment_parts: inc_items = "".join([f'
{html_module.escape(k)}:{html_module.escape(v)}
' for k, v in increment_parts.items()]) increment_parts_html = f'''
➕ 增量部分
{inc_items}
''' # 生成搜索结果HTML(网格展示,图片轮播) search_html = "" if element_name in search_results: result_obj = search_results[element_name] search_data = result_obj.get("search_data", {}) match_data = result_obj.get("match_data", None) search_params = search_data.get("search_params", {}) notes = search_data.get("notes", []) notes_count = len(notes) # 构建匹配分数字典 {channel_content_id: match_info} match_scores = {} if match_data and "匹配结果列表" in match_data: for match_item in match_data["匹配结果列表"]: business_info = match_item.get("业务信息", {}) input_info = match_item.get("输入信息", {}) content_id = business_info.get("channel_content_id", "") if content_id: match_scores[content_id] = { "score": match_item.get("匹配结果", {}).get("score", 0), "score说明": match_item.get("匹配结果", {}).get("score说明", "") or "", "相同部分": match_item.get("匹配结果", {}).get("相同部分", {}) or {}, "增量部分": match_item.get("匹配结果", {}).get("增量部分", {}) or {}, "输入B": input_info.get("B", "") or "", "输入A": input_info.get("A", "") or "", "B_Context": input_info.get("B_Context", "") or "", "A_Context": input_info.get("A_Context", "") or "" } # 为notes添加匹配分数、原始索引,并准备排序 notes_with_scores = [] for original_idx, note in enumerate(notes): note_id = note.get("channel_content_id", "") score_info = match_scores.get(note_id, None) notes_with_scores.append({ "note": note, "score_info": score_info, "original_index": original_idx, # 原始搜索结果位置(0-based) "page": (original_idx // 20) + 1, # 第几页(假设每页20条) "position_in_page": (original_idx % 20) + 1 # 页内位置 }) # 默认按分数降序排序(没有分数的放到最后) notes_with_scores.sort(key=lambda x: x["score_info"]["score"] if x["score_info"] else -1, reverse=True) # 生成搜索参数HTML search_params_html = "" if search_params: keyword = search_params.get("keyword", "") content_type = search_params.get("content_type", "不限") sort_type = search_params.get("sort_type", "综合") publish_time = search_params.get("publish_time", "不限") search_params_html = f'''
🔍 搜索参数
关键词: {html_module.escape(keyword)}
内容类型: {html_module.escape(content_type)}
排序方式: {html_module.escape(sort_type)}
发布时间: {html_module.escape(publish_time)}
''' # 生成搜索结果统计HTML(带排序按钮) search_summary_html = f'''
📊 搜索结果
共找到 {notes_count} 条相关内容
排序:
''' if notes_count > 0: notes_items = "" for note_idx, item in enumerate(notes_with_scores): # 使用包含元数据的列表 note = item["note"] score_info = item["score_info"] original_index = item["original_index"] page = item["page"] position_in_page = item["position_in_page"] title = note.get("title", "") desc = note.get("desc", "") link = note.get("link", "") author = note.get("channel_account_name", "") like_count = note.get("like_count", 0) comment_count = note.get("comment_count", 0) images = note.get("images", []) content_id = note.get("channel_content_id", "") publish_time = note.get("publish_time", "") note_id = f"note-{idx}-{note_idx}" # 生成图片轮播HTML images_html = "" if images and len(images) > 0: images_track = "".join([f'图片{i+1}' for i, img in enumerate(images)]) # 图片导航按钮和指示点 nav_buttons = "" dots_html = "" if len(images) > 1: nav_buttons = f''' ''' dots = "".join([f'
' for i in range(len(images))]) dots_html = f'
{dots}
' images_html = f''' ''' else: # 无图片时显示占位符 images_html = f''' ''' # 准备详情数据 note_data = { "title": title, "desc": desc, "link": link, "author": author, "like_count": like_count, "comment_count": comment_count, "images": images } note_data_json = json.dumps(note_data, ensure_ascii=False) note_data_escaped = html_module.escape(note_data_json) # 生成匹配分数HTML score_badge_html = "" score_detail_html = "" if score_info: note_score = score_info["score"] note_score_explain = score_info.get("score说明", "") or "" note_same_parts = score_info.get("相同部分", {}) or {} note_increment_parts = score_info.get("增量部分", {}) or {} input_b = score_info.get("输入B", "") or "" input_a = score_info.get("输入A", "") or "" b_context = score_info.get("B_Context", "") or "" a_context = score_info.get("A_Context", "") or "" # 分数详情JSON score_detail_data = { "score": note_score, "score说明": note_score_explain, "相同部分": note_same_parts, "增量部分": note_increment_parts, "输入B": input_b, "输入A": input_a, "B_Context": b_context, "A_Context": a_context } score_detail_json = json.dumps(score_detail_data, ensure_ascii=False) score_detail_escaped = html_module.escape(score_detail_json) score_badge_html = f'''
匹配分数 {note_score:.2f}
''' # 生成发布日期HTML publish_date_html = "" if publish_time: publish_date_html = f'
📅 {html_module.escape(publish_time)}
' # 计算匹配分数(用于排序) sort_score = score_info["score"] if score_info else -1 notes_items += f'''
{score_badge_html} {images_html}
{html_module.escape(title) if title else "无标题"}
{html_module.escape(desc) if desc else "暂无描述"}
{publish_date_html}
''' search_html = f'''
{search_params_html} {search_summary_html}
{notes_items}
''' else: # 没有搜索结果时也显示参数 search_html = f'''
{search_params_html} {search_summary_html}
''' # 只有第一个匹配项默认展开 expanded_class = " expanded" if idx == 0 else "" # 生成当前匹配项的灵感点和灵感分类详情 match_info_section = "" if inspiration_to_post_data and category_index_data: # 获取灵感点数据 inspiration_points = inspiration_to_post_data.get("点到帖子映射", {}).get("灵感点", {}) inspiration_info = inspiration_points.get(inspiration_name, {}) # 获取当前匹配的灵感分类数据 categories = category_index_data.get("灵感分类", {}) category_info = categories.get(element_name, {}) # 生成灵感点详情HTML(左栏) insp_detail_html = "" if inspiration_info: insp_dimension = inspiration_info.get("维度", "") insp_desc = inspiration_info.get("描述", "") insp_posts = inspiration_info.get("帖子详情列表", []) # 生成帖子卡片 insp_posts_html = "" for post in insp_posts[:6]: # 最多显示6个 insp_posts_html += generate_post_card_html(post, f"{unique_match_id}-insp-post", post_to_mapping_data) insp_detail_html = f'''
[灵感点] {html_module.escape(inspiration_name)}
{f'
维度:{html_module.escape(insp_dimension)}
' if insp_dimension else ''} {f'
描述:{html_module.escape(insp_desc)}
' if insp_desc else ''} {f'
相关帖子
{insp_posts_html}
' if insp_posts_html else ''}
''' # 生成灵感分类详情HTML(右栏) cat_detail_html = "" if category_info: cat_level = category_info.get("分类层级", "") cat_definition = category_info.get("分类定义", "") cat_posts = category_info.get("帖子详情列表", []) # 生成帖子卡片 cat_posts_html = "" for post in cat_posts[:6]: # 最多显示6个 cat_posts_html += generate_post_card_html(post, f"{unique_match_id}-cat-post", post_to_mapping_data) cat_detail_html = f'''
[灵感分类] {html_module.escape(element_name)}
{f'
层级:{html_module.escape(cat_level)}
' if cat_level else ''} {f'
定义:{html_module.escape(cat_definition)}
' if cat_definition else ''} {f'
相关帖子
{cat_posts_html}
' if cat_posts_html else ''}
''' if insp_detail_html or cat_detail_html: match_info_section = f'''
{insp_detail_html} {cat_detail_html}
''' # 步骤1:灵感点匹配灵感分类 step1_html = f'''
步骤 1 灵感点匹配灵感分类
{same_parts_html}
{increment_parts_html}
{f'
💡 分数说明
{html_module.escape(score_explain)}
' if score_explain else ''}
''' # 步骤2:搜索结果(如果有) step2_html = "" if search_html: step2_html = f'''
步骤 2 搜索
2.1 直接搜索灵感分类
{search_html}
''' # 创建安全的ID(移除特殊字符) safe_element_id = ''.join(c if c.isalnum() or c in '_-' else '_' for c in element_name) matches_html += f'''
Top {idx + 1} [灵感点] {html_module.escape(inspiration_name)}
匹配分数 {score:.2f}
[灵感分类] {html_module.escape(element_name)}
{match_info_section} {step1_html} {step2_html}
''' # 获取top1匹配的灵感分类名称(用于顶部标题) top1_category_name = "" if step1: step1_matches = step1.get("匹配结果列表", []) if step1_matches: top_match = step1_matches[0] input_info = top_match.get("输入信息", {}) top1_category_name = input_info.get("A", "") html = f'''
{matches_html}
''' return html def generate_detail_html(inspiration_data: Dict[str, Any]) -> str: """ 生成灵感点的详细信息HTML Args: inspiration_data: 灵感点数据 Returns: 详细信息的HTML字符串 """ import html as html_module summary = inspiration_data.get("summary", {}) step1 = inspiration_data.get("step1", {}) step2 = inspiration_data.get("step2", {}) inspiration_name = inspiration_data.get("inspiration_name", "未知灵感") content = f''' ''' # 获取元数据,用于后面的日志链接 metadata = summary.get("元数据", {}) # Step1 详细信息 if step1 and step1.get("灵感"): inspiration = step1.get("灵感", "") matches = step1.get("匹配结果列表", []) content += f''' ''' # 日志链接 if metadata.get("log_url"): content += f''' ''' return content def generate_detail_modal_content_js() -> str: """ 生成详情弹窗内容的JavaScript函数 Returns: JavaScript代码字符串 """ return ''' // 笔记图片当前索引管理 const noteImageStates = {}; // 移动笔记图片 function moveNoteImage(noteId, direction) { if (!noteImageStates[noteId]) { noteImageStates[noteId] = 0; } const carousel = document.querySelector(`[data-note-id="${noteId}"]`); if (!carousel) return; const totalImages = parseInt(carousel.dataset.totalImages); const track = document.getElementById(noteId + '-track'); if (!track) return; let newIndex = noteImageStates[noteId] + direction; if (newIndex < 0) newIndex = 0; if (newIndex >= totalImages) newIndex = totalImages - 1; noteImageStates[noteId] = newIndex; // 移动轨道 track.style.transform = `translateX(-${newIndex * 100}%)`; // 更新指示点 const dots = document.querySelectorAll(`#${noteId}-dots .note-image-dot`); dots.forEach((dot, i) => { dot.classList.toggle('active', i === newIndex); }); // 更新按钮状态 const prevBtn = carousel.querySelector('.note-carousel-button.prev'); const nextBtn = carousel.querySelector('.note-carousel-button.next'); if (prevBtn) { prevBtn.classList.toggle('disabled', newIndex === 0); } if (nextBtn) { nextBtn.classList.toggle('disabled', newIndex >= totalImages - 1); } } // 显示笔记详情 function showNoteDetail(element) { const noteDataStr = element.dataset.noteData; if (!noteDataStr) return; try { const noteData = JSON.parse(noteDataStr); // 生成图片HTML let imagesHtml = ''; if (noteData.images && noteData.images.length > 0) { imagesHtml = noteData.images.map(img => `图片` ).join(''); } else { imagesHtml = '
暂无图片
'; } // 生成灵感点、关键点、目的点HTML let pointsDetailHtml = ''; const hasPoints = (noteData.inspiration_points && noteData.inspiration_points.length > 0) || (noteData.key_points && noteData.key_points.length > 0) || (noteData.purpose_points && noteData.purpose_points.length > 0); if (hasPoints) { let sections = []; // 灵感点 if (noteData.inspiration_points && noteData.inspiration_points.length > 0) { const items = noteData.inspiration_points.map(p => `
${p.灵感点 || ''}
${p.描述 ? `
${p.描述}
` : ''}
` ).join(''); sections.push(`
💡 灵感点
${items}
`); } // 关键点 if (noteData.key_points && noteData.key_points.length > 0) { const items = noteData.key_points.map(k => `
${k.关键点 || ''}
${k.描述 ? `
${k.描述}
` : ''}
` ).join(''); sections.push(`
🔑 关键点
${items}
`); } // 目的点 if (noteData.purpose_points && noteData.purpose_points.length > 0) { const items = noteData.purpose_points.map(p => `
${p.目的点 || ''}
${p.描述 ? `
${p.描述}
` : ''}
` ).join(''); sections.push(`
🎯 目的点
${items}
`); } pointsDetailHtml = `
${sections.join('')}
`; } const modalHtml = `
${noteData.title || '无标题'}
@${noteData.author}
👍 ${noteData.like_count} 💬 ${noteData.comment_count}
${noteData.desc ? `
${noteData.desc}
` : ''} ${pointsDetailHtml}
${imagesHtml}
`; let modal = document.getElementById('noteDetailModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'noteDetailModal'; modal.className = 'note-detail-modal'; modal.onclick = (e) => { if (e.target === modal) closeNoteDetail(); }; document.body.appendChild(modal); } modal.innerHTML = modalHtml; modal.classList.add('active'); document.body.style.overflow = 'hidden'; } catch (e) { console.error('Error parsing note data:', e); } } // 关闭笔记详情 function closeNoteDetail() { const modal = document.getElementById('noteDetailModal'); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } } // 显示分数详情 function showScoreDetail(element) { const scoreDetailStr = element.dataset.scoreDetail; if (!scoreDetailStr) return; try { const scoreData = JSON.parse(scoreDetailStr); const modal = document.getElementById('scoreDetailModal'); const modalBody = document.getElementById('scoreModalBody'); // 生成相同部分HTML let samePartsHTML = ''; if (scoreData.相同部分 && Object.keys(scoreData.相同部分).length > 0) { const sameItems = Object.entries(scoreData.相同部分).map(([key, value]) => `
${key}:${value}
` ).join(''); samePartsHTML = `
✅ 相同部分
${sameItems}
`; } // 生成增量部分HTML let incrementPartsHTML = ''; if (scoreData.增量部分 && Object.keys(scoreData.增量部分).length > 0) { const incItems = Object.entries(scoreData.增量部分).map(([key, value]) => `
${key}:${value}
` ).join(''); incrementPartsHTML = `
➕ 增量部分
${incItems}
`; } // 生成分数说明HTML let explainHTML = ''; if (scoreData.score说明) { explainHTML = `
💡 分数说明
${scoreData.score说明}
`; } // 生成输入信息HTML let inputInfoHTML = ''; if (scoreData.输入B || scoreData.输入A) { inputInfoHTML = `
📝 输入信息
${scoreData.输入B ? `
输入B(灵感点):
${scoreData.输入B}
` : ''} ${scoreData.B_Context ? `
B_Context:
${scoreData.B_Context}
` : ''} ${scoreData.输入A ? `
输入A(帖子标题):
${scoreData.输入A}
` : ''} ${scoreData.A_Context ? `
A_Context(帖子内容摘要):
${scoreData.A_Context}
` : ''}
`; } modalBody.innerHTML = `

匹配分数详情

匹配分数: ${scoreData.score.toFixed(2)}
${inputInfoHTML} ${explainHTML}
${samePartsHTML} ${incrementPartsHTML}
`; modal.classList.add('active'); document.body.style.overflow = 'hidden'; } catch (e) { console.error('Failed to parse score detail:', e); } } // 关闭分数详情 function closeScoreDetail() { const modal = document.getElementById('scoreDetailModal'); if (modal) { modal.classList.remove('active'); document.body.style.overflow = ''; } } // 点击Modal背景关闭分数详情 function closeScoreDetailModal(event) { if (event.target.id === 'scoreDetailModal') { closeScoreDetail(); } } // 搜索结果排序 function sortSearchResults(button, containerId) { const sortType = button.dataset.sort; const container = document.getElementById(containerId); if (!container) return; // 更新按钮状态 const allButtons = button.parentElement.querySelectorAll('.search-sort-btn'); allButtons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); // 获取所有搜索结果卡片 const items = Array.from(container.querySelectorAll('.search-note-item')); // 根据排序类型排序 items.sort((a, b) => { if (sortType === 'score') { const scoreA = parseFloat(a.dataset.score) || -1; const scoreB = parseFloat(b.dataset.score) || -1; return scoreB - scoreA; // 降序 } else if (sortType === 'original') { const indexA = parseInt(a.dataset.originalIndex) || 0; const indexB = parseInt(b.dataset.originalIndex) || 0; return indexA - indexB; // 升序 } else if (sortType === 'likes') { const likesA = parseInt(a.dataset.likes) || 0; const likesB = parseInt(b.dataset.likes) || 0; return likesB - likesA; // 降序 } return 0; }); // 重新排列DOM items.forEach(item => container.appendChild(item)); } // ESC键关闭详情 document.addEventListener('keydown', function(event) { if (event.key === 'Escape') { closeNoteDetail(); closeScoreDetail(); } }); // 切换主匹配项的展开/折叠 function toggleMainMatch(element) { const matchItem = element.closest('.match-item'); matchItem.classList.toggle('expanded'); } // 切换步骤wrapper的展开/折叠 function toggleStepWrapper(element) { const stepWrapper = element.closest('.step-section-wrapper'); stepWrapper.classList.toggle('expanded'); } // 切换步骤的展开/折叠 function toggleStep(element) { const stepSection = element.closest('.step-section'); stepSection.classList.toggle('expanded'); } // 切换匹配详情的展开/折叠 function toggleMatchSection(element) { const matchSection = element.closest('.match-section'); matchSection.classList.toggle('expanded'); } // 显示指定的灵感详情 function showDetail(index) { const details = document.querySelectorAll('.inspiration-detail'); details.forEach((detail, i) => { if (i === index) { detail.classList.add('active'); // 滚动到顶部 const section = document.querySelector('.inspirations-section'); if (section) { section.scrollTop = 0; } } else { detail.classList.remove('active'); } }); } // 生成导航目录 function generateNavigation() { const details = document.querySelectorAll('.inspiration-detail'); const navList = document.getElementById('navList'); navList.innerHTML = ''; details.forEach((detail, index) => { const name = detail.dataset.inspirationName; const score = parseFloat(detail.dataset.step1Score) || 0; const top3MatchesStr = detail.dataset.top3Matches; let top3Matches = []; try { top3Matches = JSON.parse(top3MatchesStr); } catch(e) { console.error('Error parsing top3 matches:', e); } const navItem = document.createElement('div'); navItem.className = 'nav-item'; if (index === 0) navItem.classList.add('active'); navItem.dataset.cardIndex = index; // 生成匹配列表HTML - 精简版:只显示[灵感分类]名称 score let matchesHtml = ''; if (top3Matches && top3Matches.length > 0) { matchesHtml = ''; } navItem.innerHTML = ` ${matchesHtml} `; navItem.addEventListener('click', () => { // 移除所有active状态 document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); // 添加当前active状态 navItem.classList.add('active'); // 显示对应详情 showDetail(index); }); navList.appendChild(navItem); }); // 默认显示第一个详情 if (details.length > 0) { showDetail(0); } } // 更新面包屑 function updateBreadcrumb(matchName, stepName) { const breadcrumb = document.querySelector('.inspiration-detail.active #dynamicBreadcrumb'); if (!breadcrumb) return; const inspirationName = document.querySelector('.inspiration-detail.active').dataset.inspirationName; let breadcrumbHtml = ` [灵感点] ${inspirationName} `; if (matchName) { breadcrumbHtml += ` [灵感分类] ${matchName} `; } if (stepName) { breadcrumbHtml += ` ${stepName} `; } breadcrumb.innerHTML = breadcrumbHtml; } // 监听滚动,更新面包屑 function setupBreadcrumbObserver() { const activeDetail = document.querySelector('.inspiration-detail.active'); if (!activeDetail) return; const contentWrapper = activeDetail.querySelector('.inspiration-content-wrapper'); if (!contentWrapper) return; // 获取所有需要监听的section const sections = activeDetail.querySelectorAll('.step-section'); if (sections.length === 0) return; // 创建Intersection Observer const observerOptions = { root: null, rootMargin: '-100px 0px -50% 0px', threshold: 0 }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const section = entry.target; const matchItem = section.closest('.match-item'); const matchName = matchItem ? matchItem.dataset.matchName : ''; const stepName = section.dataset.stepName || ''; updateBreadcrumb(matchName, stepName); } }); }, observerOptions); // 观察所有section sections.forEach(section => observer.observe(section)); // 存储observer以便清理 if (!window.breadcrumbObservers) { window.breadcrumbObservers = []; } window.breadcrumbObservers.push(observer); } // 页面加载时生成导航 document.addEventListener('DOMContentLoaded', () => { generateNavigation(); setupBreadcrumbObserver(); }); // 当切换灵感点时,重新设置observer const originalShowDetail = showDetail; showDetail = function(index) { // 清理旧的observers if (window.breadcrumbObservers) { window.breadcrumbObservers.forEach(obs => obs.disconnect()); window.breadcrumbObservers = []; } // 调用原始函数 originalShowDetail(index); // 设置新的observer setTimeout(() => { setupBreadcrumbObserver(); }, 100); }; ''' def generate_persona_structure_html(persona_data: Dict[str, Any]) -> str: """ 生成人设结构的树状HTML Args: persona_data: 人设数据 Returns: 人设结构的HTML字符串 """ if not persona_data: return '
暂无人设数据
' inspiration_list = persona_data.get("灵感点列表", []) if not inspiration_list: return '
暂无灵感点列表数据
' html_parts = ['
'] for perspective_idx, perspective in enumerate(inspiration_list): perspective_name = perspective.get("视角名称", "未知视角") perspective_desc = perspective.get("视角描述", "") pattern_list = perspective.get("模式列表", []) # 一级节点:视角 html_parts.append(f''' ') html_parts.append('
') return ''.join(html_parts) def generate_html( inspirations_data: List[Dict[str, Any]], posts_map: Dict[str, Dict[str, Any]], persona_data: Dict[str, Any], output_path: str, inspiration_to_post_data: Dict[str, Any] = None, category_index_data: Dict[str, Any] = None, post_to_mapping_data: Dict[str, Any] = None ) -> str: """ 生成完整的可视化HTML Args: inspirations_data: 灵感点数据列表 posts_map: 帖子数据映射 persona_data: 人设数据 output_path: 输出文件路径 Returns: 输出文件路径 """ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 统计信息 total_count = len(inspirations_data) # Step1 统计 step1_excellent_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) >= 0.7) step1_good_count = sum(1 for d in inspirations_data if 0.5 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.7) step1_normal_count = sum(1 for d in inspirations_data if 0.3 <= d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.5) step1_need_opt_count = sum(1 for d in inspirations_data if d["summary"].get("关键指标", {}).get("step1_top1_score", 0) < 0.3) # 平均分数 total_step1_score = sum(d["summary"].get("关键指标", {}).get("step1_top1_score", 0) for d in inspirations_data) avg_step1_score = total_step1_score / total_count if total_count > 0 else 0 # 按Step1分数排序 inspirations_data_sorted = sorted( inspirations_data, key=lambda x: x["summary"].get("关键指标", {}).get("step1_top1_score", 0), reverse=True ) # 生成卡片HTML cards_html = [ generate_inspiration_card_html(data, inspiration_to_post_data, category_index_data, post_to_mapping_data) for data in inspirations_data_sorted ] cards_html_str = '\n'.join(cards_html) # 生成人设结构HTML persona_structure_html = generate_persona_structure_html(persona_data) # 生成JavaScript detail_modal_js = generate_detail_modal_content_js() # 完整HTML html_content = f''' 灵感点分析可视化
{cards_html_str}
''' # 写入文件 output_file = Path(output_path) output_file.parent.mkdir(parents=True, exist_ok=True) with open(output_file, 'w', encoding='utf-8') as f: f.write(html_content) return str(output_file.absolute()) def load_persona_data(persona_path: str) -> Dict[str, Any]: """ 加载人设数据 Args: persona_path: 人设JSON文件路径 Returns: 人设数据字典 """ try: with open(persona_path, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: print(f"警告: 读取人设文件失败: {e}") return {} def main(): """主函数""" import sys import os # 配置路径(使用当前脚本的相对路径) script_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.join(script_dir, "data/阿里多多酱") inspiration_dir = os.path.join(base_dir, "out/人设_1110/how/灵感点") posts_dir = os.path.join(base_dir, "作者历史帖子") persona_path = os.path.join(base_dir, "out/人设_1110/人设.json") inspiration_to_post_path = os.path.join(base_dir, "out/人设_1110/点到帖子映射.json") category_index_path = os.path.join(base_dir, "out/人设_1110/分类索引_完整.json") post_to_mapping_path = os.path.join(base_dir, "out/人设_1110/帖子到分类和点映射.json") output_path = os.path.join(base_dir, "out/人设_1110/how/灵感点可视化.html") print("=" * 60) print("灵感点分析可视化脚本") print("=" * 60) # 加载数据 print("\n📂 正在加载灵感点数据...") inspirations_data = load_inspiration_points_data(inspiration_dir) print(f"✅ 成功加载 {len(inspirations_data)} 个灵感点") print("\n📂 正在加载帖子数据...") posts_map = load_posts_data(posts_dir) print(f"✅ 成功加载 {len(posts_map)} 个帖子") print("\n📂 正在加载人设数据...") persona_data = load_persona_data(persona_path) print(f"✅ 成功加载人设数据") print("\n📂 正在加载点到帖子映射数据...") with open(inspiration_to_post_path, 'r', encoding='utf-8') as f: inspiration_to_post_data = json.load(f) print(f"✅ 成功加载点到帖子映射数据") print("\n📂 正在加载分类索引数据...") with open(category_index_path, 'r', encoding='utf-8') as f: category_index_data = json.load(f) print(f"✅ 成功加载分类索引数据") print("\n📂 正在加载帖子到分类和点映射数据...") with open(post_to_mapping_path, 'r', encoding='utf-8') as f: post_to_mapping_data = json.load(f) print(f"✅ 成功加载帖子到分类和点映射数据") # 生成HTML print("\n🎨 正在生成可视化HTML...") result_path = generate_html( inspirations_data, posts_map, persona_data, output_path, inspiration_to_post_data, category_index_data, post_to_mapping_data ) print(f"\n✅ 可视化文件已生成!") print(f"📄 文件路径: {result_path}") print(f"\n💡 在浏览器中打开该文件即可查看可视化结果") print("=" * 60) if __name__ == "__main__": main()