visualize_stage6_results.py 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Stage6评估结果可视化工具
  5. 整合两层评估结果的交互式HTML页面
  6. """
  7. import json
  8. import os
  9. from datetime import datetime
  10. from typing import List, Dict, Any
  11. def load_data(json_path: str) -> List[Dict[str, Any]]:
  12. """加载JSON数据"""
  13. with open(json_path, 'r', encoding='utf-8') as f:
  14. return json.load(f)
  15. def calculate_statistics(data: List[Dict[str, Any]]) -> Dict[str, Any]:
  16. """计算统计数据(包括评估结果)"""
  17. total_features = len(data)
  18. total_search_words = 0
  19. searched_count = 0 # 已执行搜索的数量
  20. not_searched_count = 0 # 未执行搜索的数量
  21. total_notes = 0
  22. video_count = 0
  23. normal_count = 0
  24. # 评估统计
  25. total_evaluated_notes = 0
  26. total_filtered = 0
  27. match_complete = 0 # 0.8-1.0分
  28. match_similar = 0 # 0.6-0.79分
  29. match_weak = 0 # 0.5-0.59分
  30. match_none = 0 # ≤0.4分
  31. for feature in data:
  32. grouped_results = feature.get('组合评估结果_分组', [])
  33. for group in grouped_results:
  34. search_items = group.get('top10_searches', [])
  35. total_search_words += len(search_items)
  36. for search_item in search_items:
  37. search_result = search_item.get('search_result', {})
  38. # 统计搜索状态
  39. if search_result:
  40. searched_count += 1
  41. notes = search_result.get('data', {}).get('data', [])
  42. total_notes += len(notes)
  43. # 统计视频/图文类型
  44. for note in notes:
  45. note_type = note.get('note_card', {}).get('type', '')
  46. if note_type == 'video':
  47. video_count += 1
  48. else:
  49. normal_count += 1
  50. # 统计评估结果
  51. evaluation = search_item.get('evaluation_with_filter')
  52. if evaluation:
  53. total_evaluated_notes += evaluation.get('total_notes', 0)
  54. total_filtered += evaluation.get('filtered_count', 0)
  55. stats = evaluation.get('statistics', {})
  56. match_complete += stats.get('完全匹配(0.8-1.0)', 0)
  57. match_similar += stats.get('相似匹配(0.6-0.79)', 0)
  58. match_weak += stats.get('弱相似(0.5-0.59)', 0)
  59. match_none += stats.get('无匹配(≤0.4)', 0)
  60. else:
  61. not_searched_count += 1
  62. # 计算百分比
  63. total_remaining = total_evaluated_notes - total_filtered if total_evaluated_notes > 0 else 0
  64. return {
  65. 'total_features': total_features,
  66. 'total_search_words': total_search_words,
  67. 'searched_count': searched_count,
  68. 'not_searched_count': not_searched_count,
  69. 'searched_percentage': round(searched_count / total_search_words * 100, 1) if total_search_words > 0 else 0,
  70. 'total_notes': total_notes,
  71. 'video_count': video_count,
  72. 'normal_count': normal_count,
  73. 'video_percentage': round(video_count / total_notes * 100, 1) if total_notes > 0 else 0,
  74. 'normal_percentage': round(normal_count / total_notes * 100, 1) if total_notes > 0 else 0,
  75. # 评估统计
  76. 'total_evaluated': total_evaluated_notes,
  77. 'total_filtered': total_filtered,
  78. 'total_remaining': total_remaining,
  79. 'filter_rate': round(total_filtered / total_evaluated_notes * 100, 1) if total_evaluated_notes > 0 else 0,
  80. 'match_complete': match_complete,
  81. 'match_similar': match_similar,
  82. 'match_weak': match_weak,
  83. 'match_none': match_none,
  84. 'complete_rate': round(match_complete / total_remaining * 100, 1) if total_remaining > 0 else 0,
  85. 'similar_rate': round(match_similar / total_remaining * 100, 1) if total_remaining > 0 else 0,
  86. }
  87. def generate_html(data: List[Dict[str, Any]], stats: Dict[str, Any], output_path: str):
  88. """生成HTML可视化页面"""
  89. # 准备数据JSON(用于JavaScript)
  90. data_json = json.dumps(data, ensure_ascii=False, indent=2)
  91. html_content = f'''<!DOCTYPE html>
  92. <html lang="zh-CN">
  93. <head>
  94. <meta charset="UTF-8">
  95. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  96. <title>Stage6 评估结果可视化</title>
  97. <style>
  98. * {{
  99. margin: 0;
  100. padding: 0;
  101. box-sizing: border-box;
  102. }}
  103. body {{
  104. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  105. background: #f5f7fa;
  106. color: #333;
  107. overflow-x: hidden;
  108. }}
  109. /* 顶部统计面板 */
  110. .stats-panel {{
  111. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  112. color: white;
  113. padding: 20px;
  114. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  115. }}
  116. .stats-container {{
  117. max-width: 1400px;
  118. margin: 0 auto;
  119. }}
  120. .stats-row {{
  121. display: flex;
  122. justify-content: space-around;
  123. align-items: center;
  124. flex-wrap: wrap;
  125. gap: 15px;
  126. margin-bottom: 15px;
  127. }}
  128. .stats-row:last-child {{
  129. margin-bottom: 0;
  130. padding-top: 15px;
  131. border-top: 1px solid rgba(255,255,255,0.2);
  132. }}
  133. .stat-item {{
  134. text-align: center;
  135. }}
  136. .stat-value {{
  137. font-size: 28px;
  138. font-weight: bold;
  139. margin-bottom: 5px;
  140. }}
  141. .stat-label {{
  142. font-size: 12px;
  143. opacity: 0.9;
  144. }}
  145. .stat-item.small .stat-value {{
  146. font-size: 22px;
  147. }}
  148. /* 过滤控制面板 */
  149. .filter-panel {{
  150. background: white;
  151. max-width: 1400px;
  152. margin: 20px auto;
  153. padding: 15px 20px;
  154. border-radius: 8px;
  155. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  156. display: flex;
  157. align-items: center;
  158. gap: 20px;
  159. flex-wrap: wrap;
  160. }}
  161. .filter-label {{
  162. font-weight: 600;
  163. color: #374151;
  164. }}
  165. .filter-buttons {{
  166. display: flex;
  167. gap: 10px;
  168. flex-wrap: wrap;
  169. }}
  170. .filter-btn {{
  171. padding: 6px 12px;
  172. border: 2px solid #e5e7eb;
  173. background: white;
  174. border-radius: 6px;
  175. cursor: pointer;
  176. font-size: 13px;
  177. font-weight: 500;
  178. transition: all 0.2s;
  179. }}
  180. .filter-btn:hover {{
  181. border-color: #667eea;
  182. background: #f9fafb;
  183. }}
  184. .filter-btn.active {{
  185. border-color: #667eea;
  186. background: #667eea;
  187. color: white;
  188. }}
  189. .filter-btn.complete {{
  190. border-color: #10b981;
  191. }}
  192. .filter-btn.complete.active {{
  193. background: #10b981;
  194. border-color: #10b981;
  195. }}
  196. .filter-btn.similar {{
  197. border-color: #f59e0b;
  198. }}
  199. .filter-btn.similar.active {{
  200. background: #f59e0b;
  201. border-color: #f59e0b;
  202. }}
  203. .filter-btn.weak {{
  204. border-color: #f97316;
  205. }}
  206. .filter-btn.weak.active {{
  207. background: #f97316;
  208. border-color: #f97316;
  209. }}
  210. .filter-btn.none {{
  211. border-color: #ef4444;
  212. }}
  213. .filter-btn.none.active {{
  214. background: #ef4444;
  215. border-color: #ef4444;
  216. }}
  217. .filter-btn.filtered {{
  218. border-color: #6b7280;
  219. }}
  220. .filter-btn.filtered.active {{
  221. background: #6b7280;
  222. border-color: #6b7280;
  223. }}
  224. /* 主容器 */
  225. .main-container {{
  226. display: flex;
  227. max-width: 1400px;
  228. margin: 0 auto 20px;
  229. gap: 20px;
  230. padding: 0 20px;
  231. height: calc(100vh - 260px);
  232. }}
  233. /* 左侧导航 */
  234. .left-sidebar {{
  235. width: 30%;
  236. background: white;
  237. border-radius: 8px;
  238. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  239. overflow-y: auto;
  240. position: sticky;
  241. top: 20px;
  242. height: fit-content;
  243. max-height: calc(100vh - 280px);
  244. }}
  245. .feature-group {{
  246. border-bottom: 1px solid #e5e7eb;
  247. }}
  248. .feature-header {{
  249. padding: 15px 20px;
  250. background: #f9fafb;
  251. cursor: pointer;
  252. user-select: none;
  253. transition: background 0.2s;
  254. }}
  255. .feature-header:hover {{
  256. background: #f3f4f6;
  257. }}
  258. .feature-header.active {{
  259. background: #667eea;
  260. color: white;
  261. }}
  262. .feature-title {{
  263. font-size: 16px;
  264. font-weight: 600;
  265. margin-bottom: 5px;
  266. }}
  267. .feature-meta {{
  268. font-size: 12px;
  269. color: #6b7280;
  270. }}
  271. .feature-header.active .feature-meta {{
  272. color: rgba(255,255,255,0.8);
  273. }}
  274. .search-words-list {{
  275. display: none;
  276. padding: 0;
  277. }}
  278. .search-words-list.expanded {{
  279. display: block;
  280. }}
  281. /* Base word分组层 */
  282. .base-word-group {{
  283. border-bottom: 1px solid #f3f4f6;
  284. }}
  285. .base-word-header {{
  286. padding: 12px 20px 12px 30px;
  287. background: #fafbfc;
  288. cursor: pointer;
  289. user-select: none;
  290. transition: all 0.2s;
  291. border-left: 3px solid transparent;
  292. }}
  293. .base-word-header:hover {{
  294. background: #f3f4f6;
  295. border-left-color: #a78bfa;
  296. }}
  297. .base-word-header.active {{
  298. background: #f3f4f6;
  299. border-left-color: #7c3aed;
  300. }}
  301. .base-word-title {{
  302. font-size: 15px;
  303. font-weight: 600;
  304. color: #7c3aed;
  305. margin-bottom: 4px;
  306. }}
  307. .base-word-meta {{
  308. font-size: 11px;
  309. color: #6b7280;
  310. }}
  311. .base-word-desc {{
  312. padding: 8px 20px 8px 30px;
  313. background: #fefce8;
  314. font-size: 12px;
  315. color: #854d0e;
  316. line-height: 1.5;
  317. border-left: 3px solid #fbbf24;
  318. display: none;
  319. }}
  320. .base-word-desc.expanded {{
  321. display: block;
  322. }}
  323. .search-words-sublist {{
  324. display: none;
  325. }}
  326. .search-words-sublist.expanded {{
  327. display: block;
  328. }}
  329. .search-word-item {{
  330. padding: 12px 20px 12px 50px;
  331. cursor: pointer;
  332. border-left: 3px solid transparent;
  333. transition: all 0.2s;
  334. }}
  335. .search-word-item:hover {{
  336. background: #f9fafb;
  337. border-left-color: #667eea;
  338. }}
  339. .search-word-item.active {{
  340. background: #ede9fe;
  341. border-left-color: #7c3aed;
  342. }}
  343. .search-word-text {{
  344. font-size: 14px;
  345. font-weight: 500;
  346. color: #374151;
  347. margin-bottom: 4px;
  348. }}
  349. .search-word-score {{
  350. display: inline-block;
  351. padding: 2px 8px;
  352. border-radius: 12px;
  353. font-size: 11px;
  354. font-weight: 600;
  355. margin-left: 8px;
  356. }}
  357. .score-high {{
  358. background: #d1fae5;
  359. color: #065f46;
  360. }}
  361. .score-medium {{
  362. background: #fef3c7;
  363. color: #92400e;
  364. }}
  365. .score-low {{
  366. background: #fee2e2;
  367. color: #991b1b;
  368. }}
  369. /* 评估徽章 */
  370. .eval-badge {{
  371. display: inline-block;
  372. padding: 2px 6px;
  373. border-radius: 10px;
  374. font-size: 11px;
  375. font-weight: 600;
  376. margin-left: 6px;
  377. }}
  378. .eval-complete {{
  379. background: #d1fae5;
  380. color: #065f46;
  381. border: 1px solid #10b981;
  382. }}
  383. .eval-similar {{
  384. background: #fef3c7;
  385. color: #92400e;
  386. border: 1px solid #f59e0b;
  387. }}
  388. .eval-weak {{
  389. background: #fed7aa;
  390. color: #9a3412;
  391. border: 1px solid #f97316;
  392. }}
  393. .eval-none {{
  394. background: #fee2e2;
  395. color: #991b1b;
  396. border: 1px solid #ef4444;
  397. }}
  398. .eval-filtered {{
  399. background: #e5e7eb;
  400. color: #4b5563;
  401. border: 1px solid #6b7280;
  402. }}
  403. .search-word-eval {{
  404. font-size: 11px;
  405. color: #6b7280;
  406. margin-top: 4px;
  407. }}
  408. /* 右侧结果区 */
  409. .right-content {{
  410. flex: 1;
  411. overflow-y: auto;
  412. padding-bottom: 40px;
  413. }}
  414. .result-block {{
  415. background: white;
  416. border-radius: 8px;
  417. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  418. margin-bottom: 30px;
  419. padding: 20px;
  420. scroll-margin-top: 20px;
  421. }}
  422. .result-header {{
  423. margin-bottom: 20px;
  424. padding-bottom: 15px;
  425. border-bottom: 2px solid #e5e7eb;
  426. }}
  427. .result-title {{
  428. font-size: 20px;
  429. font-weight: 600;
  430. color: #111827;
  431. margin-bottom: 10px;
  432. }}
  433. .result-stats {{
  434. display: flex;
  435. gap: 10px;
  436. font-size: 12px;
  437. color: #6b7280;
  438. flex-wrap: wrap;
  439. }}
  440. .stat-badge {{
  441. background: #f3f4f6;
  442. padding: 4px 10px;
  443. border-radius: 4px;
  444. }}
  445. .stat-badge.eval {{
  446. font-weight: 600;
  447. }}
  448. .stat-badge.eval.complete {{
  449. background: #d1fae5;
  450. color: #065f46;
  451. }}
  452. .stat-badge.eval.similar {{
  453. background: #fef3c7;
  454. color: #92400e;
  455. }}
  456. .stat-badge.eval.weak {{
  457. background: #fed7aa;
  458. color: #9a3412;
  459. }}
  460. .stat-badge.eval.none {{
  461. background: #fee2e2;
  462. color: #991b1b;
  463. }}
  464. .stat-badge.eval.filtered {{
  465. background: #e5e7eb;
  466. color: #4b5563;
  467. }}
  468. /* 帖子网格 */
  469. .notes-grid {{
  470. display: grid;
  471. grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  472. gap: 20px;
  473. }}
  474. /* 空状态样式 */
  475. .empty-state {{
  476. text-align: center;
  477. padding: 60px 40px;
  478. color: #6b7280;
  479. }}
  480. .empty-icon {{
  481. font-size: 48px;
  482. margin-bottom: 16px;
  483. }}
  484. .empty-title {{
  485. font-size: 16px;
  486. font-weight: 600;
  487. color: #374151;
  488. margin-bottom: 8px;
  489. }}
  490. .empty-desc {{
  491. font-size: 14px;
  492. line-height: 1.6;
  493. color: #9ca3af;
  494. max-width: 400px;
  495. margin: 0 auto;
  496. }}
  497. .note-card {{
  498. border: 3px solid #e5e7eb;
  499. border-radius: 8px;
  500. overflow: hidden;
  501. cursor: pointer;
  502. transition: all 0.3s;
  503. background: white;
  504. }}
  505. .note-card:hover {{
  506. transform: translateY(-4px);
  507. box-shadow: 0 10px 25px rgba(0,0,0,0.15);
  508. }}
  509. /* 根据评估分数设置边框颜色 */
  510. .note-card.eval-complete {{
  511. border-color: #10b981;
  512. }}
  513. .note-card.eval-similar {{
  514. border-color: #f59e0b;
  515. }}
  516. .note-card.eval-weak {{
  517. border-color: #f97316;
  518. }}
  519. .note-card.eval-none {{
  520. border-color: #ef4444;
  521. }}
  522. .note-card.eval-filtered {{
  523. border-color: #6b7280;
  524. opacity: 0.6;
  525. }}
  526. /* 图片轮播 */
  527. .image-carousel {{
  528. position: relative;
  529. width: 100%;
  530. height: 280px;
  531. background: #f3f4f6;
  532. overflow: hidden;
  533. }}
  534. .carousel-images {{
  535. display: flex;
  536. height: 100%;
  537. transition: transform 0.3s ease;
  538. }}
  539. .carousel-image {{
  540. min-width: 100%;
  541. height: 100%;
  542. object-fit: cover;
  543. }}
  544. .carousel-btn {{
  545. position: absolute;
  546. top: 50%;
  547. transform: translateY(-50%);
  548. background: rgba(0,0,0,0.5);
  549. color: white;
  550. border: none;
  551. width: 32px;
  552. height: 32px;
  553. border-radius: 50%;
  554. cursor: pointer;
  555. font-size: 16px;
  556. display: none;
  557. align-items: center;
  558. justify-content: center;
  559. transition: background 0.2s;
  560. z-index: 10;
  561. }}
  562. .carousel-btn:hover {{
  563. background: rgba(0,0,0,0.7);
  564. }}
  565. .carousel-btn.prev {{
  566. left: 8px;
  567. }}
  568. .carousel-btn.next {{
  569. right: 8px;
  570. }}
  571. .note-card:hover .carousel-btn {{
  572. display: flex;
  573. }}
  574. .carousel-indicators {{
  575. position: absolute;
  576. bottom: 10px;
  577. left: 50%;
  578. transform: translateX(-50%);
  579. display: flex;
  580. gap: 6px;
  581. z-index: 10;
  582. }}
  583. .dot {{
  584. width: 8px;
  585. height: 8px;
  586. border-radius: 50%;
  587. background: rgba(255,255,255,0.5);
  588. cursor: pointer;
  589. transition: all 0.2s;
  590. }}
  591. .dot.active {{
  592. background: white;
  593. width: 24px;
  594. border-radius: 4px;
  595. }}
  596. .image-counter {{
  597. position: absolute;
  598. top: 10px;
  599. right: 10px;
  600. background: rgba(0,0,0,0.6);
  601. color: white;
  602. padding: 4px 8px;
  603. border-radius: 4px;
  604. font-size: 12px;
  605. z-index: 10;
  606. }}
  607. /* 帖子信息 */
  608. .note-info {{
  609. padding: 12px;
  610. }}
  611. .note-title {{
  612. font-size: 14px;
  613. font-weight: 500;
  614. color: #111827;
  615. margin-bottom: 8px;
  616. display: -webkit-box;
  617. -webkit-line-clamp: 2;
  618. -webkit-box-orient: vertical;
  619. overflow: hidden;
  620. line-height: 1.4;
  621. }}
  622. .note-meta {{
  623. display: flex;
  624. align-items: center;
  625. justify-content: space-between;
  626. font-size: 12px;
  627. color: #6b7280;
  628. margin-bottom: 8px;
  629. }}
  630. .note-type {{
  631. padding: 3px 8px;
  632. border-radius: 4px;
  633. font-weight: 500;
  634. }}
  635. .type-video {{
  636. background: #dbeafe;
  637. color: #1e40af;
  638. }}
  639. .type-normal {{
  640. background: #d1fae5;
  641. color: #065f46;
  642. }}
  643. .note-author {{
  644. display: flex;
  645. align-items: center;
  646. gap: 6px;
  647. }}
  648. .author-avatar {{
  649. width: 24px;
  650. height: 24px;
  651. border-radius: 50%;
  652. }}
  653. /* 评估信息 */
  654. .note-eval {{
  655. padding: 8px 12px;
  656. background: #f9fafb;
  657. border-top: 1px solid #e5e7eb;
  658. font-size: 12px;
  659. }}
  660. .note-eval-header {{
  661. display: flex;
  662. align-items: center;
  663. justify-content: space-between;
  664. cursor: pointer;
  665. user-select: none;
  666. }}
  667. .note-eval-score {{
  668. font-weight: 600;
  669. }}
  670. .note-eval-toggle {{
  671. color: #6b7280;
  672. font-size: 10px;
  673. }}
  674. .note-eval-details {{
  675. margin-top: 8px;
  676. padding-top: 8px;
  677. border-top: 1px solid #e5e7eb;
  678. display: none;
  679. line-height: 1.5;
  680. }}
  681. .note-eval-details.expanded {{
  682. display: block;
  683. }}
  684. .eval-detail-label {{
  685. font-weight: 600;
  686. color: #374151;
  687. margin-top: 6px;
  688. margin-bottom: 2px;
  689. }}
  690. .eval-detail-label:first-child {{
  691. margin-top: 0;
  692. }}
  693. .eval-detail-text {{
  694. color: #6b7280;
  695. }}
  696. /* 滚动条样式 */
  697. ::-webkit-scrollbar {{
  698. width: 8px;
  699. height: 8px;
  700. }}
  701. ::-webkit-scrollbar-track {{
  702. background: #f1f1f1;
  703. }}
  704. ::-webkit-scrollbar-thumb {{
  705. background: #888;
  706. border-radius: 4px;
  707. }}
  708. ::-webkit-scrollbar-thumb:hover {{
  709. background: #555;
  710. }}
  711. /* 隐藏类 */
  712. .hidden {{
  713. display: none !important;
  714. }}
  715. </style>
  716. </head>
  717. <body>
  718. <!-- 统计面板 -->
  719. <div class="stats-panel">
  720. <div class="stats-container">
  721. <div class="stats-row">
  722. <div class="stat-item">
  723. <div class="stat-value">📊 {stats['total_features']}</div>
  724. <div class="stat-label">原始特征数</div>
  725. </div>
  726. <div class="stat-item">
  727. <div class="stat-value">🔍 {stats['total_search_words']}</div>
  728. <div class="stat-label">搜索词总数</div>
  729. </div>
  730. <div class="stat-item">
  731. <div class="stat-value">✅ {stats['searched_count']}</div>
  732. <div class="stat-label">已搜索 ({stats['searched_percentage']}%)</div>
  733. </div>
  734. <div class="stat-item">
  735. <div class="stat-value">⏸️ {stats['not_searched_count']}</div>
  736. <div class="stat-label">未搜索</div>
  737. </div>
  738. <div class="stat-item">
  739. <div class="stat-value">📝 {stats['total_notes']}</div>
  740. <div class="stat-label">帖子总数</div>
  741. </div>
  742. <div class="stat-item">
  743. <div class="stat-value">🎬 {stats['video_count']}</div>
  744. <div class="stat-label">视频 ({stats['video_percentage']}%)</div>
  745. </div>
  746. <div class="stat-item">
  747. <div class="stat-value">📷 {stats['normal_count']}</div>
  748. <div class="stat-label">图文 ({stats['normal_percentage']}%)</div>
  749. </div>
  750. </div>
  751. <div class="stats-row">
  752. <div class="stat-item small">
  753. <div class="stat-value">⚡ {stats['total_evaluated']}</div>
  754. <div class="stat-label">已评估</div>
  755. </div>
  756. <div class="stat-item small">
  757. <div class="stat-value">⚫ {stats['total_filtered']}</div>
  758. <div class="stat-label">已过滤 ({stats['filter_rate']}%)</div>
  759. </div>
  760. <div class="stat-item small">
  761. <div class="stat-value">🟢 {stats['match_complete']}</div>
  762. <div class="stat-label">完全匹配 ({stats['complete_rate']}%)</div>
  763. </div>
  764. <div class="stat-item small">
  765. <div class="stat-value">🟡 {stats['match_similar']}</div>
  766. <div class="stat-label">相似匹配 ({stats['similar_rate']}%)</div>
  767. </div>
  768. <div class="stat-item small">
  769. <div class="stat-value">🟠 {stats['match_weak']}</div>
  770. <div class="stat-label">弱相似</div>
  771. </div>
  772. <div class="stat-item small">
  773. <div class="stat-value">🔴 {stats['match_none']}</div>
  774. <div class="stat-label">无匹配</div>
  775. </div>
  776. </div>
  777. </div>
  778. </div>
  779. <!-- 过滤控制面板 -->
  780. <div class="filter-panel">
  781. <span class="filter-label">🔍 筛选显示:</span>
  782. <div class="filter-buttons">
  783. <button class="filter-btn active" onclick="filterNotes('all')">全部</button>
  784. <button class="filter-btn complete" onclick="filterNotes('complete')">🟢 完全匹配</button>
  785. <button class="filter-btn similar" onclick="filterNotes('similar')">🟡 相似匹配</button>
  786. <button class="filter-btn weak" onclick="filterNotes('weak')">🟠 弱相似</button>
  787. <button class="filter-btn none" onclick="filterNotes('none')">🔴 无匹配</button>
  788. <button class="filter-btn filtered" onclick="filterNotes('filtered')">⚫ 已过滤</button>
  789. </div>
  790. </div>
  791. <!-- 主容器 -->
  792. <div class="main-container">
  793. <!-- 左侧导航 -->
  794. <div class="left-sidebar" id="leftSidebar">
  795. <!-- 通过JavaScript动态生成 -->
  796. </div>
  797. <!-- 右侧结果区 -->
  798. <div class="right-content" id="rightContent">
  799. <!-- 通过JavaScript动态生成 -->
  800. </div>
  801. </div>
  802. <script>
  803. // 数据
  804. const data = {data_json};
  805. let currentFilter = 'all';
  806. // 创建评估映射(使用索引: "featureIdx-groupIdx-swIdx-noteIdx" -> evaluation)
  807. const noteEvaluations = {{}};
  808. data.forEach((feature, fIdx) => {{
  809. const groups = feature['组合评估结果_分组'] || [];
  810. groups.forEach((group, gIdx) => {{
  811. const searches = group['top10_searches'] || [];
  812. searches.forEach((search, sIdx) => {{
  813. const evaluation = search['evaluation_with_filter'];
  814. if (evaluation && evaluation.notes_evaluation) {{
  815. evaluation.notes_evaluation.forEach(noteEval => {{
  816. const key = `${{fIdx}}-${{gIdx}}-${{sIdx}}-${{noteEval.note_index}}`;
  817. noteEvaluations[key] = noteEval;
  818. }});
  819. }}
  820. }});
  821. }});
  822. }});
  823. // 获取评估类别
  824. function getEvalCategory(noteEval) {{
  825. if (!noteEval || noteEval['Query相关性'] !== '相关') {{
  826. return 'filtered';
  827. }}
  828. const score = noteEval['综合得分'];
  829. if (score >= 0.8) return 'complete';
  830. if (score >= 0.6) return 'similar';
  831. if (score >= 0.5) return 'weak';
  832. return 'none';
  833. }}
  834. // 渲染左侧导航
  835. function renderLeftSidebar() {{
  836. const sidebar = document.getElementById('leftSidebar');
  837. let html = '';
  838. data.forEach((feature, featureIdx) => {{
  839. const groups = feature['组合评估结果_分组'] || [];
  840. let totalSearches = 0;
  841. groups.forEach(group => {{
  842. totalSearches += (group['top10_searches'] || []).length;
  843. }});
  844. // 层级1: 原始特征
  845. html += `
  846. <div class="feature-group">
  847. <div class="feature-header" onclick="toggleFeature(${{featureIdx}})" id="feature-header-${{featureIdx}}">
  848. <div class="feature-title">${{feature['原始特征名称']}}</div>
  849. <div class="feature-meta">
  850. ${{feature['来源层级']}} · 权重: ${{feature['权重'].toFixed(2)}} · ${{totalSearches}}个搜索词
  851. </div>
  852. </div>
  853. <div class="search-words-list" id="search-words-${{featureIdx}}">
  854. `;
  855. // 层级2: Base word分组
  856. groups.forEach((group, groupIdx) => {{
  857. const baseWord = group['base_word'] || '';
  858. const baseSimilarity = group['base_word_similarity'] || 0;
  859. const searches = group['top10_searches'] || [];
  860. // 获取相关词汇
  861. const relatedWords = feature['高相似度候选_按base_word']?.[baseWord] || [];
  862. const relatedWordNames = relatedWords.map(w => w['人设特征名称']).slice(0, 10).join('、');
  863. html += `
  864. <div class="base-word-group">
  865. <div class="base-word-header" onclick="toggleBaseWord(${{featureIdx}}, ${{groupIdx}})"
  866. id="base-word-header-${{featureIdx}}-${{groupIdx}}">
  867. <div class="base-word-title">🎯 ${{baseWord}}</div>
  868. <div class="base-word-meta">相似度: ${{baseSimilarity.toFixed(2)}} · ${{searches.length}}个搜索词</div>
  869. </div>
  870. <div class="base-word-desc" id="base-word-desc-${{featureIdx}}-${{groupIdx}}">
  871. <strong>关联特征范围(可用词汇池):</strong>${{relatedWordNames || '无相关词汇'}}
  872. </div>
  873. <div class="search-words-sublist" id="search-words-sublist-${{featureIdx}}-${{groupIdx}}">
  874. `;
  875. // 层级3: 搜索词列表
  876. searches.forEach((sw, swIdx) => {{
  877. const score = sw.score || 0;
  878. const scoreClass = score >= 0.9 ? 'score-high' : score >= 0.7 ? 'score-medium' : 'score-low';
  879. const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
  880. const sourceWord = sw.source_word || '';
  881. // 获取评估统计
  882. const evaluation = sw['evaluation_with_filter'];
  883. let evalBadges = '';
  884. if (evaluation) {{
  885. const stats = evaluation.statistics || {{}};
  886. const complete = stats['完全匹配(0.8-1.0)'] || 0;
  887. const similar = stats['相似匹配(0.6-0.79)'] || 0;
  888. const weak = stats['弱相似(0.5-0.59)'] || 0;
  889. const none = stats['无匹配(≤0.4)'] || 0;
  890. const filtered = evaluation.filtered_count || 0;
  891. if (complete > 0) evalBadges += `<span class="eval-badge eval-complete">🟢${{complete}}</span>`;
  892. if (similar > 0) evalBadges += `<span class="eval-badge eval-similar">🟡${{similar}}</span>`;
  893. if (weak > 0) evalBadges += `<span class="eval-badge eval-weak">🟠${{weak}}</span>`;
  894. if (none > 0) evalBadges += `<span class="eval-badge eval-none">🔴${{none}}</span>`;
  895. if (filtered > 0) evalBadges += `<span class="eval-badge eval-filtered">⚫${{filtered}}</span>`;
  896. }}
  897. html += `
  898. <div class="search-word-item" onclick="scrollToBlock('${{blockId}}')"
  899. id="sw-${{featureIdx}}-${{groupIdx}}-${{swIdx}}"
  900. data-block-id="${{blockId}}">
  901. <div class="search-word-text">
  902. 🔍 ${{sw.search_word}}
  903. </div>
  904. <div class="search-word-meta" style="font-size:11px;color:#9ca3af;margin-top:2px">
  905. 来源: ${{sourceWord}}
  906. </div>
  907. <div class="search-word-eval">${{evalBadges}}</div>
  908. </div>
  909. `;
  910. }});
  911. html += `
  912. </div>
  913. </div>
  914. `;
  915. }});
  916. html += `
  917. </div>
  918. </div>
  919. `;
  920. }});
  921. sidebar.innerHTML = html;
  922. }}
  923. // 渲染右侧结果区
  924. function renderRightContent() {{
  925. const content = document.getElementById('rightContent');
  926. let html = '';
  927. data.forEach((feature, featureIdx) => {{
  928. const groups = feature['组合评估结果_分组'] || [];
  929. groups.forEach((group, groupIdx) => {{
  930. const searches = group['top10_searches'] || [];
  931. searches.forEach((sw, swIdx) => {{
  932. const blockId = `block-${{featureIdx}}-${{groupIdx}}-${{swIdx}}`;
  933. const hasSearchResult = sw.search_result != null;
  934. const searchResult = sw.search_result || {{}};
  935. const notes = searchResult.data?.data || [];
  936. const videoCount = notes.filter(n => n.note_card?.type === 'video').length;
  937. const normalCount = notes.length - videoCount;
  938. // 获取评估统计
  939. const evaluation = sw['evaluation_with_filter'];
  940. let evalStats = '';
  941. if (evaluation) {{
  942. const stats = evaluation.statistics || {{}};
  943. const complete = stats['完全匹配(0.8-1.0)'] || 0;
  944. const similar = stats['相似匹配(0.6-0.79)'] || 0;
  945. const weak = stats['弱相似(0.5-0.59)'] || 0;
  946. const none = stats['无匹配(≤0.4)'] || 0;
  947. const filtered = evaluation.filtered_count || 0;
  948. if (complete > 0) evalStats += `<span class="stat-badge eval complete">🟢 完全:${{complete}}</span>`;
  949. if (similar > 0) evalStats += `<span class="stat-badge eval similar">🟡 相似:${{similar}}</span>`;
  950. if (weak > 0) evalStats += `<span class="stat-badge eval weak">🟠 弱:${{weak}}</span>`;
  951. if (none > 0) evalStats += `<span class="stat-badge eval none">🔴 无:${{none}}</span>`;
  952. if (filtered > 0) evalStats += `<span class="stat-badge eval filtered">⚫ 过滤:${{filtered}}</span>`;
  953. }}
  954. // 构建结果块
  955. html += `
  956. <div class="result-block" id="${{blockId}}">
  957. <div class="result-header">
  958. <div class="result-title">${{sw.search_word}}</div>
  959. <div class="result-stats">
  960. `;
  961. // 根据搜索状态显示不同的统计信息
  962. if (!hasSearchResult) {{
  963. // 未执行搜索
  964. html += `
  965. <span class="stat-badge" style="background:#fef3c7;color:#92400e;font-weight:600">⏸️ 未执行搜索</span>
  966. `;
  967. }} else if (notes.length === 0) {{
  968. // 搜索完成但无结果
  969. html += `
  970. <span class="stat-badge">📝 0 条帖子</span>
  971. <span class="stat-badge" style="background:#fee2e2;color:#991b1b;font-weight:600">❌ 未找到匹配</span>
  972. `;
  973. }} else {{
  974. // 正常有结果
  975. html += `
  976. <span class="stat-badge">📝 ${{notes.length}} 条帖子</span>
  977. <span class="stat-badge">🎬 ${{videoCount}} 视频</span>
  978. <span class="stat-badge">📷 ${{normalCount}} 图文</span>
  979. ${{evalStats}}
  980. `;
  981. }}
  982. html += `
  983. </div>
  984. </div>
  985. `;
  986. // 根据搜索状态显示不同的内容区域
  987. if (!hasSearchResult) {{
  988. // 未执行搜索 - 显示空状态消息
  989. html += `
  990. <div class="empty-state">
  991. <div class="empty-icon">⏸️</div>
  992. <div class="empty-title">该搜索词未执行搜索</div>
  993. <div class="empty-desc">由于搜索次数限制(--max-searches-per-feature 和 --max-searches-per-base-word),该搜索词未被执行</div>
  994. </div>
  995. `;
  996. }} else if (notes.length === 0) {{
  997. // 搜索完成但无结果
  998. html += `
  999. <div class="empty-state">
  1000. <div class="empty-icon">❌</div>
  1001. <div class="empty-title">搜索完成,但未找到匹配的帖子</div>
  1002. <div class="empty-desc">该搜索词已执行,但小红书返回了 0 条结果</div>
  1003. </div>
  1004. `;
  1005. }} else {{
  1006. // 正常有结果 - 显示帖子网格
  1007. html += `
  1008. <div class="notes-grid">
  1009. ${{notes.map((note, noteIdx) => renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx)).join('')}}
  1010. </div>
  1011. `;
  1012. }}
  1013. html += `
  1014. </div>
  1015. `;
  1016. }});
  1017. }});
  1018. }});
  1019. content.innerHTML = html;
  1020. }}
  1021. // 渲染单个帖子卡片
  1022. function renderNoteCard(note, featureIdx, groupIdx, swIdx, noteIdx) {{
  1023. const card = note.note_card || {{}};
  1024. const images = card.image_list || [];
  1025. const title = card.display_title || '无标题';
  1026. const noteType = card.type || 'normal';
  1027. const noteId = note.id || '';
  1028. const user = card.user || {{}};
  1029. const userName = user.nick_name || '未知用户';
  1030. const userAvatar = user.avatar || '';
  1031. const carouselId = `carousel-${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
  1032. // 获取评估结果(使用索引key)
  1033. const evalKey = `${{featureIdx}}-${{groupIdx}}-${{swIdx}}-${{noteIdx}}`;
  1034. const noteEval = noteEvaluations[evalKey];
  1035. const evalCategory = getEvalCategory(noteEval);
  1036. const evalClass = `eval-${{evalCategory}}`;
  1037. let evalSection = '';
  1038. if (noteEval) {{
  1039. const score = noteEval['综合得分'];
  1040. const scoreEmoji = score >= 0.8 ? '🟢' : score >= 0.6 ? '🟡' : score >= 0.5 ? '🟠' : '🔴';
  1041. const scoreText = score >= 0.8 ? '完全匹配' : score >= 0.6 ? '相似匹配' : score >= 0.5 ? '弱相似' : '无匹配';
  1042. const reasoning = noteEval['评分说明'] || '无';
  1043. const matchingPoints = (noteEval['关键匹配点'] || []).join('、') || '无';
  1044. evalSection = `
  1045. <div class="note-eval">
  1046. <div class="note-eval-header" onclick="event.stopPropagation(); toggleEvalDetails('${{carouselId}}')">
  1047. <span class="note-eval-score">${{scoreEmoji}} ${{scoreText}} (${{score}}分)</span>
  1048. <span class="note-eval-toggle" id="${{carouselId}}-toggle">▼ 详情</span>
  1049. </div>
  1050. <div class="note-eval-details" id="${{carouselId}}-details">
  1051. <div class="eval-detail-label">评估理由:</div>
  1052. <div class="eval-detail-text">${{reasoning}}</div>
  1053. <div class="eval-detail-label">匹配要点:</div>
  1054. <div class="eval-detail-text">${{matchingPoints}}</div>
  1055. </div>
  1056. </div>
  1057. `;
  1058. }} else if (evalCategory === 'filtered') {{
  1059. evalSection = `
  1060. <div class="note-eval">
  1061. <div class="note-eval-score">⚫ 已过滤(与搜索无关)</div>
  1062. </div>
  1063. `;
  1064. }}
  1065. return `
  1066. <div class="note-card ${{evalClass}}" data-eval-category="${{evalCategory}}" onclick="openNote('${{noteId}}')">
  1067. <div class="image-carousel" id="${{carouselId}}">
  1068. <div class="carousel-images">
  1069. ${{images.map(img => `<img class="carousel-image" src="${{img}}" alt="帖子图片" loading="lazy">`).join('')}}
  1070. </div>
  1071. ${{images.length > 1 ? `
  1072. <button class="carousel-btn prev" onclick="event.stopPropagation(); changeImage('${{carouselId}}', -1)">←</button>
  1073. <button class="carousel-btn next" onclick="event.stopPropagation(); changeImage('${{carouselId}}', 1)">→</button>
  1074. <div class="carousel-indicators">
  1075. ${{images.map((_, i) => `<span class="dot ${{i === 0 ? 'active' : ''}}" onclick="event.stopPropagation(); goToImage('${{carouselId}}', ${{i}})"></span>`).join('')}}
  1076. </div>
  1077. <span class="image-counter">1/${{images.length}}</span>
  1078. ` : ''}}
  1079. </div>
  1080. <div class="note-info">
  1081. <div class="note-title">${{title}}</div>
  1082. <div class="note-meta">
  1083. <span class="note-type type-${{noteType}}">
  1084. ${{noteType === 'video' ? '🎬 视频' : '📷 图文'}}
  1085. </span>
  1086. <div class="note-author">
  1087. ${{userAvatar ? `<img class="author-avatar" src="${{userAvatar}}" alt="${{userName}}">` : ''}}
  1088. <span>${{userName}}</span>
  1089. </div>
  1090. </div>
  1091. </div>
  1092. ${{evalSection}}
  1093. </div>
  1094. `;
  1095. }}
  1096. // 图片轮播逻辑
  1097. const carouselStates = {{}};
  1098. function changeImage(carouselId, direction) {{
  1099. if (!carouselStates[carouselId]) {{
  1100. carouselStates[carouselId] = {{ currentIndex: 0 }};
  1101. }}
  1102. const carousel = document.getElementById(carouselId);
  1103. const imagesContainer = carousel.querySelector('.carousel-images');
  1104. const images = carousel.querySelectorAll('.carousel-image');
  1105. const dots = carousel.querySelectorAll('.dot');
  1106. const counter = carousel.querySelector('.image-counter');
  1107. let newIndex = carouselStates[carouselId].currentIndex + direction;
  1108. if (newIndex < 0) newIndex = images.length - 1;
  1109. if (newIndex >= images.length) newIndex = 0;
  1110. carouselStates[carouselId].currentIndex = newIndex;
  1111. imagesContainer.style.transform = `translateX(-${{newIndex * 100}}%)`;
  1112. // 更新指示器
  1113. dots.forEach((dot, i) => {{
  1114. dot.classList.toggle('active', i === newIndex);
  1115. }});
  1116. // 更新计数器
  1117. if (counter) {{
  1118. counter.textContent = `${{newIndex + 1}}/${{images.length}}`;
  1119. }}
  1120. }}
  1121. function goToImage(carouselId, index) {{
  1122. if (!carouselStates[carouselId]) {{
  1123. carouselStates[carouselId] = {{ currentIndex: 0 }};
  1124. }}
  1125. const carousel = document.getElementById(carouselId);
  1126. const imagesContainer = carousel.querySelector('.carousel-images');
  1127. const dots = carousel.querySelectorAll('.dot');
  1128. const counter = carousel.querySelector('.image-counter');
  1129. carouselStates[carouselId].currentIndex = index;
  1130. imagesContainer.style.transform = `translateX(-${{index * 100}}%)`;
  1131. // 更新指示器
  1132. dots.forEach((dot, i) => {{
  1133. dot.classList.toggle('active', i === index);
  1134. }});
  1135. // 更新计数器
  1136. if (counter) {{
  1137. counter.textContent = `${{index + 1}}/${{dots.length}}`;
  1138. }}
  1139. }}
  1140. // 展开/折叠特征组
  1141. function toggleFeature(featureIdx) {{
  1142. const searchWordsList = document.getElementById(`search-words-${{featureIdx}}`);
  1143. const featureHeader = document.getElementById(`feature-header-${{featureIdx}}`);
  1144. searchWordsList.classList.toggle('expanded');
  1145. featureHeader.classList.toggle('active');
  1146. }}
  1147. // 展开/折叠base word分组
  1148. function toggleBaseWord(featureIdx, groupIdx) {{
  1149. const baseWordHeader = document.getElementById(`base-word-header-${{featureIdx}}-${{groupIdx}}`);
  1150. const baseWordDesc = document.getElementById(`base-word-desc-${{featureIdx}}-${{groupIdx}}`);
  1151. const searchWordsSublist = document.getElementById(`search-words-sublist-${{featureIdx}}-${{groupIdx}}`);
  1152. baseWordHeader.classList.toggle('active');
  1153. baseWordDesc.classList.toggle('expanded');
  1154. searchWordsSublist.classList.toggle('expanded');
  1155. }}
  1156. // 滚动到指定结果块
  1157. function scrollToBlock(blockId) {{
  1158. const block = document.getElementById(blockId);
  1159. if (block) {{
  1160. block.scrollIntoView({{ behavior: 'smooth', block: 'start' }});
  1161. // 高亮对应的搜索词
  1162. document.querySelectorAll('.search-word-item').forEach(item => {{
  1163. item.classList.remove('active');
  1164. }});
  1165. document.querySelectorAll(`[data-block-id="${{blockId}}"]`).forEach(item => {{
  1166. item.classList.add('active');
  1167. }});
  1168. }}
  1169. }}
  1170. // 切换评估详情
  1171. function toggleEvalDetails(carouselId) {{
  1172. const details = document.getElementById(`${{carouselId}}-details`);
  1173. const toggle = document.getElementById(`${{carouselId}}-toggle`);
  1174. if (details && toggle) {{
  1175. details.classList.toggle('expanded');
  1176. toggle.textContent = details.classList.contains('expanded') ? '▲ 收起' : '▼ 详情';
  1177. }}
  1178. }}
  1179. // 过滤帖子
  1180. function filterNotes(category) {{
  1181. currentFilter = category;
  1182. // 更新按钮状态
  1183. document.querySelectorAll('.filter-btn').forEach(btn => {{
  1184. btn.classList.remove('active');
  1185. }});
  1186. event.target.classList.add('active');
  1187. // 过滤帖子卡片
  1188. document.querySelectorAll('.note-card').forEach(card => {{
  1189. const evalCategory = card.getAttribute('data-eval-category');
  1190. if (category === 'all' || evalCategory === category) {{
  1191. card.classList.remove('hidden');
  1192. }} else {{
  1193. card.classList.add('hidden');
  1194. }}
  1195. }});
  1196. // 隐藏空的结果块
  1197. document.querySelectorAll('.result-block').forEach(block => {{
  1198. const visibleCards = block.querySelectorAll('.note-card:not(.hidden)');
  1199. if (visibleCards.length === 0) {{
  1200. block.classList.add('hidden');
  1201. }} else {{
  1202. block.classList.remove('hidden');
  1203. }}
  1204. }});
  1205. }}
  1206. // 打开小红书帖子
  1207. function openNote(noteId) {{
  1208. if (noteId) {{
  1209. window.open(`https://www.xiaohongshu.com/explore/${{noteId}}`, '_blank');
  1210. }}
  1211. }}
  1212. // 初始化
  1213. document.addEventListener('DOMContentLoaded', () => {{
  1214. renderLeftSidebar();
  1215. renderRightContent();
  1216. // 默认展开第一个特征组和第一个base_word
  1217. if (data.length > 0) {{
  1218. toggleFeature(0);
  1219. // 展开第一个base_word分组
  1220. const firstGroups = data[0]['组合评估结果_分组'];
  1221. if (firstGroups && firstGroups.length > 0) {{
  1222. toggleBaseWord(0, 0);
  1223. }}
  1224. }}
  1225. }});
  1226. </script>
  1227. </body>
  1228. </html>
  1229. '''
  1230. # 写入文件
  1231. with open(output_path, 'w', encoding='utf-8') as f:
  1232. f.write(html_content)
  1233. def main():
  1234. """主函数"""
  1235. # 配置路径
  1236. script_dir = os.path.dirname(os.path.abspath(__file__))
  1237. json_path = os.path.join(script_dir, 'output_v2', 'stage6_with_evaluations.json')
  1238. output_dir = os.path.join(script_dir, 'visualization')
  1239. os.makedirs(output_dir, exist_ok=True)
  1240. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  1241. output_path = os.path.join(output_dir, f'stage6_interactive_{timestamp}.html')
  1242. # 加载数据
  1243. print(f"📖 加载数据: {json_path}")
  1244. data = load_data(json_path)
  1245. print(f"✓ 加载了 {len(data)} 个原始特征")
  1246. # 计算统计
  1247. print("📊 计算统计数据...")
  1248. stats = calculate_statistics(data)
  1249. print(f"✓ 统计完成:")
  1250. print(f" - 原始特征: {stats['total_features']}")
  1251. print(f" - 搜索词总数: {stats['total_search_words']}")
  1252. print(f" - 已搜索: {stats['searched_count']} ({stats['searched_percentage']}%)")
  1253. print(f" - 未搜索: {stats['not_searched_count']}")
  1254. print(f" - 帖子总数: {stats['total_notes']}")
  1255. print(f" - 视频: {stats['video_count']} ({stats['video_percentage']}%)")
  1256. print(f" - 图文: {stats['normal_count']} ({stats['normal_percentage']}%)")
  1257. print(f"\n 评估结果:")
  1258. print(f" - 已评估: {stats['total_evaluated']}")
  1259. print(f" - 已过滤: {stats['total_filtered']} ({stats['filter_rate']}%)")
  1260. print(f" - 完全匹配: {stats['match_complete']} ({stats['complete_rate']}%)")
  1261. print(f" - 相似匹配: {stats['match_similar']} ({stats['similar_rate']}%)")
  1262. print(f" - 弱相似: {stats['match_weak']}")
  1263. print(f" - 无匹配: {stats['match_none']}")
  1264. # 生成HTML
  1265. print(f"\n🎨 生成可视化页面...")
  1266. generate_html(data, stats, output_path)
  1267. print(f"✓ 生成完成: {output_path}")
  1268. # 打印访问提示
  1269. print(f"\n🌐 在浏览器中打开查看:")
  1270. print(f" file://{output_path}")
  1271. return output_path
  1272. if __name__ == '__main__':
  1273. main()