| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- 级联搜索结果可视化工具
- 展示候选词 → Top3人设特征 → 搜索词 → 搜索结果的完整流程
- """
- import json
- import os
- import sys
- from datetime import datetime
- from typing import List, Dict, Any, Set
- import webbrowser
- def load_json(file_path: str) -> Any:
- """加载JSON文件"""
- with open(file_path, 'r', encoding='utf-8') as f:
- return json.load(f)
- def load_all_data(output_dir: str = "output_v2") -> Dict[str, Any]:
- """
- 加载所有需要的数据文件
- Returns:
- 包含所有数据的字典
- """
- print("正在加载数据文件...")
- data = {
- 'filtered_features': load_json(os.path.join(output_dir, 'filtered_features.json')),
- 'candidate_words': load_json(os.path.join(output_dir, 'candidate_words.json')),
- 'search_queries': load_json(os.path.join(output_dir, 'search_queries_evaluated.json')),
- 'search_results': load_json(os.path.join(output_dir, 'search_results.json')),
- 'evaluated_results': load_json(os.path.join(output_dir, 'evaluated_results.json'))
- }
- # 尝试加载深度分析数据(可选)
- deep_path = os.path.join(output_dir, 'deep_analysis_results.json')
- similarity_path = os.path.join(output_dir, 'similarity_analysis_results.json')
- if os.path.exists(deep_path):
- deep_data = load_json(deep_path)
- # 创建note_id到解构数据的映射
- data['stage7_mapping'] = {}
- for result in deep_data.get('results', []):
- note_id = result.get('note_id')
- if note_id:
- data['stage7_mapping'][note_id] = result
- else:
- data['stage7_mapping'] = {}
- if os.path.exists(similarity_path):
- sim_data = load_json(similarity_path)
- # 创建note_id到相似度数据的映射
- data['stage8_mapping'] = {}
- for result in sim_data.get('results', []):
- note_id = result.get('note_id')
- if note_id:
- data['stage8_mapping'][note_id] = result
- else:
- data['stage8_mapping'] = {}
- print(f" ✓ 已加载 {len(data['filtered_features'])} 个原始特征")
- print(f" ✓ 已加载 {len(data['candidate_words'])} 个候选词数据")
- print(f" ✓ 已加载解构数据: {len(data['stage7_mapping'])} 个帖子")
- print(f" ✓ 已加载相似度数据: {len(data['stage8_mapping'])} 个帖子")
- return data
- def extract_global_candidates(data: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
- """
- 提取全局候选词并按相似度分类
- Returns:
- {
- 'matched': [...], # 相似度 >= 0.8
- 'partial': [...], # 0.5 <= 相似度 < 0.8
- 'unmatched': [...] # 相似度 < 0.5
- }
- """
- print("\n提取全局候选词...")
- candidates_map = {} # 用于去重
- # 遍历所有特征的候选词
- for feature_data in data['candidate_words']:
- candidates_by_base = feature_data.get('高相似度候选_按base_word', {})
- for base_word, candidates in candidates_by_base.items():
- for cand in candidates:
- cand_name = cand.get('候选词', '')
- if not cand_name:
- continue
- # 计算相似度
- similarity = cand.get('相似度', 0)
- # 如果是帖子候选词,使用点最高人设相似度
- if cand.get('候选词类型') == 'post':
- similarity = cand.get('点最高人设相似度', similarity)
- # 去重:保留最高相似度
- if cand_name not in candidates_map or similarity > candidates_map[cand_name]['相似度']:
- candidates_map[cand_name] = {
- '名称': cand_name,
- '类型': cand.get('候选词类型', 'unknown'),
- '相似度': similarity,
- '特征类型': cand.get('特征类型', ''),
- '来源路径': cand.get('来源路径', ''),
- '匹配说明': cand.get('匹配说明', '')
- }
- # 按相似度分类
- result = {
- 'matched': [], # >= 0.8
- 'partial': [], # 0.5 ~ 0.8
- 'unmatched': [] # < 0.5
- }
- for cand in candidates_map.values():
- similarity = cand['相似度']
- if similarity >= 0.8:
- result['matched'].append(cand)
- elif similarity >= 0.5:
- result['partial'].append(cand)
- else:
- result['unmatched'].append(cand)
- # 排序:按相似度降序
- for category in result.values():
- category.sort(key=lambda x: x['相似度'], reverse=True)
- print(f" ✓ 已匹配: {len(result['matched'])} 个")
- print(f" ✓ 部分匹配: {len(result['partial'])} 个")
- print(f" ✓ 不匹配: {len(result['unmatched'])} 个")
- return result
- def render_left_candidates_html(global_candidates: Dict[str, List[Dict[str, Any]]]) -> str:
- """
- 渲染左侧固定候选词区域HTML
- Args:
- global_candidates: 分类后的全局候选词
- Returns:
- HTML字符串
- """
- html_parts = []
- html_parts.append('''
- <div class="left-candidates-panel">
- <div class="candidates-header">
- <div class="candidates-title">📚 可用候选词</div>
- <div class="candidates-hint">此区域固定展示 不随滚动</div>
- </div>
- <div class="candidates-content">
- ''')
- # 已匹配区域
- html_parts.append('''
- <div class="candidates-section matched-section">
- <div class="section-title">✅ 已匹配 <span class="section-count">({count})</span></div>
- <div class="section-hint">与人设相似度 ≥ 0.8</div>
- <div class="candidates-list">
- '''.format(count=len(global_candidates['matched'])))
- for cand in global_candidates['matched']:
- icon = '📝' if cand['类型'] == 'post' else '👤'
- type_label = '帖子' if cand['类型'] == 'post' else '人设'
- html_parts.append(f'''
- <div class="candidate-item matched">
- <div class="candidate-icon">{icon}</div>
- <div class="candidate-info">
- <div class="candidate-name">{cand['名称']}</div>
- <div class="candidate-meta">
- <span class="candidate-type">{type_label}</span>
- <span class="candidate-similarity">{cand['相似度']:.2f}</span>
- </div>
- </div>
- </div>
- ''')
- html_parts.append('''
- </div>
- </div>
- ''')
- # 部分匹配区域
- html_parts.append('''
- <div class="candidates-section partial-section">
- <div class="section-title">🟡 部分匹配 <span class="section-count">({count})</span></div>
- <div class="section-hint">与人设特征相似度 0.5-0.8</div>
- <div class="candidates-list">
- '''.format(count=len(global_candidates['partial'])))
- for cand in global_candidates['partial']:
- icon = '📝' if cand['类型'] == 'post' else '👤'
- type_label = '帖子' if cand['类型'] == 'post' else '人设'
- html_parts.append(f'''
- <div class="candidate-item partial">
- <div class="candidate-icon">{icon}</div>
- <div class="candidate-info">
- <div class="candidate-name">{cand['名称']}</div>
- <div class="candidate-meta">
- <span class="candidate-type">{type_label}</span>
- <span class="candidate-similarity">{cand['相似度']:.2f}</span>
- </div>
- </div>
- </div>
- ''')
- html_parts.append('''
- </div>
- </div>
- ''')
- # 不匹配区域
- html_parts.append('''
- <div class="candidates-section unmatched-section">
- <div class="section-title">❌ 不匹配 <span class="section-count">({count})</span></div>
- <div class="section-hint">与人设特征相似度 < 0.5</div>
- <div class="candidates-list">
- '''.format(count=len(global_candidates['unmatched'])))
- for cand in global_candidates['unmatched']:
- icon = '📝' if cand['类型'] == 'post' else '👤'
- type_label = '帖子' if cand['类型'] == 'post' else '人设'
- html_parts.append(f'''
- <div class="candidate-item unmatched">
- <div class="candidate-icon">{icon}</div>
- <div class="candidate-info">
- <div class="candidate-name">{cand['名称']}</div>
- <div class="candidate-meta">
- <span class="candidate-type">{type_label}</span>
- <span class="candidate-similarity">{cand['相似度']:.2f}</span>
- </div>
- </div>
- </div>
- ''')
- html_parts.append('''
- </div>
- </div>
- </div>
- </div>
- ''')
- return ''.join(html_parts)
- def render_cascade_flow_html(data: Dict[str, Any]) -> str:
- """
- 渲染中间级联流程HTML(三层结构)
- Returns:
- HTML字符串
- """
- html_parts = []
- html_parts.append('''
- <div class="cascade-flow-panel">
- <div class="cascade-header">
- <div class="cascade-title">🔄 级联搜索流程</div>
- </div>
- <div class="cascade-content" id="cascadeContent">
- ''')
- # 默认显示第一个特征的级联流程
- if data['evaluated_results']:
- first_feature = data['evaluated_results'][0]
- html_parts.append(render_single_cascade(first_feature, 0, data))
- html_parts.append('''
- </div>
- </div>
- ''')
- return ''.join(html_parts)
- def render_single_cascade(feature_data: Dict[str, Any], feature_idx: int, data: Dict[str, Any]) -> str:
- """
- 渲染单个特征的级联流程
- Args:
- feature_data: 特征数据
- feature_idx: 特征索引
- data: 全部数据
- Returns:
- HTML字符串
- """
- html_parts = []
- original_feature = feature_data.get('原始特征名称', '')
- top3_matches = feature_data.get('top3匹配信息', [])
- groups = feature_data.get('组合评估结果_分组', [])
- # 层级1: 原始特征
- html_parts.append(f'''
- <div class="cascade-layer layer-1">
- <div class="layer-title">📌 帖子选题点</div>
- <div class="feature-selector">
- <div class="selected-feature">
- <div class="feature-name">{original_feature}</div>
- <div class="feature-actions">
- <button class="switch-feature-btn" onclick="showFeatureSelector()">切换特征</button>
- </div>
- </div>
- </div>
- </div>
- ''')
- # 层级2: Top3人设特征
- html_parts.append('''
- <div class="cascade-arrow">↓</div>
- <div class="cascade-layer layer-2">
- <div class="layer-title">🎯 Top1各 相似度(x)</div>
- <div class="top3-container">
- ''')
- for idx, match in enumerate(top3_matches[:3], 1):
- base_word = match.get('人设特征名称', '')
- similarity = match.get('相似度', 0)
- is_top1 = (idx == 1)
- card_class = 'top3-card top1-card' if is_top1 else 'top3-card'
- html_parts.append(f'''
- <div class="{card_class}" data-feature-idx="{feature_idx}" data-match-idx="{idx-1}" onclick="selectBaseWord({feature_idx}, {idx-1})">
- <div class="top3-rank">Top{idx}</div>
- <div class="top3-name">{base_word}</div>
- <div class="top3-similarity">相似度: {similarity:.2f}</div>
- </div>
- ''')
- html_parts.append('''
- </div>
- </div>
- ''')
- # 层级3: 搜索词(默认展开Top1)
- if groups:
- html_parts.append('''
- <div class="cascade-arrow">↓</div>
- <div class="cascade-layer layer-3">
- <div class="layer-title">🔍 搜索词生成</div>
- <div class="search-words-container" id="searchWordsContainer">
- ''')
- # 默认显示第一个group(Top1)
- html_parts.append(render_search_words_group(groups[0], feature_idx, 0))
- html_parts.append('''
- </div>
- </div>
- ''')
- return ''.join(html_parts)
- def render_search_words_group(group: Dict[str, Any], feature_idx: int, group_idx: int) -> str:
- """
- 渲染搜索词组
- Args:
- group: 搜索词组数据
- feature_idx: 特征索引
- group_idx: 组索引
- Returns:
- HTML字符串
- """
- html_parts = []
- base_word = group.get('base_word', '')
- searches = group.get('top10_searches', [])
- available_words = group.get('available_words', [])
- html_parts.append(f'''
- <div class="search-words-group" data-base-word="{base_word}">
- <div class="base-word-label">中心词: <span class="base-word-value">{base_word}</span></div>
- ''')
- # 显示每个搜索词
- for sw_idx, search in enumerate(searches):
- html_parts.append(render_search_word_card(search, feature_idx, group_idx, sw_idx, available_words))
- html_parts.append('''
- </div>
- ''')
- return ''.join(html_parts)
- def render_search_word_card(search: Dict[str, Any], feature_idx: int, group_idx: int, sw_idx: int, available_words: List) -> str:
- """
- 渲染单个搜索词卡片
- Args:
- search: 搜索词数据
- feature_idx, group_idx, sw_idx: 索引
- available_words: 可用候选词列表
- Returns:
- HTML字符串
- """
- search_word = search.get('search_word', '')
- score = search.get('score', 0)
- reasoning = search.get('reasoning', '')
- has_result = search.get('search_result') is not None
- # 检查是否已执行搜索
- status_icon = '✅' if has_result else '⏸️'
- status_text = '已搜索' if has_result else '未搜索'
- status_class = 'searched' if has_result else 'not-searched'
- # 显示候选词(最多前10个)
- cand_names = [w.get('候选词', '') if isinstance(w, dict) else w for w in available_words[:10]]
- cand_display = ', '.join(cand_names) if cand_names else '无'
- html = f'''
- <div class="search-word-card {status_class}" data-feature-idx="{feature_idx}" data-group-idx="{group_idx}" data-sw-idx="{sw_idx}" onclick="selectSearchWord({feature_idx}, {group_idx}, {sw_idx})">
- <div class="sw-header">
- <div class="sw-status">{status_icon} {status_text}</div>
- <div class="sw-rank">#{sw_idx + 1}</div>
- </div>
- <div class="sw-candidates-pool">
- <div class="sw-label">可用候选词池:</div>
- <div class="sw-candidates">{cand_display}</div>
- </div>
- <div class="sw-arrow-container">
- <div class="sw-arrow">
- <span class="arrow-line">→</span>
- <span class="arrow-score">score: {score:.2f}</span>
- </div>
- </div>
- <div class="sw-result">
- <div class="sw-query">{search_word}</div>
- </div>
- <div class="sw-reasoning">
- <div class="reasoning-label">💡 LLM推理理由:</div>
- <div class="reasoning-content">{reasoning}</div>
- </div>
- </div>
- '''
- return html
- def generate_html(data: Dict[str, Any], global_candidates: Dict[str, List[Dict[str, Any]]]) -> str:
- """
- 生成完整HTML页面
- Args:
- data: 所有数据
- global_candidates: 全局候选词
- Returns:
- 完整HTML字符串
- """
- print("\n正在生成HTML...")
- # 准备数据JSON
- data_json = json.dumps(data['evaluated_results'], ensure_ascii=False)
- stage7_json = json.dumps(data['stage7_mapping'], ensure_ascii=False)
- stage8_json = json.dumps(data['stage8_mapping'], ensure_ascii=False)
- # 生成各部分HTML
- left_html = render_left_candidates_html(global_candidates)
- cascade_html = render_cascade_flow_html(data)
- # 生成完整HTML
- html_template = f'''<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>级联搜索结果可视化</title>
- <style>
- {get_css_styles()}
- </style>
- </head>
- <body>
- <div class="page-header">
- <div class="header-title">🔍 级联搜索结果可视化系统</div>
- <div class="header-subtitle">候选词 → Top3人设特征 → 搜索词 → 搜索结果</div>
- </div>
- <div class="main-layout">
- <!-- 左侧:候选词库 -->
- {left_html}
- <!-- 中间:级联流程 -->
- {cascade_html}
- <!-- 右侧:搜索结果 -->
- <div class="right-results-panel">
- <div class="results-header">
- <div class="results-title">📝 搜索结果卡片</div>
- <div class="results-subtitle" id="resultsSubtitle">请选择一个搜索词查看结果</div>
- </div>
- <div class="results-content" id="resultsContent">
- <div class="empty-results">
- <div class="empty-icon">🔍</div>
- <div class="empty-text">选择搜索词后,这里将显示对应的搜索结果</div>
- </div>
- </div>
- </div>
- </div>
- <!-- 特征选择器Modal -->
- <div class="modal-overlay" id="featureSelectorModal">
- <div class="modal-window">
- <div class="modal-header">
- <div class="modal-title">选择原始特征</div>
- <button class="modal-close-btn" onclick="closeFeatureSelector()">×</button>
- </div>
- <div class="modal-body">
- <div class="feature-list" id="featureList"></div>
- </div>
- </div>
- </div>
- <script>
- // 数据
- const allData = {data_json};
- const stage7Data = {stage7_json};
- const stage8Data = {stage8_json};
- let currentFeatureIdx = 0;
- let currentGroupIdx = 0;
- let currentSwIdx = 0;
- {get_javascript_code()}
- </script>
- </body>
- </html>
- '''
- print(" ✓ HTML生成完成")
- return html_template
- def get_css_styles() -> str:
- """获取CSS样式"""
- return '''
- * {
- 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;
- }
- /* 页面头部 */
- .page-header {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 20px;
- text-align: center;
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- }
- .header-title {
- font-size: 24px;
- font-weight: bold;
- margin-bottom: 5px;
- }
- .header-subtitle {
- font-size: 14px;
- opacity: 0.9;
- }
- /* 主布局 - 三栏 */
- .main-layout {
- display: flex;
- gap: 20px;
- padding: 20px;
- height: calc(100vh - 100px);
- }
- /* 左侧候选词面板 - 固定 */
- .left-candidates-panel {
- width: 280px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- position: sticky;
- top: 20px;
- height: fit-content;
- max-height: calc(100vh - 140px);
- display: flex;
- flex-direction: column;
- }
- .candidates-header {
- padding: 15px;
- border-bottom: 2px solid #e5e7eb;
- }
- .candidates-title {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 5px;
- }
- .candidates-hint {
- font-size: 11px;
- color: #ef4444;
- font-weight: 500;
- }
- .candidates-content {
- flex: 1;
- overflow-y: auto;
- padding: 10px;
- }
- .candidates-section {
- margin-bottom: 15px;
- }
- .section-title {
- font-size: 13px;
- font-weight: 600;
- margin-bottom: 5px;
- display: flex;
- align-items: center;
- gap: 5px;
- }
- .section-count {
- color: #6b7280;
- font-size: 12px;
- }
- .section-hint {
- font-size: 11px;
- color: #6b7280;
- margin-bottom: 8px;
- }
- .candidates-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .candidate-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px;
- border-radius: 6px;
- border: 1px solid #e5e7eb;
- transition: all 0.2s;
- cursor: pointer;
- }
- .candidate-item:hover {
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
- transform: translateY(-1px);
- }
- .candidate-item.matched {
- background: #f0fdf4;
- border-color: #86efac;
- }
- .candidate-item.partial {
- background: #fffbeb;
- border-color: #fcd34d;
- }
- .candidate-item.unmatched {
- background: #fef2f2;
- border-color: #fca5a5;
- }
- .candidate-icon {
- font-size: 18px;
- flex-shrink: 0;
- }
- .candidate-info {
- flex: 1;
- min-width: 0;
- }
- .candidate-name {
- font-size: 12px;
- font-weight: 500;
- color: #374151;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .candidate-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 2px;
- }
- .candidate-type {
- font-size: 10px;
- color: #6b7280;
- }
- .candidate-similarity {
- font-size: 10px;
- font-weight: 600;
- color: #10b981;
- }
- /* 中间级联流程面板 */
- .cascade-flow-panel {
- flex: 1;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- overflow-y: auto;
- padding: 20px;
- }
- .cascade-header {
- margin-bottom: 20px;
- padding-bottom: 10px;
- border-bottom: 2px solid #e5e7eb;
- }
- .cascade-title {
- font-size: 18px;
- font-weight: 600;
- color: #374151;
- }
- .cascade-content {
- display: flex;
- flex-direction: column;
- gap: 15px;
- }
- .cascade-layer {
- background: #f9fafb;
- border-radius: 8px;
- padding: 15px;
- }
- .layer-title {
- font-size: 14px;
- font-weight: 600;
- color: #6b7280;
- margin-bottom: 10px;
- }
- /* 层级1: 特征选择器 */
- .selected-feature {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 12px;
- background: white;
- border-radius: 6px;
- border: 2px solid #667eea;
- }
- .feature-name {
- font-size: 15px;
- font-weight: 600;
- color: #374151;
- }
- .switch-feature-btn {
- padding: 6px 12px;
- background: #667eea;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- transition: all 0.2s;
- }
- .switch-feature-btn:hover {
- background: #5568d3;
- }
- /* 层级2: Top3卡片 */
- .top3-container {
- display: flex;
- gap: 10px;
- }
- .top3-card {
- flex: 1;
- padding: 12px;
- background: white;
- border-radius: 6px;
- border: 2px solid #e5e7eb;
- cursor: pointer;
- transition: all 0.2s;
- }
- .top3-card:hover {
- border-color: #667eea;
- box-shadow: 0 2px 6px rgba(102, 126, 234, 0.2);
- }
- .top3-card.top1-card {
- border-color: #10b981;
- background: #f0fdf4;
- }
- .top3-card.top1-card:hover {
- border-color: #059669;
- }
- .top3-card.selected {
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
- }
- .top3-rank {
- font-size: 11px;
- font-weight: 600;
- color: #6b7280;
- margin-bottom: 4px;
- }
- .top3-name {
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 4px;
- }
- .top3-similarity {
- font-size: 12px;
- color: #10b981;
- }
- /* 级联箭头 */
- .cascade-arrow {
- text-align: center;
- font-size: 24px;
- color: #667eea;
- margin: 5px 0;
- }
- /* 层级3: 搜索词 */
- .base-word-label {
- font-size: 13px;
- color: #6b7280;
- margin-bottom: 12px;
- }
- .base-word-value {
- font-weight: 600;
- color: #10b981;
- }
- .search-word-card {
- background: white;
- border-radius: 8px;
- border: 2px solid #e5e7eb;
- padding: 15px;
- margin-bottom: 12px;
- cursor: pointer;
- transition: all 0.2s;
- }
- .search-word-card:hover {
- border-color: #667eea;
- box-shadow: 0 2px 6px rgba(0,0,0,0.1);
- }
- .search-word-card.searched {
- border-color: #10b981;
- }
- .search-word-card.selected {
- border-color: #667eea;
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
- }
- .sw-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 10px;
- }
- .sw-status {
- font-size: 12px;
- font-weight: 600;
- color: #10b981;
- }
- .sw-rank {
- font-size: 11px;
- color: #6b7280;
- }
- .sw-candidates-pool {
- margin-bottom: 10px;
- }
- .sw-label {
- font-size: 11px;
- color: #6b7280;
- margin-bottom: 4px;
- }
- .sw-candidates {
- font-size: 12px;
- color: #374151;
- background: #f9fafb;
- padding: 6px;
- border-radius: 4px;
- }
- .sw-arrow-container {
- text-align: center;
- margin: 10px 0;
- }
- .sw-arrow {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- }
- .arrow-line {
- font-size: 20px;
- color: #667eea;
- }
- .arrow-score {
- font-size: 12px;
- font-weight: 600;
- color: #667eea;
- background: #ede9fe;
- padding: 2px 8px;
- border-radius: 4px;
- }
- .sw-result {
- text-align: center;
- margin-bottom: 10px;
- }
- .sw-query {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- background: #f0fdf4;
- padding: 8px;
- border-radius: 6px;
- border: 1px solid #86efac;
- }
- .sw-reasoning {
- background: #fffbeb;
- padding: 10px;
- border-radius: 6px;
- border: 1px solid #fcd34d;
- }
- .reasoning-label {
- font-size: 12px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 4px;
- }
- .reasoning-content {
- font-size: 12px;
- color: #6b7280;
- line-height: 1.5;
- }
- /* 右侧结果面板 */
- .right-results-panel {
- width: 500px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- }
- .results-header {
- padding: 15px;
- border-bottom: 2px solid #e5e7eb;
- }
- .results-title {
- font-size: 16px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 5px;
- }
- .results-subtitle {
- font-size: 12px;
- color: #6b7280;
- }
- .results-content {
- flex: 1;
- padding: 15px;
- }
- .empty-results {
- text-align: center;
- padding: 60px 20px;
- }
- .empty-icon {
- font-size: 48px;
- margin-bottom: 15px;
- }
- .empty-text {
- font-size: 14px;
- color: #6b7280;
- }
- /* Modal */
- .modal-overlay {
- display: none;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0,0,0,0.5);
- z-index: 1000;
- align-items: center;
- justify-content: center;
- }
- .modal-overlay.active {
- display: flex;
- }
- .modal-window {
- background: white;
- border-radius: 12px;
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
- max-width: 600px;
- width: 90%;
- max-height: 80vh;
- display: flex;
- flex-direction: column;
- }
- .modal-header {
- padding: 20px;
- border-bottom: 1px solid #e5e7eb;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .modal-title {
- font-size: 18px;
- font-weight: 600;
- color: #374151;
- }
- .modal-close-btn {
- background: none;
- border: none;
- font-size: 28px;
- color: #6b7280;
- cursor: pointer;
- padding: 0;
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 4px;
- }
- .modal-close-btn:hover {
- background: #f3f4f6;
- }
- .modal-body {
- flex: 1;
- overflow-y: auto;
- padding: 20px;
- }
- .feature-list {
- display: flex;
- flex-direction: column;
- gap: 10px;
- }
- .feature-list-item {
- padding: 12px;
- background: #f9fafb;
- border-radius: 6px;
- border: 2px solid #e5e7eb;
- cursor: pointer;
- transition: all 0.2s;
- }
- .feature-list-item:hover {
- border-color: #667eea;
- background: white;
- }
- .feature-list-item.active {
- border-color: #10b981;
- background: #f0fdf4;
- }
- '''
- def get_javascript_code() -> str:
- """获取JavaScript代码"""
- return '''
- // 初始化
- document.addEventListener('DOMContentLoaded', function() {
- console.log('页面加载完成');
- renderFeatureList();
- });
- // 显示特征选择器
- function showFeatureSelector() {
- const modal = document.getElementById('featureSelectorModal');
- modal.classList.add('active');
- }
- // 关闭特征选择器
- function closeFeatureSelector() {
- const modal = document.getElementById('featureSelectorModal');
- modal.classList.remove('active');
- }
- // 渲染特征列表
- function renderFeatureList() {
- const listEl = document.getElementById('featureList');
- let html = '';
- allData.forEach((feature, idx) => {
- const name = feature['原始特征名称'];
- const isActive = idx === currentFeatureIdx;
- const activeClass = isActive ? 'active' : '';
- html += `
- <div class="feature-list-item ${activeClass}" onclick="selectFeature(${idx})">
- ${name}
- </div>
- `;
- });
- listEl.innerHTML = html;
- }
- // 选择特征
- function selectFeature(featureIdx) {
- currentFeatureIdx = featureIdx;
- currentGroupIdx = 0;
- currentSwIdx = 0;
- closeFeatureSelector();
- updateCascadeView();
- renderFeatureList();
- }
- // 更新级联视图
- function updateCascadeView() {
- const feature = allData[currentFeatureIdx];
- const cascadeContent = document.getElementById('cascadeContent');
- // 重新渲染级联流程(这里简化处理,实际应该用JavaScript动态更新)
- location.reload(); // 简化版:重新加载页面
- }
- // 选择base_word
- function selectBaseWord(featureIdx, matchIdx) {
- currentFeatureIdx = featureIdx;
- currentGroupIdx = matchIdx;
- currentSwIdx = 0;
- // 移除所有选中状态
- document.querySelectorAll('.top3-card').forEach(card => {
- card.classList.remove('selected');
- });
- // 添加选中状态
- event.target.closest('.top3-card').classList.add('selected');
- // 更新搜索词显示
- const feature = allData[currentFeatureIdx];
- const groups = feature['组合评估结果_分组'] || [];
- if (groups[currentGroupIdx]) {
- // TODO: 更新搜索词列表显示
- console.log('切换到group:', currentGroupIdx);
- }
- }
- // 选择搜索词
- function selectSearchWord(featureIdx, groupIdx, swIdx) {
- currentFeatureIdx = featureIdx;
- currentGroupIdx = groupIdx;
- currentSwIdx = swIdx;
- // 移除所有搜索词的选中状态
- document.querySelectorAll('.search-word-card').forEach(card => {
- card.classList.remove('selected');
- });
- // 添加选中状态
- event.target.closest('.search-word-card').classList.add('selected');
- // 显示搜索结果
- renderSearchResults(featureIdx, groupIdx, swIdx);
- }
- // 渲染搜索结果
- function renderSearchResults(featureIdx, groupIdx, swIdx) {
- const feature = allData[featureIdx];
- const groups = feature['组合评估结果_分组'] || [];
- const group = groups[groupIdx];
- if (!group) return;
- const searches = group['top10_searches'] || [];
- const search = searches[swIdx];
- if (!search) return;
- const searchWord = search['search_word'] || '';
- const searchResult = search['search_result'];
- const resultsContent = document.getElementById('resultsContent');
- const resultsSubtitle = document.getElementById('resultsSubtitle');
- resultsSubtitle.textContent = `搜索词: ${searchWord}`;
- if (!searchResult) {
- resultsContent.innerHTML = `
- <div class="empty-results">
- <div class="empty-icon">❌</div>
- <div class="empty-text">该搜索词未执行搜索</div>
- </div>
- `;
- return;
- }
- const notes = searchResult.data?.data || [];
- if (notes.length === 0) {
- resultsContent.innerHTML = `
- <div class="empty-results">
- <div class="empty-icon">📭</div>
- <div class="empty-text">未找到匹配的帖子</div>
- </div>
- `;
- return;
- }
- // 渲染帖子卡片(简化版)
- let html = '<div class="notes-grid">';
- notes.forEach((note, idx) => {
- const card = note.note_card || {};
- const title = card.display_title || '无标题';
- const image = (card.image_list || [])[0] || '';
- html += `
- <div class="note-card-simple">
- ${image ? `<img src="${image}" alt="${title}" loading="lazy">` : ''}
- <div class="note-title-simple">${title}</div>
- </div>
- `;
- });
- html += '</div>';
- resultsContent.innerHTML = html;
- }
- '''
- def main():
- """主函数"""
- print("=" * 60)
- print("级联搜索结果可视化工具")
- print("=" * 60)
- # 加载数据
- data = load_all_data()
- # 提取全局候选词
- global_candidates = extract_global_candidates(data)
- # 生成HTML
- html_content = generate_html(data, global_candidates)
- # 保存HTML文件
- output_path = "visualization/cascade_search_results.html"
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
- with open(output_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
- print(f"\n✓ HTML文件已保存: {output_path}")
- # 打开HTML文件
- abs_path = os.path.abspath(output_path)
- print(f"正在打开浏览器...")
- webbrowser.open(f'file://{abs_path}')
- print("\n" + "=" * 60)
- print("✅ 可视化生成完成!")
- print("=" * 60)
- if __name__ == '__main__':
- main()
|