visualize_steps.py 72 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164
  1. #!/usr/bin/env python3
  2. """
  3. Steps 可视化工具
  4. 将 steps.json 转换为 HTML 可视化页面
  5. """
  6. import json
  7. import argparse
  8. from pathlib import Path
  9. from datetime import datetime
  10. HTML_TEMPLATE = """<!DOCTYPE html>
  11. <html lang="zh-CN">
  12. <head>
  13. <meta charset="UTF-8">
  14. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  15. <title>Query Optimization Steps 可视化</title>
  16. <style>
  17. * {
  18. margin: 0;
  19. padding: 0;
  20. box-sizing: border-box;
  21. }
  22. body {
  23. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  24. background: #f5f5f5;
  25. color: #333;
  26. line-height: 1.6;
  27. display: flex;
  28. margin: 0;
  29. padding: 0;
  30. }
  31. /* 左侧导航 */
  32. .sidebar {
  33. width: 280px;
  34. background: white;
  35. height: 100vh;
  36. position: fixed;
  37. left: 0;
  38. top: 0;
  39. overflow-y: auto;
  40. box-shadow: 2px 0 8px rgba(0,0,0,0.1);
  41. z-index: 100;
  42. }
  43. .sidebar-header {
  44. padding: 20px;
  45. background: #2563eb;
  46. color: white;
  47. font-size: 18px;
  48. font-weight: 600;
  49. }
  50. .toc {
  51. padding: 10px 0;
  52. }
  53. .toc-item {
  54. padding: 10px 20px;
  55. cursor: pointer;
  56. transition: background 0.2s;
  57. border-left: 3px solid transparent;
  58. }
  59. .toc-item:hover {
  60. background: #f0f9ff;
  61. }
  62. .toc-item.active {
  63. background: #eff6ff;
  64. border-left-color: #2563eb;
  65. color: #2563eb;
  66. font-weight: 600;
  67. }
  68. .toc-item-level-0 {
  69. font-weight: 600;
  70. color: #1a1a1a;
  71. font-size: 14px;
  72. }
  73. .toc-item-level-1 {
  74. padding-left: 35px;
  75. font-size: 13px;
  76. color: #666;
  77. }
  78. .toc-item-level-2 {
  79. padding-left: 50px;
  80. font-size: 12px;
  81. color: #999;
  82. }
  83. .toc-toggle {
  84. display: inline-block;
  85. width: 16px;
  86. height: 16px;
  87. margin-left: 5px;
  88. cursor: pointer;
  89. transition: transform 0.2s;
  90. float: right;
  91. }
  92. .toc-toggle.collapsed {
  93. transform: rotate(-90deg);
  94. }
  95. .toc-children {
  96. display: none;
  97. }
  98. .toc-children.expanded {
  99. display: block;
  100. }
  101. .container {
  102. margin-left: 280px;
  103. width: calc(100% - 280px);
  104. padding: 20px;
  105. }
  106. .header {
  107. background: white;
  108. padding: 30px;
  109. border-radius: 12px;
  110. margin-bottom: 30px;
  111. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  112. }
  113. .header h1 {
  114. font-size: 32px;
  115. margin-bottom: 20px;
  116. color: #1a1a1a;
  117. }
  118. .question-box {
  119. background: #f0f9ff;
  120. padding: 20px;
  121. border-radius: 8px;
  122. margin-bottom: 20px;
  123. border-left: 4px solid #0284c7;
  124. }
  125. .question-label {
  126. font-size: 14px;
  127. color: #0369a1;
  128. margin-bottom: 8px;
  129. font-weight: 600;
  130. }
  131. .question-text {
  132. font-size: 18px;
  133. color: #1a1a1a;
  134. line-height: 1.6;
  135. }
  136. .overview {
  137. display: flex;
  138. gap: 30px;
  139. flex-wrap: wrap;
  140. }
  141. .overview-item {
  142. flex: 1;
  143. min-width: 150px;
  144. }
  145. .overview-label {
  146. font-size: 14px;
  147. color: #666;
  148. margin-bottom: 5px;
  149. }
  150. .overview-value {
  151. font-size: 28px;
  152. font-weight: bold;
  153. color: #2563eb;
  154. }
  155. .step-section {
  156. background: white;
  157. padding: 30px;
  158. border-radius: 12px;
  159. margin-bottom: 30px;
  160. box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  161. }
  162. .step-header {
  163. border-bottom: 3px solid #2563eb;
  164. padding-bottom: 15px;
  165. margin-bottom: 20px;
  166. display: flex;
  167. justify-content: space-between;
  168. align-items: center;
  169. }
  170. .step-title {
  171. font-size: 26px;
  172. color: #1a1a1a;
  173. }
  174. .step-type {
  175. background: #e0e7ff;
  176. color: #4338ca;
  177. padding: 6px 15px;
  178. border-radius: 20px;
  179. font-size: 13px;
  180. font-weight: 600;
  181. font-family: monospace;
  182. }
  183. .step-content {
  184. margin-top: 20px;
  185. }
  186. .info-grid {
  187. display: grid;
  188. grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  189. gap: 20px;
  190. margin-bottom: 20px;
  191. }
  192. .info-item {
  193. background: #f8f9fa;
  194. padding: 15px;
  195. border-radius: 8px;
  196. }
  197. .info-label {
  198. font-size: 13px;
  199. color: #666;
  200. margin-bottom: 5px;
  201. }
  202. .info-value {
  203. font-size: 20px;
  204. font-weight: bold;
  205. color: #1a1a1a;
  206. }
  207. .posts-grid {
  208. display: grid;
  209. grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  210. gap: 20px;
  211. margin-top: 20px;
  212. padding-top: 100px;
  213. margin-top: -80px;
  214. }
  215. .post-card {
  216. background: white;
  217. border-radius: 8px;
  218. overflow: visible;
  219. transition: transform 0.2s, box-shadow 0.2s;
  220. border: 1px solid #e5e7eb;
  221. cursor: pointer;
  222. position: relative;
  223. }
  224. .post-card:hover {
  225. transform: translateY(-4px);
  226. box-shadow: 0 6px 16px rgba(0,0,0,0.15);
  227. }
  228. .post-image-wrapper {
  229. width: 100%;
  230. background: #f3f4f6;
  231. position: relative;
  232. padding-top: 133.33%; /* 3:4 aspect ratio */
  233. overflow: hidden;
  234. border-radius: 8px 8px 0 0;
  235. }
  236. .post-image {
  237. position: absolute;
  238. top: 0;
  239. left: 0;
  240. width: 100%;
  241. height: 100%;
  242. object-fit: cover;
  243. }
  244. .no-image {
  245. position: absolute;
  246. top: 50%;
  247. left: 50%;
  248. transform: translate(-50%, -50%);
  249. color: #9ca3af;
  250. font-size: 14px;
  251. }
  252. .post-type-badge {
  253. position: absolute;
  254. top: 10px;
  255. right: 10px;
  256. background: rgba(0, 0, 0, 0.7);
  257. color: white;
  258. padding: 4px 10px;
  259. border-radius: 15px;
  260. font-size: 11px;
  261. font-weight: 600;
  262. }
  263. .post-info {
  264. padding: 15px;
  265. position: relative;
  266. overflow: visible;
  267. }
  268. .post-title {
  269. font-size: 14px;
  270. font-weight: 600;
  271. margin-bottom: 8px;
  272. color: #1a1a1a;
  273. display: -webkit-box;
  274. -webkit-line-clamp: 2;
  275. -webkit-box-orient: vertical;
  276. overflow: hidden;
  277. }
  278. .post-desc {
  279. font-size: 12px;
  280. color: #6b7280;
  281. margin-bottom: 10px;
  282. display: -webkit-box;
  283. -webkit-line-clamp: 2;
  284. -webkit-box-orient: vertical;
  285. overflow: hidden;
  286. }
  287. .post-meta {
  288. display: flex;
  289. gap: 15px;
  290. margin-bottom: 8px;
  291. font-size: 12px;
  292. color: #9ca3af;
  293. }
  294. .post-meta-item {
  295. display: flex;
  296. align-items: center;
  297. gap: 4px;
  298. }
  299. .post-author {
  300. font-size: 12px;
  301. color: #6b7280;
  302. margin-bottom: 8px;
  303. }
  304. .post-id {
  305. font-size: 10px;
  306. color: #9ca3af;
  307. font-family: monospace;
  308. }
  309. .evaluation-reason {
  310. position: absolute;
  311. bottom: calc(100% + 10px);
  312. left: 50%;
  313. transform: translateX(-50%);
  314. background: #2d3748;
  315. color: white;
  316. padding: 12px 16px;
  317. border-radius: 8px;
  318. font-size: 12px;
  319. line-height: 1.5;
  320. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  321. z-index: 1000;
  322. display: none;
  323. white-space: normal;
  324. width: 280px;
  325. }
  326. /* Tooltip 箭头 - 指向下方进度条 */
  327. .evaluation-reason::after {
  328. content: '';
  329. position: absolute;
  330. top: 100%;
  331. left: 50%;
  332. transform: translateX(-50%);
  333. border: 6px solid transparent;
  334. border-top-color: #2d3748;
  335. }
  336. .confidence-bar:hover .evaluation-reason {
  337. display: block !important;
  338. }
  339. /* Debug: 让进度条更明显可悬停 */
  340. .confidence-bar {
  341. min-height: 32px;
  342. }
  343. .evaluation-reason strong {
  344. color: #fbbf24;
  345. font-size: 13px;
  346. }
  347. .evaluation-scores {
  348. display: flex;
  349. gap: 10px;
  350. margin-top: 10px;
  351. font-size: 12px;
  352. flex-wrap: wrap;
  353. }
  354. .score-item {
  355. background: rgba(255, 255, 255, 0.15);
  356. padding: 5px 10px;
  357. border-radius: 12px;
  358. color: #fbbf24;
  359. border: 1px solid rgba(251, 191, 36, 0.3);
  360. }
  361. .confidence-bar {
  362. width: 100%;
  363. height: 32px;
  364. background: #f3f4f6;
  365. position: relative;
  366. cursor: help;
  367. display: flex;
  368. align-items: center;
  369. border-radius: 0 0 8px 8px;
  370. overflow: hidden;
  371. }
  372. .confidence-bar-fill {
  373. height: 100%;
  374. transition: width 0.5s ease-out;
  375. display: flex;
  376. align-items: center;
  377. padding: 0 12px;
  378. position: relative;
  379. }
  380. .confidence-bar-fill.confidence-low {
  381. background: linear-gradient(90deg, #ef4444, #f87171);
  382. }
  383. .confidence-bar-fill.confidence-medium {
  384. background: linear-gradient(90deg, #f59e0b, #fbbf24);
  385. }
  386. .confidence-bar-fill.confidence-high {
  387. background: linear-gradient(90deg, #10b981, #34d399);
  388. }
  389. .confidence-bar-text {
  390. color: white;
  391. font-size: 12px;
  392. font-weight: 600;
  393. white-space: nowrap;
  394. position: relative;
  395. z-index: 1;
  396. text-shadow: 0 1px 2px rgba(0,0,0,0.2);
  397. }
  398. /* 保留旧的badge样式用于兼容 */
  399. .confidence-badge {
  400. background: #10b981;
  401. color: white;
  402. padding: 4px 10px;
  403. border-radius: 15px;
  404. font-size: 12px;
  405. font-weight: bold;
  406. display: inline-block;
  407. margin-bottom: 10px;
  408. position: relative;
  409. cursor: help;
  410. }
  411. .confidence-low {
  412. background: #ef4444;
  413. }
  414. .confidence-medium {
  415. background: #f59e0b;
  416. }
  417. .confidence-high {
  418. background: #10b981;
  419. }
  420. .query-list {
  421. background: #f8f9fa;
  422. padding: 20px;
  423. border-radius: 8px;
  424. margin-top: 15px;
  425. }
  426. .query-item {
  427. background: white;
  428. padding: 15px;
  429. border-radius: 6px;
  430. margin-bottom: 10px;
  431. border-left: 3px solid #2563eb;
  432. }
  433. .query-text {
  434. font-size: 15px;
  435. font-weight: 600;
  436. color: #1a1a1a;
  437. margin-bottom: 5px;
  438. }
  439. .query-meta {
  440. font-size: 13px;
  441. color: #666;
  442. }
  443. .answer-box {
  444. background: #f0fdf4;
  445. border: 2px solid #10b981;
  446. border-radius: 8px;
  447. padding: 25px;
  448. margin-top: 20px;
  449. }
  450. .answer-header {
  451. font-size: 18px;
  452. color: #059669;
  453. margin-bottom: 15px;
  454. font-weight: 600;
  455. }
  456. .answer-content {
  457. font-size: 15px;
  458. line-height: 1.8;
  459. color: #1a1a1a;
  460. white-space: pre-wrap;
  461. }
  462. .answer-meta {
  463. margin-top: 15px;
  464. padding-top: 15px;
  465. border-top: 1px solid #d1fae5;
  466. display: flex;
  467. gap: 20px;
  468. font-size: 13px;
  469. color: #059669;
  470. }
  471. .keyword-tags {
  472. display: flex;
  473. flex-wrap: wrap;
  474. gap: 10px;
  475. margin-top: 15px;
  476. }
  477. .keyword-tag {
  478. background: #dbeafe;
  479. color: #1e40af;
  480. padding: 6px 12px;
  481. border-radius: 15px;
  482. font-size: 13px;
  483. font-weight: 500;
  484. }
  485. .level-analysis {
  486. background: #fef3c7;
  487. border-left: 4px solid #f59e0b;
  488. padding: 20px;
  489. border-radius: 6px;
  490. margin-top: 15px;
  491. }
  492. .level-analysis-title {
  493. font-size: 16px;
  494. color: #92400e;
  495. margin-bottom: 10px;
  496. font-weight: 600;
  497. }
  498. .level-analysis-text {
  499. font-size: 14px;
  500. color: #78350f;
  501. line-height: 1.8;
  502. }
  503. .timestamp {
  504. font-size: 12px;
  505. color: #9ca3af;
  506. margin-top: 10px;
  507. }
  508. a {
  509. color: #2563eb;
  510. text-decoration: none;
  511. }
  512. a:hover {
  513. text-decoration: underline;
  514. }
  515. /* 模态框样式 */
  516. .modal-overlay {
  517. display: none;
  518. position: fixed;
  519. top: 0;
  520. left: 0;
  521. right: 0;
  522. bottom: 0;
  523. background: rgba(0, 0, 0, 0.85);
  524. z-index: 1000;
  525. align-items: center;
  526. justify-content: center;
  527. padding: 20px;
  528. overflow-y: auto;
  529. }
  530. .modal-overlay.active {
  531. display: flex;
  532. }
  533. .modal-content {
  534. background: white;
  535. border-radius: 12px;
  536. max-width: 1000px;
  537. width: 100%;
  538. max-height: 90vh;
  539. overflow-y: auto;
  540. position: relative;
  541. animation: modalSlideIn 0.3s;
  542. }
  543. @keyframes modalSlideIn {
  544. from { opacity: 0; transform: translateY(-30px); }
  545. to { opacity: 1; transform: translateY(0); }
  546. }
  547. .modal-close {
  548. position: sticky;
  549. top: 0;
  550. right: 0;
  551. background: white;
  552. border: none;
  553. font-size: 36px;
  554. color: #6b7280;
  555. cursor: pointer;
  556. padding: 15px 25px;
  557. z-index: 10;
  558. text-align: right;
  559. border-bottom: 2px solid #e5e7eb;
  560. transition: color 0.2s;
  561. }
  562. .modal-close:hover {
  563. color: #1f2937;
  564. }
  565. .modal-body {
  566. padding: 30px;
  567. }
  568. .modal-title {
  569. font-size: 26px;
  570. font-weight: 700;
  571. color: #1a1a1a;
  572. margin-bottom: 15px;
  573. line-height: 1.4;
  574. }
  575. .modal-meta {
  576. display: flex;
  577. gap: 20px;
  578. flex-wrap: wrap;
  579. margin-bottom: 25px;
  580. padding-bottom: 20px;
  581. border-bottom: 1px solid #e5e7eb;
  582. }
  583. .modal-meta-item {
  584. display: flex;
  585. align-items: center;
  586. gap: 6px;
  587. font-size: 14px;
  588. color: #6b7280;
  589. }
  590. .modal-images {
  591. margin-bottom: 25px;
  592. }
  593. .modal-images-grid {
  594. display: grid;
  595. grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  596. gap: 12px;
  597. }
  598. .modal-image-item {
  599. border-radius: 8px;
  600. overflow: hidden;
  601. border: 2px solid #e5e7eb;
  602. transition: border-color 0.2s;
  603. cursor: pointer;
  604. }
  605. .modal-image-item:hover {
  606. border-color: #2563eb;
  607. }
  608. .modal-image {
  609. width: 100%;
  610. height: auto;
  611. display: block;
  612. max-height: 250px;
  613. object-fit: cover;
  614. }
  615. .modal-section {
  616. margin-bottom: 25px;
  617. }
  618. .modal-section-title {
  619. font-size: 17px;
  620. font-weight: 600;
  621. color: #374151;
  622. margin-bottom: 12px;
  623. }
  624. .modal-text-content {
  625. font-size: 15px;
  626. color: #1f2937;
  627. line-height: 1.8;
  628. white-space: pre-wrap;
  629. background: #f9fafb;
  630. padding: 18px;
  631. border-radius: 8px;
  632. }
  633. .modal-evaluation {
  634. background: #fef3c7;
  635. border-left: 4px solid #f59e0b;
  636. padding: 18px;
  637. border-radius: 6px;
  638. }
  639. .modal-link {
  640. margin-top: 25px;
  641. padding-top: 25px;
  642. border-top: 2px solid #e5e7eb;
  643. text-align: center;
  644. }
  645. .modal-link-btn {
  646. display: inline-flex;
  647. align-items: center;
  648. gap: 10px;
  649. padding: 12px 28px;
  650. background: #2563eb;
  651. color: white;
  652. text-decoration: none;
  653. border-radius: 8px;
  654. font-size: 15px;
  655. font-weight: 600;
  656. transition: all 0.2s;
  657. }
  658. .modal-link-btn:hover {
  659. background: #1d4ed8;
  660. transform: translateY(-2px);
  661. box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
  662. }
  663. /* 卡片上的图片轮播指示器 */
  664. .carousel-dots {
  665. position: absolute;
  666. bottom: 8px;
  667. left: 50%;
  668. transform: translateX(-50%);
  669. display: flex;
  670. gap: 6px;
  671. z-index: 15;
  672. }
  673. .carousel-dot {
  674. width: 8px;
  675. height: 8px;
  676. border-radius: 50%;
  677. background: rgba(255, 255, 255, 0.5);
  678. border: 1px solid rgba(0, 0, 0, 0.2);
  679. transition: all 0.2s;
  680. }
  681. .carousel-dot.active {
  682. background: white;
  683. width: 24px;
  684. border-radius: 4px;
  685. }
  686. /* 可折叠区域样式 */
  687. .collapsible-section {
  688. margin: 20px 0;
  689. }
  690. .collapsible-header {
  691. background: #f3f4f6;
  692. padding: 12px 15px;
  693. border-radius: 8px;
  694. cursor: pointer;
  695. display: flex;
  696. align-items: center;
  697. gap: 10px;
  698. transition: background 0.2s;
  699. user-select: none;
  700. }
  701. .collapsible-header:hover {
  702. background: #e5e7eb;
  703. }
  704. .collapsible-toggle {
  705. font-size: 14px;
  706. transition: transform 0.2s;
  707. }
  708. .collapsible-toggle.collapsed {
  709. transform: rotate(-90deg);
  710. }
  711. .collapsible-title {
  712. font-weight: 600;
  713. font-size: 16px;
  714. color: #374151;
  715. }
  716. .collapsible-content {
  717. max-height: 10000px;
  718. overflow: hidden;
  719. transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
  720. opacity: 1;
  721. }
  722. .collapsible-content.collapsed {
  723. max-height: 0;
  724. opacity: 0;
  725. }
  726. </style>
  727. </head>
  728. <body>
  729. <!-- 左侧导航 -->
  730. <div class="sidebar">
  731. <div class="sidebar-header">📑 目录</div>
  732. <div class="toc" id="toc"></div>
  733. </div>
  734. <!-- 主内容区 -->
  735. <div class="container">
  736. {content}
  737. </div>
  738. <!-- 模态框 -->
  739. <div id="postModal" class="modal-overlay" onclick="if(event.target === this) closeModal()">
  740. <div class="modal-content" onclick="event.stopPropagation()">
  741. <button class="modal-close" onclick="closeModal()">&times;</button>
  742. <div class="modal-body" id="modalBody">
  743. <!-- 动态内容 -->
  744. </div>
  745. </div>
  746. </div>
  747. <script>
  748. // 模态框功能
  749. function openModal(postData) {
  750. const modal = document.getElementById('postModal');
  751. const modalBody = document.getElementById('modalBody');
  752. // 构建图片网格
  753. let imagesHtml = '';
  754. if (postData.images && postData.images.length > 0) {
  755. imagesHtml = '<div class="modal-images"><div class="modal-images-grid">';
  756. postData.images.forEach((img, idx) => {
  757. imagesHtml += `<div class="modal-image-item"><img src="${img}" class="modal-image" alt="图片 ${idx + 1}"></div>`;
  758. });
  759. imagesHtml += '</div></div>';
  760. }
  761. // 构建评估详情
  762. let evalHtml = '';
  763. if (postData.evaluation) {
  764. evalHtml = `
  765. <div class="modal-section">
  766. <div class="modal-section-title">💡 评估详情</div>
  767. <div class="modal-evaluation">
  768. <div style="margin-bottom: 12px;"><strong>评估理由:</strong></div>
  769. <div style="color: #78350f; line-height: 1.8;">${postData.evaluation.reason || '无'}</div>
  770. <div class="evaluation-scores" style="margin-top: 12px;">
  771. <span class="score-item">📌 标题相关性: ${postData.evaluation.title_relevance?.toFixed(2) || '0.00'}</span>
  772. <span class="score-item">📄 内容期望: ${postData.evaluation.content_expectation?.toFixed(2) || '0.00'}</span>
  773. <span class="score-item">🎯 置信度: ${postData.evaluation.confidence_score?.toFixed(2) || '0.00'}</span>
  774. </div>
  775. </div>
  776. </div>`;
  777. }
  778. modalBody.innerHTML = `
  779. <div class="modal-title">${postData.title}</div>
  780. <div class="modal-meta">
  781. <div class="modal-meta-item">👤 ${postData.user}</div>
  782. <div class="modal-meta-item">❤️ ${postData.likes}</div>
  783. <div class="modal-meta-item">⭐ ${postData.collects}</div>
  784. <div class="modal-meta-item">💬 ${postData.comments}</div>
  785. ${postData.type === 'video' ? '<div class="modal-meta-item">📹 视频</div>' : ''}
  786. </div>
  787. ${imagesHtml}
  788. <div class="modal-section">
  789. <div class="modal-section-title">📝 描述</div>
  790. <div class="modal-text-content">${postData.desc || '无描述'}</div>
  791. </div>
  792. ${evalHtml}
  793. <div class="modal-link">
  794. <a href="${postData.url}" target="_blank" class="modal-link-btn">
  795. 🔗 在小红书中查看
  796. </a>
  797. </div>
  798. `;
  799. modal.classList.add('active');
  800. document.body.style.overflow = 'hidden';
  801. }
  802. function closeModal() {
  803. const modal = document.getElementById('postModal');
  804. modal.classList.remove('active');
  805. document.body.style.overflow = '';
  806. }
  807. // ESC键关闭模态框
  808. document.addEventListener('keydown', function(e) {
  809. if (e.key === 'Escape') {
  810. closeModal();
  811. }
  812. });
  813. // 卡片上的图片轮播(简单版本,使用点击切换)
  814. function initCarousels() {
  815. document.querySelectorAll('.post-card').forEach(card => {
  816. const images = JSON.parse(card.dataset.images || '[]');
  817. if (images.length <= 1) return;
  818. let currentIndex = 0;
  819. const imgElement = card.querySelector('.post-image');
  820. const dots = card.querySelectorAll('.carousel-dot');
  821. card.addEventListener('click', function(e) {
  822. // 如果点击的是图片区域,切换图片
  823. if (e.target.closest('.post-image-wrapper')) {
  824. e.stopPropagation();
  825. currentIndex = (currentIndex + 1) % images.length;
  826. if (imgElement) {
  827. imgElement.src = images[currentIndex];
  828. }
  829. // 更新指示器
  830. dots.forEach((dot, idx) => {
  831. dot.classList.toggle('active', idx === currentIndex);
  832. });
  833. }
  834. });
  835. });
  836. }
  837. // 生成目录(显示步骤和可折叠的子项)
  838. function generateTOC() {
  839. const toc = document.getElementById('toc');
  840. const sections = document.querySelectorAll('.step-section');
  841. sections.forEach((section, index) => {
  842. const title = section.querySelector('.step-title')?.textContent || `步骤 ${index + 1}`;
  843. const id = `step-${index}`;
  844. section.id = id;
  845. // 查找该section下的直接子可折叠项(不包括嵌套的)
  846. const collapsibleSections = section.querySelectorAll(':scope > .step-content > .collapsible-section[id]');
  847. // 创建步骤项
  848. const stepItem = document.createElement('div');
  849. stepItem.className = 'toc-item toc-item-level-0';
  850. if (collapsibleSections.length > 0) {
  851. // 如果有子项,添加展开/折叠图标(箭头放在右侧)
  852. const toggleId = `toc-toggle-${index}`;
  853. stepItem.innerHTML = `<span>${title}</span><span class="toc-toggle" id="${toggleId}">▼</span>`;
  854. const toggle = stepItem.querySelector('.toc-toggle');
  855. const childrenId = `toc-children-${index}`;
  856. toggle.onclick = (e) => {
  857. e.stopPropagation();
  858. toggle.classList.toggle('collapsed');
  859. const children = document.getElementById(childrenId);
  860. if (children) {
  861. children.classList.toggle('expanded');
  862. }
  863. };
  864. } else {
  865. stepItem.textContent = title;
  866. }
  867. stepItem.onclick = (e) => {
  868. if (!e.target.classList.contains('toc-toggle')) {
  869. scrollToSection(id);
  870. }
  871. };
  872. toc.appendChild(stepItem);
  873. // 添加子项目录(支持嵌套)
  874. if (collapsibleSections.length > 0) {
  875. const childrenContainer = document.createElement('div');
  876. childrenContainer.id = `toc-children-${index}`;
  877. childrenContainer.className = 'toc-children expanded';
  878. collapsibleSections.forEach(collapsible => {
  879. const subTitle = collapsible.getAttribute('data-title') || '子项';
  880. const subId = collapsible.id;
  881. const subItem = document.createElement('div');
  882. subItem.className = 'toc-item toc-item-level-1';
  883. subItem.textContent = subTitle;
  884. subItem.onclick = () => scrollToSection(subId);
  885. childrenContainer.appendChild(subItem);
  886. // 查找该可折叠区域内的嵌套可折叠区域
  887. const nestedCollapsibles = collapsible.querySelectorAll(':scope > .collapsible-content > .collapsible-section[id]');
  888. if (nestedCollapsibles.length > 0) {
  889. nestedCollapsibles.forEach(nested => {
  890. const nestedTitle = nested.getAttribute('data-title') || '子项';
  891. const nestedId = nested.id;
  892. const nestedItem = document.createElement('div');
  893. nestedItem.className = 'toc-item toc-item-level-2';
  894. nestedItem.textContent = nestedTitle;
  895. nestedItem.onclick = () => scrollToSection(nestedId);
  896. childrenContainer.appendChild(nestedItem);
  897. });
  898. }
  899. });
  900. toc.appendChild(childrenContainer);
  901. }
  902. });
  903. }
  904. // 滚动到指定section
  905. function scrollToSection(id) {
  906. const element = document.getElementById(id);
  907. if (element) {
  908. const offset = 80;
  909. const elementPosition = element.getBoundingClientRect().top;
  910. const offsetPosition = elementPosition + window.pageYOffset - offset;
  911. window.scrollTo({
  912. top: offsetPosition,
  913. behavior: 'smooth'
  914. });
  915. // 更新active状态
  916. document.querySelectorAll('.toc-item').forEach(item => item.classList.remove('active'));
  917. event.target.classList.add('active');
  918. }
  919. }
  920. // 滚动时高亮当前section
  921. function updateActiveTOC() {
  922. const sections = document.querySelectorAll('.step-section');
  923. const tocItems = document.querySelectorAll('.toc-item');
  924. let currentIndex = -1;
  925. sections.forEach((section, index) => {
  926. const rect = section.getBoundingClientRect();
  927. if (rect.top <= 100) {
  928. currentIndex = index;
  929. }
  930. });
  931. tocItems.forEach((item, index) => {
  932. item.classList.toggle('active', index === currentIndex);
  933. });
  934. }
  935. // 初始化可折叠区域
  936. function initCollapsibles() {
  937. document.querySelectorAll('.collapsible-header').forEach(header => {
  938. header.addEventListener('click', function() {
  939. const toggle = this.querySelector('.collapsible-toggle');
  940. const content = this.nextElementSibling;
  941. if (content && content.classList.contains('collapsible-content')) {
  942. toggle.classList.toggle('collapsed');
  943. content.classList.toggle('collapsed');
  944. }
  945. });
  946. });
  947. }
  948. // 页面加载完成后初始化
  949. document.addEventListener('DOMContentLoaded', function() {
  950. initCarousels();
  951. generateTOC();
  952. initCollapsibles();
  953. window.addEventListener('scroll', updateActiveTOC);
  954. updateActiveTOC();
  955. });
  956. </script>
  957. </body>
  958. </html>
  959. """
  960. def make_collapsible(title, content, collapsed=True, section_id=None):
  961. """创建可折叠区域的HTML"""
  962. collapsed_class = " collapsed" if collapsed else ""
  963. id_attr = f' id="{section_id}"' if section_id else ""
  964. # 添加 data-title 属性用于目录生成
  965. title_attr = f' data-title="{title}"' if section_id else ""
  966. return f"""
  967. <div class="collapsible-section"{id_attr}{title_attr}>
  968. <div class="collapsible-header">
  969. <span class="collapsible-toggle{collapsed_class}">▼</span>
  970. <span class="collapsible-title">{title}</span>
  971. </div>
  972. <div class="collapsible-content{collapsed_class}">
  973. {content}
  974. </div>
  975. </div>
  976. """
  977. def get_confidence_class(score):
  978. """根据置信度分数返回CSS类"""
  979. if score >= 0.7:
  980. return "confidence-high"
  981. elif score >= 0.5:
  982. return "confidence-medium"
  983. else:
  984. return "confidence-low"
  985. def escape_js_string(s):
  986. """转义JavaScript字符串"""
  987. import json
  988. return json.dumps(str(s) if s else "")
  989. def build_post_json_data(note, evaluation=None):
  990. """构建帖子的JSON数据用于模态框"""
  991. import json
  992. image_list = note.get('image_list', [])
  993. if not image_list and note.get('cover_image'):
  994. cover = note.get('cover_image')
  995. # cover_image 可能是字典或字符串
  996. if isinstance(cover, dict):
  997. image_list = [cover.get('image_url', '')]
  998. else:
  999. image_list = [cover]
  1000. # image_list 现在已经是 URL 字符串列表(由搜索API预处理)
  1001. images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
  1002. interact = note.get('interact_info', {})
  1003. user = note.get('user', {})
  1004. data = {
  1005. 'title': note.get('title', '无标题'),
  1006. 'desc': note.get('desc', ''),
  1007. 'user': user.get('nickname', '未知'),
  1008. 'likes': interact.get('liked_count', 0),
  1009. 'collects': interact.get('collected_count', 0),
  1010. 'comments': interact.get('comment_count', 0),
  1011. 'type': note.get('type', 'normal'),
  1012. 'url': note.get('note_url', ''),
  1013. 'images': images
  1014. }
  1015. if evaluation:
  1016. data['evaluation'] = {
  1017. 'reason': evaluation.get('reason', ''),
  1018. 'title_relevance': evaluation.get('title_relevance', 0),
  1019. 'content_expectation': evaluation.get('content_expectation', 0),
  1020. 'confidence_score': evaluation.get('confidence_score', 0)
  1021. }
  1022. return json.dumps(data, ensure_ascii=False)
  1023. def render_header(steps_data):
  1024. """渲染页面头部"""
  1025. # 获取基本信息
  1026. first_step = steps_data[0] if steps_data else {}
  1027. last_step = steps_data[-1] if steps_data else {}
  1028. original_question = ""
  1029. keywords = []
  1030. total_steps = len(steps_data)
  1031. satisfied_notes = 0
  1032. # 提取关键信息
  1033. for step in steps_data:
  1034. if step.get("step_type") == "keyword_extraction":
  1035. original_question = step.get("data", {}).get("input_question", "")
  1036. keywords = step.get("data", {}).get("keywords", [])
  1037. elif step.get("step_type") == "final_result":
  1038. satisfied_notes = step.get("data", {}).get("satisfied_notes_count", 0)
  1039. keywords_html = "".join([f'<span class="keyword-tag">{k}</span>' for k in keywords])
  1040. html = f"""
  1041. <div class="header">
  1042. <h1>🔍 Query Optimization Steps</h1>
  1043. <div class="question-box">
  1044. <div class="question-label">原始问题</div>
  1045. <div class="question-text">{original_question}</div>
  1046. </div>
  1047. {f'<div class="keyword-tags">{keywords_html}</div>' if keywords else ''}
  1048. <div class="overview">
  1049. <div class="overview-item">
  1050. <div class="overview-label">总步骤数</div>
  1051. <div class="overview-value">{total_steps}</div>
  1052. </div>
  1053. <div class="overview-item">
  1054. <div class="overview-label">满足需求的帖子</div>
  1055. <div class="overview-value">{satisfied_notes}</div>
  1056. </div>
  1057. </div>
  1058. </div>
  1059. """
  1060. return html
  1061. def render_keyword_extraction(step):
  1062. """渲染关键词提取步骤"""
  1063. data = step.get("data", {})
  1064. keywords = data.get("keywords", [])
  1065. reasoning = data.get("reasoning", "")
  1066. keywords_html = "".join([f'<span class="keyword-tag">{k}</span>' for k in keywords])
  1067. html = f"""
  1068. <div class="step-section">
  1069. <div class="step-header">
  1070. <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
  1071. <div class="step-type">{step['step_type']}</div>
  1072. </div>
  1073. <div class="step-content">
  1074. <div class="keyword-tags">{keywords_html}</div>
  1075. {f'<p style="margin-top: 15px; color: #666; font-size: 14px;">{reasoning}</p>' if reasoning else ''}
  1076. </div>
  1077. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1078. </div>
  1079. """
  1080. return html
  1081. def render_level_exploration(step):
  1082. """渲染层级探索步骤"""
  1083. data = step.get("data", {})
  1084. level = data.get("level", 0)
  1085. query_count = data.get("query_count", 0)
  1086. results = data.get("results", [])
  1087. queries_html = ""
  1088. for result in results:
  1089. query = result.get("query", "")
  1090. suggestions = result.get("suggestions", [])
  1091. # 使用标签样式显示推荐词
  1092. suggestions_tags = ""
  1093. for suggestion in suggestions:
  1094. suggestions_tags += f'<span class="keyword-tag" style="margin: 3px;">{suggestion}</span>'
  1095. queries_html += f"""
  1096. <div class="query-item">
  1097. <div class="query-text">{query}</div>
  1098. <div style="margin-top: 10px;">
  1099. <div style="color: #666; font-size: 13px; margin-bottom: 5px;">推荐词 ({len(suggestions)} 个):</div>
  1100. <div style="display: flex; flex-wrap: wrap; gap: 5px;">
  1101. {suggestions_tags}
  1102. </div>
  1103. </div>
  1104. </div>
  1105. """
  1106. html = f"""
  1107. <div class="step-section">
  1108. <div class="step-header">
  1109. <div class="step-title">步骤 {step['step_number']}: Level {level} 探索</div>
  1110. <div class="step-type">{step['step_type']}</div>
  1111. </div>
  1112. <div class="step-content">
  1113. <div class="info-grid">
  1114. <div class="info-item">
  1115. <div class="info-label">探索query数</div>
  1116. <div class="info-value">{query_count}</div>
  1117. </div>
  1118. <div class="info-item">
  1119. <div class="info-label">获得推荐词总数</div>
  1120. <div class="info-value">{data.get('total_suggestions', 0)}</div>
  1121. </div>
  1122. </div>
  1123. <div class="query-list">{queries_html}</div>
  1124. </div>
  1125. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1126. </div>
  1127. """
  1128. return html
  1129. def render_level_analysis(step):
  1130. """渲染层级分析步骤"""
  1131. data = step.get("data", {})
  1132. level = data.get("level", 0)
  1133. key_findings = data.get("key_findings", "")
  1134. should_evaluate = data.get("should_evaluate_now", False)
  1135. promising_signals_count = data.get("promising_signals_count", 0)
  1136. next_combinations = data.get("next_combinations", [])
  1137. promising_signals = data.get("promising_signals", [])
  1138. reasoning = data.get("reasoning", "")
  1139. step_num = step['step_number']
  1140. # 渲染推理过程
  1141. reasoning_html = ""
  1142. if reasoning:
  1143. reasoning_html = f"""
  1144. <div style="margin-top: 20px;">
  1145. <div class="level-analysis">
  1146. <div class="level-analysis-title">💭 推理过程</div>
  1147. <div class="level-analysis-text">{reasoning}</div>
  1148. </div>
  1149. </div>
  1150. """
  1151. # 渲染下一层探索
  1152. next_html = ""
  1153. if next_combinations:
  1154. next_items = "".join([f'<span class="keyword-tag">{q}</span>' for q in next_combinations])
  1155. next_html = f'<div style="margin-top: 15px;"><strong>下一层探索:</strong><div class="keyword-tags" style="margin-top: 10px;">{next_items}</div></div>'
  1156. # 渲染有价值的信号
  1157. signals_html = ""
  1158. if promising_signals:
  1159. signals_items = ""
  1160. for signal in promising_signals:
  1161. query = signal.get("query", "")
  1162. from_level = signal.get("from_level", "")
  1163. reason = signal.get("reason", "")
  1164. signals_items += f"""
  1165. <div class="query-item" style="border-left: 3px solid #10b981; padding-left: 15px;">
  1166. <div class="query-text" style="font-weight: 600;">{query}</div>
  1167. <div style="margin-top: 8px; color: #666; font-size: 13px;">
  1168. <span style="color: #10b981;">来自 Level {from_level}</span>
  1169. </div>
  1170. <div style="margin-top: 8px; color: #555; font-size: 14px; line-height: 1.5;">
  1171. {reason}
  1172. </div>
  1173. </div>
  1174. """
  1175. signals_html = make_collapsible(
  1176. f"💡 有价值的信号 ({len(promising_signals)} 个)",
  1177. f'<div style="display: flex; flex-direction: column; gap: 15px; margin-top: 10px;">{signals_items}</div>',
  1178. collapsed=True,
  1179. section_id=f"step{step_num}-signals"
  1180. )
  1181. html = f"""
  1182. <div class="step-section">
  1183. <div class="step-header">
  1184. <div class="step-title">步骤 {step['step_number']}: Level {level} 分析</div>
  1185. <div class="step-type">{step['step_type']}</div>
  1186. </div>
  1187. <div class="step-content">
  1188. <div class="level-analysis">
  1189. <div class="level-analysis-title">🔎 关键发现</div>
  1190. <div class="level-analysis-text">{key_findings}</div>
  1191. </div>
  1192. <div class="info-grid" style="margin-top: 20px;">
  1193. <div class="info-item">
  1194. <div class="info-label">有价值信号数</div>
  1195. <div class="info-value">{promising_signals_count}</div>
  1196. </div>
  1197. <div class="info-item">
  1198. <div class="info-label">是否开始评估</div>
  1199. <div class="info-value">{'是' if should_evaluate else '否'}</div>
  1200. </div>
  1201. </div>
  1202. {signals_html}
  1203. {reasoning_html}
  1204. {next_html}
  1205. </div>
  1206. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1207. </div>
  1208. """
  1209. return html
  1210. def render_search_results(step):
  1211. """渲染搜索结果步骤"""
  1212. data = step.get("data", {})
  1213. search_results = data.get("search_results", [])
  1214. posts_html = ""
  1215. step_num = step['step_number']
  1216. for idx, sr in enumerate(search_results):
  1217. query = sr.get("query", "")
  1218. note_count = sr.get("note_count", 0)
  1219. notes_summary = sr.get("notes_summary", [])
  1220. # 渲染该query的帖子
  1221. posts_cards = ""
  1222. for note in notes_summary:
  1223. # 获取封面图
  1224. image_list = note.get('image_list', [])
  1225. if image_list:
  1226. # image_list 已经是 URL 字符串列表,第一张就是封面
  1227. cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
  1228. else:
  1229. cover = note.get("cover_image", {})
  1230. cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
  1231. interact = note.get("interact_info", {})
  1232. user = note.get("user", {})
  1233. # image_list 现在已经是 URL 字符串列表
  1234. images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
  1235. # 构建帖子数据用于模态框
  1236. post_data = build_post_json_data(note)
  1237. images_json = json.dumps(images)
  1238. image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
  1239. type_badge = ""
  1240. if note.get("type") == "video":
  1241. type_badge = '<div class="post-type-badge">📹 视频</div>'
  1242. # 轮播指示器
  1243. dots_html = ""
  1244. if len(images) > 1:
  1245. dots_html = '<div class="carousel-dots">'
  1246. for i in range(len(images)):
  1247. active_class = ' active' if i == 0 else ''
  1248. dots_html += f'<div class="carousel-dot{active_class}"></div>'
  1249. dots_html += '</div>'
  1250. posts_cards += f"""
  1251. <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
  1252. <div class="post-image-wrapper">
  1253. {image_html}
  1254. {type_badge}
  1255. {dots_html}
  1256. </div>
  1257. <div class="post-info">
  1258. <div class="post-title">{note.get('title', '无标题')}</div>
  1259. <div class="post-desc">{note.get('desc', '')}</div>
  1260. <div class="post-meta">
  1261. <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
  1262. <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
  1263. <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
  1264. </div>
  1265. <div class="post-author">👤 {user.get('nickname', '未知')}</div>
  1266. <div class="post-id">{note.get('note_id', '')}</div>
  1267. </div>
  1268. </div>
  1269. """
  1270. # 使用可折叠区域包装每个query的搜索结果,添加唯一ID
  1271. query_content = f'<div class="posts-grid">{posts_cards}</div>'
  1272. posts_html += make_collapsible(
  1273. f"🔎 {query} (找到 {note_count} 个帖子)",
  1274. query_content,
  1275. collapsed=True,
  1276. section_id=f"step{step_num}-search-{idx}"
  1277. )
  1278. html = f"""
  1279. <div class="step-section">
  1280. <div class="step-header">
  1281. <div class="step-title">步骤 {step['step_number']}: 搜索结果</div>
  1282. <div class="step-type">{step['step_type']}</div>
  1283. </div>
  1284. <div class="step-content">
  1285. <div class="info-grid">
  1286. <div class="info-item">
  1287. <div class="info-label">搜索query数</div>
  1288. <div class="info-value">{data.get('qualified_count', 0)}</div>
  1289. </div>
  1290. </div>
  1291. {posts_html}
  1292. </div>
  1293. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1294. </div>
  1295. """
  1296. return html
  1297. def render_note_evaluations(step):
  1298. """渲染帖子评估步骤"""
  1299. data = step.get("data", {})
  1300. note_evaluations = data.get("note_evaluations", [])
  1301. total_satisfied = data.get("total_satisfied", 0)
  1302. evals_html = ""
  1303. step_num = step["step_number"]
  1304. for idx, query_eval in enumerate(note_evaluations):
  1305. query = query_eval.get("query", "")
  1306. satisfied_count = query_eval.get("satisfied_count", 0)
  1307. evaluated_notes = query_eval.get("evaluated_notes", [])
  1308. # 分离满足和不满足需求的帖子
  1309. satisfied_notes = [n for n in evaluated_notes if n.get('evaluation', {}).get('need_satisfaction')]
  1310. unsatisfied_notes = [n for n in evaluated_notes if not n.get('evaluation', {}).get('need_satisfaction')]
  1311. # 渲染满足需求的帖子
  1312. satisfied_cards = ""
  1313. for note in satisfied_notes:
  1314. # 获取封面图
  1315. image_list = note.get('image_list', [])
  1316. if image_list:
  1317. cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
  1318. else:
  1319. cover = note.get("cover_image", {})
  1320. cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
  1321. interact = note.get("interact_info", {})
  1322. user = note.get("user", {})
  1323. evaluation = note.get("evaluation", {})
  1324. confidence = evaluation.get("confidence_score", 0)
  1325. # image_list 现在已经是 URL 字符串列表
  1326. images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
  1327. # 构建帖子数据用于模态框
  1328. post_data = build_post_json_data(note, evaluation)
  1329. images_json = json.dumps(images)
  1330. image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
  1331. type_badge = ""
  1332. if note.get("type") == "video":
  1333. type_badge = '<div class="post-type-badge">📹 视频</div>'
  1334. # 轮播指示器
  1335. dots_html = ""
  1336. if len(images) > 1:
  1337. dots_html = '<div class="carousel-dots">'
  1338. for i in range(len(images)):
  1339. active_class = ' active' if i == 0 else ''
  1340. dots_html += f'<div class="carousel-dot{active_class}"></div>'
  1341. dots_html += '</div>'
  1342. # 评估详情
  1343. eval_reason = evaluation.get("reason", "")
  1344. title_rel = evaluation.get("title_relevance", 0)
  1345. content_exp = evaluation.get("content_expectation", 0)
  1346. eval_details = ""
  1347. # 置信度百分比
  1348. confidence_percent = int(confidence * 100)
  1349. satisfied_cards += f"""
  1350. <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
  1351. <div class="post-image-wrapper">
  1352. {image_html}
  1353. {type_badge}
  1354. {dots_html}
  1355. </div>
  1356. <div class="post-info">
  1357. <div class="post-title">{note.get('title', '无标题')}</div>
  1358. <div class="post-desc">{note.get('desc', '')}</div>
  1359. <div class="post-meta">
  1360. <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
  1361. <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
  1362. <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
  1363. </div>
  1364. <div class="post-author">👤 {user.get('nickname', '未知')}</div>
  1365. <div class="post-id">{note.get('note_id', '')}</div>
  1366. </div>
  1367. <div class="confidence-bar">
  1368. <div class="confidence-bar-fill {get_confidence_class(confidence)}" style="width: {confidence_percent}%">
  1369. <span class="confidence-bar-text">置信度: {confidence:.2f}</span>
  1370. </div>
  1371. {eval_details}
  1372. </div>
  1373. </div>
  1374. """
  1375. # 渲染不满足需求的帖子
  1376. unsatisfied_cards = ""
  1377. for note in unsatisfied_notes:
  1378. # 获取封面图
  1379. image_list = note.get('image_list', [])
  1380. if image_list:
  1381. cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
  1382. else:
  1383. cover = note.get("cover_image", {})
  1384. cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
  1385. interact = note.get("interact_info", {})
  1386. user = note.get("user", {})
  1387. evaluation = note.get("evaluation", {})
  1388. confidence = evaluation.get("confidence_score", 0)
  1389. # image_list 现在已经是 URL 字符串列表
  1390. images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
  1391. post_data = build_post_json_data(note, evaluation)
  1392. images_json = json.dumps(images)
  1393. image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
  1394. type_badge = ""
  1395. if note.get("type") == "video":
  1396. type_badge = '<div class="post-type-badge">📹 视频</div>'
  1397. dots_html = ""
  1398. if len(images) > 1:
  1399. dots_html = '<div class="carousel-dots">'
  1400. for i in range(len(images)):
  1401. active_class = ' active' if i == 0 else ''
  1402. dots_html += f'<div class="carousel-dot{active_class}"></div>'
  1403. dots_html += '</div>'
  1404. eval_reason = evaluation.get("reason", "")
  1405. title_rel = evaluation.get("title_relevance", 0)
  1406. content_exp = evaluation.get("content_expectation", 0)
  1407. eval_details = ""
  1408. confidence_percent = int(confidence * 100)
  1409. unsatisfied_cards += f"""
  1410. <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
  1411. <div class="post-image-wrapper">
  1412. {image_html}
  1413. {type_badge}
  1414. {dots_html}
  1415. </div>
  1416. <div class="post-info">
  1417. <div class="post-title">{note.get('title', '无标题')}</div>
  1418. <div class="post-desc">{note.get('desc', '')}</div>
  1419. <div class="post-meta">
  1420. <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
  1421. <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
  1422. <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
  1423. </div>
  1424. <div class="post-author">👤 {user.get('nickname', '未知')}</div>
  1425. <div class="post-id">{note.get('note_id', '')}</div>
  1426. </div>
  1427. <div class="confidence-bar">
  1428. <div class="confidence-bar-fill {get_confidence_class(confidence)}" style="width: {confidence_percent}%">
  1429. <span class="confidence-bar-text">置信度: {confidence:.2f}</span>
  1430. </div>
  1431. {eval_details}
  1432. </div>
  1433. </div>
  1434. """
  1435. # 构建该query的评估结果,使用嵌套可折叠区域
  1436. query_sections = ""
  1437. if satisfied_cards:
  1438. query_sections += make_collapsible(
  1439. f"✅ 满足需求 ({len(satisfied_notes)} 个帖子)",
  1440. f'<div class="posts-grid">{satisfied_cards}</div>',
  1441. collapsed=True,
  1442. section_id=f"step{step_num}-eval-{idx}-satisfied"
  1443. )
  1444. if unsatisfied_cards:
  1445. query_sections += make_collapsible(
  1446. f"❌ 不满足需求 ({len(unsatisfied_notes)} 个帖子)",
  1447. f'<div class="posts-grid">{unsatisfied_cards}</div>',
  1448. collapsed=True,
  1449. section_id=f"step{step_num}-eval-{idx}-unsatisfied"
  1450. )
  1451. if query_sections:
  1452. # 使用可折叠区域包装每个query的评估结果
  1453. evals_html += make_collapsible(
  1454. f"📊 {query} ({satisfied_count}/{len(evaluated_notes)} 个满足需求)",
  1455. query_sections,
  1456. collapsed=True,
  1457. section_id=f"step{step_num}-eval-{idx}"
  1458. )
  1459. html = f"""
  1460. <div class="step-section">
  1461. <div class="step-header">
  1462. <div class="step-title">步骤 {step['step_number']}: 帖子评估结果</div>
  1463. <div class="step-type">{step['step_type']}</div>
  1464. </div>
  1465. <div class="step-content">
  1466. <div class="info-grid">
  1467. <div class="info-item">
  1468. <div class="info-label">评估的query数</div>
  1469. <div class="info-value">{data.get('query_count', 0)}</div>
  1470. </div>
  1471. <div class="info-item">
  1472. <div class="info-label">总帖子数</div>
  1473. <div class="info-value">{data.get('total_notes', 0)}</div>
  1474. </div>
  1475. <div class="info-item">
  1476. <div class="info-label">满足需求的帖子</div>
  1477. <div class="info-value">{total_satisfied}</div>
  1478. </div>
  1479. </div>
  1480. {evals_html}
  1481. </div>
  1482. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1483. </div>
  1484. """
  1485. return html
  1486. def render_answer_generation(step):
  1487. """渲染答案生成步骤"""
  1488. data = step.get("data", {})
  1489. result = data.get("result", {})
  1490. answer = result.get("answer", "")
  1491. confidence = result.get("confidence", 0)
  1492. summary = result.get("summary", "")
  1493. cited_notes = result.get("cited_notes", [])
  1494. # 渲染引用的帖子
  1495. cited_html = ""
  1496. for note in cited_notes:
  1497. # 获取封面图
  1498. image_list = note.get('image_list', [])
  1499. if image_list:
  1500. cover_url = image_list[0] if isinstance(image_list[0], str) else image_list[0].get('image_url', '')
  1501. else:
  1502. cover = note.get("cover_image", {})
  1503. cover_url = cover.get("image_url", "") if isinstance(cover, dict) else cover if cover else ""
  1504. interact = note.get("interact_info", {})
  1505. user = note.get("user", {})
  1506. # image_list 现在已经是 URL 字符串列表
  1507. images = [img if isinstance(img, str) else img.get('image_url', '') for img in image_list if img]
  1508. # 构建帖子数据用于模态框(包含评估信息)
  1509. eval_data = {
  1510. 'reason': note.get("reason", ""),
  1511. 'title_relevance': note.get("title_relevance", 0),
  1512. 'content_expectation': note.get("content_expectation", 0),
  1513. 'confidence_score': note.get('confidence_score', 0)
  1514. }
  1515. post_data = build_post_json_data(note, eval_data)
  1516. images_json = json.dumps(images)
  1517. image_html = f'<img src="{cover_url}" class="post-image" alt="{note.get("title", "")}">' if cover_url else '<div class="no-image">无图片</div>'
  1518. # 类型标识
  1519. type_badge = ""
  1520. if note.get("type") == "video":
  1521. type_badge = '<div class="post-type-badge">📹 视频</div>'
  1522. # 轮播指示器
  1523. dots_html = ""
  1524. if len(images) > 1:
  1525. dots_html = '<div class="carousel-dots">'
  1526. for i in range(len(images)):
  1527. active_class = ' active' if i == 0 else ''
  1528. dots_html += f'<div class="carousel-dot{active_class}"></div>'
  1529. dots_html += '</div>'
  1530. # 评估详情
  1531. eval_reason = note.get("reason", "")
  1532. title_rel = note.get("title_relevance", 0)
  1533. content_exp = note.get("content_expectation", 0)
  1534. eval_details = ""
  1535. # 置信度百分比
  1536. note_confidence = note.get('confidence_score', 0)
  1537. confidence_percent = int(note_confidence * 100)
  1538. cited_html += f"""
  1539. <div class="post-card" onclick='openModal({post_data})' data-images='{images_json}'>
  1540. <div class="post-image-wrapper">
  1541. {image_html}
  1542. {type_badge}
  1543. {dots_html}
  1544. </div>
  1545. <div class="post-info">
  1546. <div class="post-title">[{note.get('index')}] {note.get('title', '无标题')}</div>
  1547. <div class="post-desc">{note.get('desc', '')}</div>
  1548. <div class="post-meta">
  1549. <div class="post-meta-item">❤️ {interact.get('liked_count', 0)}</div>
  1550. <div class="post-meta-item">⭐ {interact.get('collected_count', 0)}</div>
  1551. <div class="post-meta-item">💬 {interact.get('comment_count', 0)}</div>
  1552. </div>
  1553. <div class="post-author">👤 {user.get('nickname', '未知')}</div>
  1554. <div class="post-id">{note.get('note_id', '')}</div>
  1555. </div>
  1556. <div class="confidence-bar">
  1557. <div class="confidence-bar-fill {get_confidence_class(note_confidence)}" style="width: {confidence_percent}%">
  1558. <span class="confidence-bar-text">置信度: {note_confidence:.2f}</span>
  1559. </div>
  1560. {eval_details}
  1561. </div>
  1562. </div>
  1563. """
  1564. # 使用可折叠区域包装引用的帖子
  1565. step_num = step['step_number']
  1566. cited_section = ""
  1567. if cited_html:
  1568. cited_section = make_collapsible(
  1569. f"📌 引用的帖子 ({len(cited_notes)} 个)",
  1570. f'<div class="posts-grid">{cited_html}</div>',
  1571. collapsed=True,
  1572. section_id=f"step{step_num}-cited"
  1573. )
  1574. html = f"""
  1575. <div class="step-section">
  1576. <div class="step-header">
  1577. <div class="step-title">步骤 {step['step_number']}: 生成答案</div>
  1578. <div class="step-type">{step['step_type']}</div>
  1579. </div>
  1580. <div class="step-content">
  1581. <div class="answer-box">
  1582. <div class="answer-header">📝 生成的答案</div>
  1583. <div class="answer-content">{answer}</div>
  1584. <div class="answer-meta">
  1585. <div><strong>置信度:</strong> {confidence:.2f}</div>
  1586. <div><strong>引用帖子:</strong> {len(cited_notes)} 个</div>
  1587. </div>
  1588. </div>
  1589. {f'<p style="margin-top: 15px; color: #666;"><strong>摘要:</strong> {summary}</p>' if summary else ''}
  1590. {cited_section}
  1591. </div>
  1592. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1593. </div>
  1594. """
  1595. return html
  1596. def render_final_result(step):
  1597. """渲染最终结果步骤"""
  1598. data = step.get("data", {})
  1599. success = data.get("success", False)
  1600. message = data.get("message", "")
  1601. satisfied_notes_count = data.get("satisfied_notes_count", 0)
  1602. status_color = "#10b981" if success else "#ef4444"
  1603. status_text = "✅ 成功" if success else "❌ 失败"
  1604. html = f"""
  1605. <div class="step-section" style="border: 3px solid {status_color};">
  1606. <div class="step-header">
  1607. <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
  1608. <div class="step-type">{step['step_type']}</div>
  1609. </div>
  1610. <div class="step-content">
  1611. <div class="info-grid">
  1612. <div class="info-item" style="background: {status_color}20;">
  1613. <div class="info-label">状态</div>
  1614. <div class="info-value" style="color: {status_color};">{status_text}</div>
  1615. </div>
  1616. <div class="info-item">
  1617. <div class="info-label">满足需求的帖子</div>
  1618. <div class="info-value">{satisfied_notes_count}</div>
  1619. </div>
  1620. </div>
  1621. <p style="margin-top: 20px; font-size: 15px; color: #666;">{message}</p>
  1622. </div>
  1623. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1624. </div>
  1625. """
  1626. return html
  1627. def render_query_suggestion_evaluation(step):
  1628. """渲染候选query推荐词评估步骤"""
  1629. data = step.get("data", {})
  1630. candidate_count = data.get("candidate_count", 0)
  1631. results = data.get("results", [])
  1632. results_html = ""
  1633. step_num = step['step_number']
  1634. for idx, result in enumerate(results):
  1635. candidate = result.get("candidate", "")
  1636. suggestions = result.get("suggestions", [])
  1637. evaluations = result.get("evaluations", [])
  1638. # 渲染每个候选词的推荐词评估
  1639. eval_cards = ""
  1640. for evaluation in evaluations:
  1641. query = evaluation.get("query", "")
  1642. intent_match = evaluation.get("intent_match", False)
  1643. relevance_score = evaluation.get("relevance_score", 0)
  1644. reason = evaluation.get("reason", "")
  1645. intent_badge = "✅ 意图匹配" if intent_match else "❌ 意图不匹配"
  1646. intent_class = "confidence-high" if intent_match else "confidence-low"
  1647. eval_cards += f"""
  1648. <div class="query-item" style="margin: 10px 0; padding: 15px; background: white; border: 1px solid #e5e7eb; border-radius: 8px;">
  1649. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
  1650. <div class="query-text" style="flex: 1;">{query}</div>
  1651. <div style="display: flex; gap: 10px; align-items: center;">
  1652. <span class="confidence-badge {intent_class}" style="margin: 0;">{intent_badge}</span>
  1653. <span class="confidence-badge confidence-medium" style="margin: 0;">相关性: {relevance_score:.2f}</span>
  1654. </div>
  1655. </div>
  1656. <div style="color: #666; font-size: 13px; line-height: 1.6; background: #f8f9fa; padding: 10px; border-radius: 4px;">
  1657. {reason}
  1658. </div>
  1659. </div>
  1660. """
  1661. if eval_cards:
  1662. # 使用可折叠区域包装每个候选词的推荐词列表,添加唯一ID
  1663. results_html += make_collapsible(
  1664. f"候选词: {candidate} ({len(evaluations)} 个推荐词)",
  1665. eval_cards,
  1666. collapsed=True,
  1667. section_id=f"step{step_num}-candidate-{idx}"
  1668. )
  1669. html = f"""
  1670. <div class="step-section">
  1671. <div class="step-header">
  1672. <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
  1673. <div class="step-type">{step['step_type']}</div>
  1674. </div>
  1675. <div class="step-content">
  1676. <div class="info-grid">
  1677. <div class="info-item">
  1678. <div class="info-label">候选query数</div>
  1679. <div class="info-value">{candidate_count}</div>
  1680. </div>
  1681. <div class="info-item">
  1682. <div class="info-label">总推荐词数</div>
  1683. <div class="info-value">{sum(len(r.get('evaluations', [])) for r in results)}</div>
  1684. </div>
  1685. </div>
  1686. {results_html}
  1687. </div>
  1688. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1689. </div>
  1690. """
  1691. return html
  1692. def render_filter_qualified_queries(step):
  1693. """渲染筛选合格推荐词步骤"""
  1694. data = step.get("data", {})
  1695. input_count = data.get("input_evaluation_count", 0)
  1696. qualified_count = data.get("qualified_count", 0)
  1697. min_relevance = data.get("min_relevance_score", 0.7)
  1698. all_queries = data.get("all_queries", [])
  1699. # 如果没有all_queries,使用旧的qualified_queries
  1700. if not all_queries:
  1701. all_queries = data.get("qualified_queries", [])
  1702. # 分离合格和不合格的查询
  1703. qualified_html = ""
  1704. unqualified_html = ""
  1705. for item in all_queries:
  1706. query = item.get("query", "")
  1707. from_candidate = item.get("from_candidate", "")
  1708. intent_match = item.get("intent_match", False)
  1709. relevance_score = item.get("relevance_score", 0)
  1710. reason = item.get("reason", "")
  1711. is_qualified = item.get("is_qualified", True) # 默认为True以兼容旧数据
  1712. intent_badge = "✅ 意图匹配" if intent_match else "❌ 意图不匹配"
  1713. intent_class = "confidence-high" if intent_match else "confidence-low"
  1714. # 根据相关性分数确定badge颜色
  1715. if relevance_score >= 0.8:
  1716. score_class = "confidence-high"
  1717. elif relevance_score >= 0.6:
  1718. score_class = "confidence-medium"
  1719. else:
  1720. score_class = "confidence-low"
  1721. # 确定边框颜色和背景色
  1722. if is_qualified:
  1723. border_color = "#10b981"
  1724. bg_color = "#f0fdf4"
  1725. border_left_color = "#10b981"
  1726. else:
  1727. border_color = "#e5e7eb"
  1728. bg_color = "#f9fafb"
  1729. border_left_color = "#9ca3af"
  1730. query_html = f"""
  1731. <div class="query-item" style="margin: 15px 0; padding: 15px; background: white; border: 2px solid {border_color}; border-radius: 8px;">
  1732. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
  1733. <div style="flex: 1;">
  1734. <div class="query-text">{query}</div>
  1735. <div style="color: #9ca3af; font-size: 12px; margin-top: 5px;">来自候选词: {from_candidate}</div>
  1736. </div>
  1737. <div style="display: flex; gap: 10px; align-items: center;">
  1738. <span class="confidence-badge {intent_class}" style="margin: 0;">{intent_badge}</span>
  1739. <span class="confidence-badge {score_class}" style="margin: 0;">相关性: {relevance_score:.2f}</span>
  1740. </div>
  1741. </div>
  1742. <div style="color: #666; font-size: 13px; line-height: 1.6; background: {bg_color}; padding: 10px; border-radius: 4px; border-left: 3px solid {border_left_color};">
  1743. {reason}
  1744. </div>
  1745. </div>
  1746. """
  1747. if is_qualified:
  1748. qualified_html += query_html
  1749. else:
  1750. unqualified_html += query_html
  1751. # 构建HTML - 使用可折叠区域
  1752. step_num = step['step_number']
  1753. qualified_section = make_collapsible(
  1754. f"✅ 合格的推荐词 ({qualified_count})",
  1755. qualified_html,
  1756. collapsed=True,
  1757. section_id=f"step{step_num}-qualified"
  1758. ) if qualified_html else ''
  1759. unqualified_section = make_collapsible(
  1760. f"❌ 不合格的推荐词 ({input_count - qualified_count})",
  1761. unqualified_html,
  1762. collapsed=True,
  1763. section_id=f"step{step_num}-unqualified"
  1764. ) if unqualified_html else ''
  1765. html = f"""
  1766. <div class="step-section">
  1767. <div class="step-header">
  1768. <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
  1769. <div class="step-type">{step['step_type']}</div>
  1770. </div>
  1771. <div class="step-content">
  1772. <div class="info-grid">
  1773. <div class="info-item">
  1774. <div class="info-label">输入推荐词数</div>
  1775. <div class="info-value">{input_count}</div>
  1776. </div>
  1777. <div class="info-item">
  1778. <div class="info-label">合格推荐词数</div>
  1779. <div class="info-value">{qualified_count}</div>
  1780. </div>
  1781. <div class="info-item">
  1782. <div class="info-label">最低相关性</div>
  1783. <div class="info-value">{min_relevance:.2f}</div>
  1784. </div>
  1785. </div>
  1786. {qualified_section}
  1787. {unqualified_section}
  1788. </div>
  1789. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1790. </div>
  1791. """
  1792. return html
  1793. def render_generic_step(step):
  1794. """通用步骤渲染"""
  1795. data = step.get("data", {})
  1796. # 提取数据的简单展示
  1797. data_html = ""
  1798. if data:
  1799. data_html = "<div class='step-content'><pre style='background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px;'>"
  1800. import json
  1801. data_html += json.dumps(data, ensure_ascii=False, indent=2)[:500] # 限制长度
  1802. if len(json.dumps(data)) > 500:
  1803. data_html += "\n..."
  1804. data_html += "</pre></div>"
  1805. return f"""
  1806. <div class="step-section">
  1807. <div class="step-header">
  1808. <div class="step-title">步骤 {step['step_number']}: {step['step_name']}</div>
  1809. <div class="step-type">{step['step_type']}</div>
  1810. </div>
  1811. {data_html}
  1812. <div class="timestamp">⏰ {step.get('timestamp', '')}</div>
  1813. </div>
  1814. """
  1815. def render_step(step):
  1816. """根据步骤类型渲染对应的HTML"""
  1817. step_type = step.get("step_type", "")
  1818. renderers = {
  1819. "keyword_extraction": render_keyword_extraction,
  1820. "level_exploration": render_level_exploration,
  1821. "level_analysis": render_level_analysis,
  1822. "query_suggestion_evaluation": render_query_suggestion_evaluation,
  1823. "filter_qualified_queries": render_filter_qualified_queries,
  1824. "search_qualified_queries": render_search_results,
  1825. "evaluate_search_notes": render_note_evaluations,
  1826. "answer_generation": render_answer_generation,
  1827. "final_result": render_final_result,
  1828. }
  1829. renderer = renderers.get(step_type)
  1830. if renderer:
  1831. return renderer(step)
  1832. else:
  1833. # 使用通用渲染显示数据
  1834. return render_generic_step(step)
  1835. def generate_html(steps_json_path, output_path=None):
  1836. """生成HTML可视化文件"""
  1837. # 读取 steps.json
  1838. with open(steps_json_path, 'r', encoding='utf-8') as f:
  1839. steps_data = json.load(f)
  1840. # 生成内容
  1841. content_parts = [render_header(steps_data)]
  1842. for step in steps_data:
  1843. content_parts.append(render_step(step))
  1844. content = "\n".join(content_parts)
  1845. # 生成最终HTML(使用replace而不是format来避免CSS中的花括号问题)
  1846. html = HTML_TEMPLATE.replace("{content}", content)
  1847. # 确定输出路径
  1848. if output_path is None:
  1849. steps_path = Path(steps_json_path)
  1850. output_path = steps_path.parent / "steps_visualization.html"
  1851. # 写入文件
  1852. with open(output_path, 'w', encoding='utf-8') as f:
  1853. f.write(html)
  1854. return output_path
  1855. def main():
  1856. parser = argparse.ArgumentParser(description="Steps 可视化工具")
  1857. parser.add_argument("steps_json", type=str, help="steps.json 文件路径")
  1858. parser.add_argument("-o", "--output", type=str, help="输出HTML文件路径(可选)")
  1859. args = parser.parse_args()
  1860. # 生成可视化
  1861. output_path = generate_html(args.steps_json, args.output)
  1862. print(f"✅ 可视化生成成功!")
  1863. print(f"📄 输出文件: {output_path}")
  1864. output_abs = Path(output_path).absolute() if isinstance(output_path, str) else output_path.absolute()
  1865. print(f"\n💡 在浏览器中打开查看: file://{output_abs}")
  1866. if __name__ == "__main__":
  1867. main()