viz_capability.html 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. <!doctype html>
  2. <html lang="zh">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>能力 · 五层筛选</title>
  6. <style>
  7. :root{
  8. --bg:#f8fafc; --panel:#ffffff; --panel2:#f1f5f9; --border:#e2e8f0;
  9. --fg:#0f172a; --muted:#64748b; --accent:#3b82f6; --accent2:#8b5cf6;
  10. --tag-bg:#e2e8f0; --tag-bg-active:#3b82f6; --tag-fg-active:#fff;
  11. --code:#f1f5f9; --warn:#eab308; --suggest:#8b5cf6; --inferred:#94a3b8;
  12. --shi-bg:#dbeafe; --shi-fg:#1e40af;
  13. --xing-bg:#ede9fe; --xing-fg:#6d28d9;
  14. --both-bg:#fef3c7; --both-fg:#92400e;
  15. }
  16. *{box-sizing:border-box}
  17. html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);
  18. font:13px/1.55 -apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",sans-serif}
  19. header{display:flex;align-items:center;gap:12px;padding:8px 14px;
  20. border-bottom:1px solid var(--border);background:var(--panel);
  21. position:sticky;top:0;z-index:5;flex-wrap:wrap}
  22. h1{font-size:14px;margin:0;font-weight:600}
  23. .stats{color:var(--muted);font-size:11px}
  24. .clear-all{margin-left:auto;font-size:11px;color:var(--muted);cursor:pointer;
  25. padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:transparent}
  26. .clear-all:hover{color:var(--fg);border-color:var(--accent)}
  27. .clear-all.active{color:var(--warn);border-color:var(--warn)}
  28. main{display:grid;grid-template-columns:160px 360px 220px 360px 1fr;
  29. height:calc(100vh - 41px);min-width:1520px}
  30. section{overflow:auto;border-right:1px solid var(--border);padding:8px 10px}
  31. section:last-child{border-right:none}
  32. section.fac1{background:var(--panel)}
  33. section.fac2{background:#fafbfd}
  34. section.fac3{background:var(--panel)}
  35. section.fac4{background:#fafbfd}
  36. section.detail{background:var(--panel)}
  37. .col-title{font-size:10px;text-transform:uppercase;letter-spacing:.6px;
  38. color:var(--muted);margin:2px 4px 8px;display:flex;justify-content:space-between;align-items:baseline}
  39. .col-count{font-size:10px;color:var(--muted)}
  40. .col-clear{font-size:10px;color:var(--accent);cursor:pointer;display:none}
  41. .col-clear.show{display:inline}
  42. .item{padding:6px 8px;border-radius:5px;cursor:pointer;margin-bottom:3px;
  43. border:1px solid transparent;display:flex;justify-content:space-between;align-items:center;gap:6px}
  44. .item:hover:not(.disabled):not(.active){background:var(--panel2)}
  45. .item.active{background:var(--tag-bg-active);color:var(--tag-fg-active);border-color:var(--tag-bg-active)}
  46. .item.disabled{opacity:.42;cursor:not-allowed}
  47. .item-name{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  48. .item-count{font-size:10px;background:var(--bg);padding:1px 5px;border-radius:8px;
  49. color:var(--muted);min-width:20px;text-align:center;flex-shrink:0}
  50. .item.active .item-count{background:rgba(255,255,255,.3);color:#fff}
  51. .item-def{display:block;font-size:10px;color:var(--muted);margin-top:2px;line-height:1.4}
  52. .item.active .item-def{color:rgba(255,255,255,.85)}
  53. .item-stack{flex:1;min-width:0}
  54. /* col-2 tabs */
  55. .scope-tabs{display:flex;gap:4px;margin:0 0 8px;border-bottom:1px solid var(--border);padding-bottom:5px}
  56. .scope-tab{padding:4px 10px;border-radius:5px 5px 0 0;cursor:pointer;
  57. font-size:12px;color:var(--muted);background:transparent;
  58. border:1px solid transparent;border-bottom:none;user-select:none}
  59. .scope-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
  60. .scope-tab:hover:not(.active){color:var(--fg)}
  61. .scope-pane{display:none}
  62. .scope-pane.active{display:block}
  63. .scope-meta{font-size:10px;color:var(--muted);margin:0 4px 8px;line-height:1.4}
  64. /* facets */
  65. .facet-title{margin-top:10px;padding:4px 6px;font-size:11px;color:var(--muted);
  66. font-weight:600;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.5px}
  67. .facet-title:first-child{margin-top:0}
  68. /* itemsets */
  69. .iset{padding:8px 10px;border-radius:5px;cursor:pointer;margin-bottom:5px;
  70. border:1px solid var(--border);background:var(--panel)}
  71. .iset:hover:not(.disabled):not(.active){border-color:var(--accent)}
  72. .iset.active{border-color:var(--accent);background:#eff6ff;
  73. box-shadow:0 0 0 1px var(--accent) inset}
  74. .iset.disabled{opacity:.42;cursor:not-allowed}
  75. .iset-head{display:flex;align-items:center;gap:6px;margin-bottom:5px}
  76. .iset-sup{background:var(--accent);color:#fff;padding:1px 7px;border-radius:3px;
  77. font-weight:700;font-size:11px}
  78. .iset-k{background:var(--tag-bg);color:var(--fg);padding:1px 6px;border-radius:3px;
  79. font-size:10px;letter-spacing:.3px}
  80. .iset-meta{font-size:10px;color:var(--muted);margin-left:auto}
  81. .iset-paths{display:flex;flex-direction:column;gap:3px}
  82. .iset-path{display:flex;gap:5px;font-size:11px;line-height:1.4;align-items:flex-start}
  83. .iset-fdot{width:18px;flex-shrink:0;font-size:9px;font-weight:700;text-align:center;
  84. border-radius:3px;line-height:16px;height:16px;letter-spacing:0;margin-top:1px}
  85. .iset-fdot.shi{background:var(--shi-bg);color:var(--shi-fg)}
  86. .iset-fdot.xing{background:var(--xing-bg);color:var(--xing-fg)}
  87. .iset-fdot.both{background:var(--both-bg);color:var(--both-fg)}
  88. .iset-leaf{font-weight:600;color:var(--fg);word-break:break-all}
  89. .iset-parent{color:var(--muted);font-size:10px;margin-top:1px;
  90. word-break:break-all;line-height:1.3;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  91. .iset-caret{width:14px;display:inline-block;text-align:center;color:var(--muted);
  92. cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
  93. .iset-caret:hover{color:var(--fg)}
  94. .iset-caret.invis{visibility:hidden}
  95. .iset-children{margin-left:14px;border-left:1px dashed var(--border);padding-left:8px;
  96. margin-top:-2px;margin-bottom:5px}
  97. .iset-childcnt{font-size:10px;color:var(--muted);margin-left:4px}
  98. /* tree */
  99. .tnode{margin-left:0}
  100. .tnode .tnode{margin-left:12px;border-left:1px dashed var(--border);padding-left:6px}
  101. .tline{display:flex;align-items:center;gap:4px;padding:3px 4px;border-radius:4px;
  102. cursor:pointer;font-size:12px;line-height:1.35}
  103. .tline:hover:not(.disabled):not(.active){background:var(--panel2)}
  104. .tline.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  105. .tline.disabled{opacity:.42;cursor:not-allowed}
  106. .tline.suggested .tname{color:var(--suggest);font-style:italic}
  107. .tline.suggested.active .tname{color:#fff}
  108. .tline.inferred .tname{color:var(--inferred);font-style:italic}
  109. .tline.inferred.active .tname{color:#fff}
  110. .tcaret{width:12px;display:inline-block;text-align:center;color:var(--muted);
  111. cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
  112. .tcaret.invis{visibility:hidden}
  113. .tname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  114. .tcount{font-size:10px;color:var(--muted);background:var(--bg);padding:1px 5px;border-radius:8px;flex-shrink:0}
  115. .tline.active .tcount{background:rgba(255,255,255,.3);color:#fff}
  116. .tcollapsed > .tnode{display:none}
  117. /* col-4 frag view tabs */
  118. .fv-tabs{display:flex;gap:4px;margin:0 0 6px;border-bottom:1px solid var(--border);padding-bottom:3px}
  119. .fv-tab{padding:3px 10px;border-radius:4px 4px 0 0;cursor:pointer;
  120. font-size:11px;color:var(--muted);background:transparent;
  121. border:1px solid transparent;border-bottom:none;user-select:none}
  122. .fv-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
  123. .fv-tab:hover:not(.active){color:var(--fg)}
  124. /* fragments */
  125. .frag{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
  126. border:1px solid var(--border);background:var(--panel)}
  127. .frag:hover:not(.active){background:var(--panel2)}
  128. .frag.active{border-color:var(--accent);background:#eff6ff}
  129. .frag-head{font-size:10px;color:var(--muted);margin-bottom:3px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
  130. .case-badge{background:var(--accent);color:#fff;padding:1px 5px;border-radius:3px;
  131. font-weight:600;font-size:10px}
  132. .frag-badge{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;
  133. font-weight:600;font-size:10px}
  134. .act-badge{background:var(--warn);color:#fff;padding:1px 5px;border-radius:3px;
  135. font-weight:600;font-size:10px}
  136. .side-badge{padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
  137. .side-badge.shi{background:var(--shi-bg);color:var(--shi-fg)}
  138. .side-badge.xing{background:var(--xing-bg);color:var(--xing-fg)}
  139. .src-badge{background:var(--suggest);color:#fff;padding:1px 5px;border-radius:3px;
  140. font-size:10px;font-weight:600}
  141. .frag-sig{font-size:11px;color:var(--muted);margin-top:2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  142. .frag-body{font-size:12px;color:#334155;margin-top:4px;line-height:1.45;
  143. display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  144. /* excerpt rows */
  145. .excerpt-row{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
  146. border:1px solid var(--border);background:var(--panel)}
  147. .excerpt-row:hover:not(.active){background:var(--panel2)}
  148. .excerpt-row.active{border-color:var(--accent);background:#eff6ff}
  149. .excerpt-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
  150. color:var(--accent);margin-top:4px;word-break:break-all;line-height:1.35}
  151. .excerpt-main{font-size:12.5px;color:var(--fg);margin-top:4px;line-height:1.5}
  152. .excerpt-note{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.45}
  153. .excerpt-body{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.45;
  154. display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  155. /* structured tab */
  156. .frag-excerpts{display:flex;flex-direction:column;gap:4px;margin-top:4px}
  157. .frag-excerpts .ex{font-size:12px;color:#334155;line-height:1.55;
  158. background:var(--code);padding:5px 8px;border-radius:4px;
  159. border-left:2px solid var(--accent2)}
  160. .frag-excerpts.empty{font-size:12px;color:var(--muted);font-style:italic;
  161. background:var(--code);padding:5px 8px;border-radius:4px}
  162. /* detail */
  163. .detail-empty{color:var(--muted);text-align:center;padding:60px 20px;font-size:13px}
  164. .detail h2{font-size:14px;margin:0 0 8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
  165. .detail-section{margin:12px 0}
  166. .detail-section h3{font-size:10px;color:var(--muted);text-transform:uppercase;
  167. letter-spacing:.6px;margin:0 0 6px;font-weight:600}
  168. .body-text{font-size:12px;line-height:1.6;color:#334155;background:var(--code);
  169. padding:8px 10px;border-radius:5px;white-space:pre-wrap}
  170. .effect-card{background:var(--code);border-radius:5px;padding:7px 9px;margin-bottom:5px;
  171. border-left:3px solid var(--accent2);font-size:12px}
  172. .effect-stmt{font-weight:600;margin-bottom:4px}
  173. .effect-meta{color:var(--muted);font-size:11px;margin-top:3px}
  174. .pill{display:inline-block;padding:1px 6px;background:var(--tag-bg);border-radius:8px;
  175. font-size:11px;margin-right:3px;margin-bottom:3px}
  176. .pill.in{background:var(--shi-bg);color:var(--shi-fg)}
  177. .pill.out{background:var(--xing-bg);color:var(--xing-fg)}
  178. .pill.cfg{background:var(--both-bg);color:var(--both-fg)}
  179. .io-row{margin-bottom:4px;font-size:11px}
  180. .io-row .lbl{color:var(--muted);font-size:10px;margin-right:6px;text-transform:uppercase}
  181. .pathline{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
  182. color:var(--accent);margin-bottom:2px;word-break:break-all}
  183. .pathline.suggest{color:var(--suggest)}
  184. .rationale{color:var(--muted);font-size:11px;margin-bottom:3px;margin-left:4px;line-height:1.45}
  185. .excerpt-line{color:var(--muted);font-size:11px;margin-left:4px;line-height:1.45;margin-bottom:6px;
  186. border-left:2px solid var(--border);padding-left:6px}
  187. .excerpt-line .em{color:#334155}
  188. .json-raw{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
  189. background:var(--code);padding:8px;border-radius:5px;white-space:pre-wrap;
  190. color:#334155;max-height:240px;overflow:auto}
  191. details summary{cursor:pointer;color:var(--muted);font-size:10px;margin:6px 0;user-select:none}
  192. details summary:hover{color:var(--fg)}
  193. /* case-detail (original-post) section in detail panel */
  194. .case-detail{margin:8px 0;padding-bottom:8px;border-bottom:1px solid var(--border)}
  195. .case-detail summary{font-weight:bold;color:var(--accent);padding:6px 8px;
  196. background:var(--panel2);border-radius:4px;font-size:11px;cursor:pointer;outline:none;margin:0}
  197. .case-content{padding:10px;background:var(--code);border-radius:5px;
  198. margin-top:4px;border:1px solid var(--border)}
  199. .case-images{display:flex;gap:8px;overflow-x:auto;margin-bottom:8px;padding-bottom:4px}
  200. .case-images img{height:160px;border-radius:5px;object-fit:contain;
  201. background:var(--panel2);border:1px solid var(--border)}
  202. .case-body{font-size:12px;line-height:1.6;color:var(--fg);white-space:pre-wrap;
  203. margin-bottom:10px;max-height:250px;overflow-y:auto;padding-right:4px}
  204. .case-link{font-size:11px;color:var(--accent);display:inline-block;
  205. background:var(--panel2);padding:4px 8px;border-radius:4px;text-decoration:none}
  206. .case-feedback{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;gap:12px;flex-wrap:wrap}
  207. .case-feedback .fb{display:inline-flex;gap:3px;align-items:center}
  208. .case-author{font-size:11px;color:var(--muted);margin-bottom:6px}
  209. .empty-msg{color:var(--muted);text-align:center;padding:30px 10px;font-size:11px;font-style:italic}
  210. /* modality side filter */
  211. .mod-filter{margin:0 4px 8px;padding:6px 6px 4px;background:var(--code);
  212. border:1px solid var(--border);border-radius:5px}
  213. .mod-filter-row{display:flex;align-items:center;gap:6px;margin-bottom:4px}
  214. .mod-filter-row:last-child{margin-bottom:0}
  215. .mod-filter-lbl{font-size:10px;color:var(--muted);min-width:42px;flex-shrink:0}
  216. .mod-chips{display:flex;gap:4px;flex-wrap:wrap}
  217. .mod-chip{font-size:11px;padding:1px 7px;background:var(--tag-bg);border-radius:8px;
  218. cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent}
  219. .mod-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
  220. .mod-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  221. .mod-chip.disabled{opacity:.42;cursor:not-allowed}
  222. /* selection summary chip */
  223. .filter-chip{font-size:11px;padding:3px 8px;background:var(--tag-bg);border-radius:10px;
  224. color:var(--fg);display:inline-flex;align-items:center;gap:6px;margin-right:4px}
  225. .filter-chip .x{color:var(--muted);cursor:pointer;font-weight:bold}
  226. .filter-chip .x:hover{color:var(--warn)}
  227. .filter-chip .lbl{color:var(--muted);font-size:9px;text-transform:uppercase}
  228. .loading{display:flex;align-items:center;justify-content:center;
  229. height:100vh;font-size:14px;color:var(--muted)}
  230. </style>
  231. </head>
  232. <body>
  233. <div id="loading" class="loading">加载能力数据中…</div>
  234. <div id="appRoot" style="display:none">
  235. <header>
  236. <h1>能力</h1>
  237. <span id="chips"></span>
  238. <button class="clear-all" id="clearAll">清除全部筛选</button>
  239. </header>
  240. <main>
  241. <section class="fac1" id="actCol">
  242. <div class="col-title"><span>① 动作</span><span class="col-count" id="actCnt"></span></div>
  243. <div id="actList"></div>
  244. </section>
  245. <section class="fac2" id="scopeCol">
  246. <div class="col-title">
  247. <span>② 作用域</span>
  248. <span class="col-count" id="scopeStat"></span>
  249. </div>
  250. <div class="scope-tabs">
  251. <div class="scope-tab" data-mode="node" id="tabNode">node · 内容树</div>
  252. <div class="scope-tab" data-mode="pattern" id="tabPattern">pattern · 频繁项集</div>
  253. <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
  254. </div>
  255. <div class="scope-pane" id="paneNode">
  256. <div class="facet-title">实质</div>
  257. <div id="shizhiTree"></div>
  258. <div class="facet-title">形式</div>
  259. <div id="xingshiTree"></div>
  260. </div>
  261. <div class="scope-pane" id="panePattern">
  262. <div class="scope-meta" id="isParams"></div>
  263. <div id="isList"></div>
  264. </div>
  265. </section>
  266. <section class="fac3" id="modCol">
  267. <div class="col-title"><span>③ 输入 → 输出模态</span><span class="col-clear" id="modClear">清除</span></div>
  268. <div class="mod-filter">
  269. <div class="mod-filter-row">
  270. <span class="mod-filter-lbl">输入含</span>
  271. <span class="mod-chips" id="modInChips"></span>
  272. </div>
  273. <div class="mod-filter-row">
  274. <span class="mod-filter-lbl">输出含</span>
  275. <span class="mod-chips" id="modOutChips"></span>
  276. </div>
  277. </div>
  278. <div id="modList"></div>
  279. </section>
  280. <section class="fac4" id="fragCol">
  281. <div class="col-title"><span id="fragColTitle">④ 原子能力</span><span class="col-count" id="fragCnt"></span></div>
  282. <div class="fv-tabs" id="fvTabs" style="display:none">
  283. <div class="fv-tab" data-view="raw" id="fvRaw">raw</div>
  284. <div class="fv-tab" data-view="structured" id="fvStructured">structured</div>
  285. </div>
  286. <div id="fragList"></div>
  287. </section>
  288. <section class="detail" id="detailCol">
  289. <div class="col-title"><span>⑤ 能力详情</span></div>
  290. <div id="detailBody"><div class="detail-empty">点击左侧能力查看详情</div></div>
  291. </section>
  292. </main>
  293. </div>
  294. <script>
  295. let data = null;
  296. let casesByIndex = {};
  297. let SIDE_MODS = null;
  298. let shiDesc = null;
  299. let xingDesc = null;
  300. const fragPaths = new Map();
  301. const state = {
  302. action: null,
  303. scopeMode: 'node',
  304. shizhiPath: null,
  305. xingshiPath: null,
  306. itemsetIdx: null,
  307. patternView: 'raw',
  308. modSig: null,
  309. modInFilter: new Set(),
  310. modOutFilter: new Set(),
  311. fragKey: null,
  312. collapsed: new Set(),
  313. isetExpanded: new Set(),
  314. };
  315. function $(id){return document.getElementById(id)}
  316. function el(tag, attrs, ...kids){
  317. const e = document.createElement(tag);
  318. if (attrs) for (const k in attrs){
  319. if (k === 'class') e.className = attrs[k];
  320. else if (k === 'onClick') e.addEventListener('click', attrs[k]);
  321. else if (k.startsWith('data-')) e.setAttribute(k, attrs[k]);
  322. else if (k === 'title') e.title = attrs[k];
  323. else if (k === 'style') e.setAttribute('style', attrs[k]);
  324. else e[k] = attrs[k];
  325. }
  326. for (const kid of kids){
  327. if (kid == null || kid === false) continue;
  328. if (typeof kid === 'string') e.appendChild(document.createTextNode(kid));
  329. else e.appendChild(kid);
  330. }
  331. return e;
  332. }
  333. const fragKey = f => f.case_index + ':' + f.fragment_id;
  334. function sigParts(sig){
  335. const [inS, outS] = sig.split(' → ');
  336. const parse = s => new Set(s.split('+').filter(x => x && x !== '∅'));
  337. return { in: parse(inS), out: parse(outS) };
  338. }
  339. function buildDescMap(roots){
  340. const map = new Map();
  341. function walk(node){
  342. const all = [node.path];
  343. for (const c of node.children || []){
  344. const sub = walk(c);
  345. for (const p of sub) all.push(p);
  346. }
  347. map.set(node.path, all);
  348. return all;
  349. }
  350. for (const r of roots) walk(r);
  351. return map;
  352. }
  353. function fragPathSet(f){
  354. const s = new Set();
  355. for (const e of (f.apply_shizhi || [])) if (e.category_path) s.add(e.category_path);
  356. for (const e of (f.apply_xingshi || [])) if (e.category_path) s.add(e.category_path);
  357. return s;
  358. }
  359. function fragMatchesPath(f, key, selPath, descMap){
  360. if (!selPath) return true;
  361. const allowed = new Set(descMap.get(selPath) || [selPath]);
  362. const list = (key === 'shizhi') ? f.apply_shizhi : f.apply_xingshi;
  363. for (const e of (list || [])){
  364. if (allowed.has(e.category_path)) return true;
  365. }
  366. return false;
  367. }
  368. function fragMatchesItemset(f, itemset){
  369. if (!itemset) return true;
  370. const ps = fragPaths.get(fragKey(f));
  371. for (const p of itemset.items) if (!ps.has(p)) return false;
  372. return true;
  373. }
  374. function scopeFilterActive(){
  375. if (state.scopeMode === 'node') return !!(state.shizhiPath || state.xingshiPath);
  376. if (state.scopeMode === 'pattern') return state.itemsetIdx != null;
  377. return false;
  378. }
  379. function applyFilters(except){
  380. return data.fragments.filter(f => {
  381. if (except !== 'action' && state.action && f.action !== state.action) return false;
  382. if (except !== 'scope' && scopeFilterActive()){
  383. if (state.scopeMode === 'node'){
  384. if (state.shizhiPath && !fragMatchesPath(f, 'shizhi', state.shizhiPath, shiDesc)) return false;
  385. if (state.xingshiPath && !fragMatchesPath(f, 'xingshi', state.xingshiPath, xingDesc)) return false;
  386. } else if (state.scopeMode === 'pattern'){
  387. const sel = data.itemsets[state.itemsetIdx];
  388. if (sel && !fragMatchesItemset(f, sel)) return false;
  389. }
  390. }
  391. if (except !== 'mod' && state.modSig && f.modality_signature !== state.modSig) return false;
  392. return true;
  393. });
  394. }
  395. function renderActions(){
  396. const baseFiltered = applyFilters('action');
  397. const counts = {};
  398. for (const f of baseFiltered) counts[f.action] = (counts[f.action]||0)+1;
  399. const wrap = $('actList'); wrap.innerHTML = '';
  400. for (const a of data.actions){
  401. const c = counts[a.verb] || 0;
  402. const node = el('div', {
  403. class: 'item' + (state.action === a.verb ? ' active' : '') + (c === 0 && state.action !== a.verb ? ' disabled' : ''),
  404. onClick: () => {
  405. if (c === 0 && state.action !== a.verb) return;
  406. state.action = (state.action === a.verb) ? null : a.verb;
  407. renderAll();
  408. }
  409. });
  410. const stack = el('div', {class:'item-stack'},
  411. el('div', {class:'item-name'}, a.verb),
  412. a.definition ? el('div', {class:'item-def'}, a.definition) : null);
  413. node.appendChild(stack);
  414. node.appendChild(el('span', {class:'item-count'}, String(c)));
  415. wrap.appendChild(node);
  416. }
  417. $('actCnt').textContent = '共 ' + data.actions.length + ' 个';
  418. }
  419. function renderTreeFacet(roots, key, mountId, selPath, descMap){
  420. const wrap = $(mountId); wrap.innerHTML = '';
  421. const baseFiltered = applyFilters('scope');
  422. const hitCounts = new Map();
  423. for (const f of baseFiltered){
  424. const list = (key === 'shizhi') ? f.apply_shizhi : f.apply_xingshi;
  425. const seen = new Set();
  426. for (const e of (list || [])){
  427. const parts = (e.category_path || '').split('/').filter(Boolean);
  428. for (let i = 1; i <= parts.length; i++){
  429. const p = '/' + parts.slice(0,i).join('/');
  430. if (seen.has(p)) continue;
  431. seen.add(p);
  432. }
  433. }
  434. for (const p of seen) hitCounts.set(p, (hitCounts.get(p)||0)+1);
  435. }
  436. function renderNode(node, parent){
  437. const c = hitCounts.get(node.path) || 0;
  438. const collapsed = state.collapsed.has(node.path);
  439. const hasChildren = (node.children || []).length > 0;
  440. const tnode = el('div', {class: 'tnode' + (collapsed ? ' tcollapsed' : '')});
  441. const cls = ['tline'];
  442. if (selPath === node.path) cls.push('active');
  443. if (c === 0 && selPath !== node.path) cls.push('disabled');
  444. if (node.is_suggested && !node.is_hit) cls.push('suggested');
  445. if (node.is_inferred) cls.push('inferred');
  446. const line = el('div', {class: cls.join(' '), title: node.path + (node.description ? '\n\n' + node.description : '')});
  447. const caret = el('span', {
  448. class: 'tcaret' + (hasChildren ? '' : ' invis'),
  449. onClick: (ev) => {
  450. ev.stopPropagation();
  451. if (state.collapsed.has(node.path)) state.collapsed.delete(node.path);
  452. else state.collapsed.add(node.path);
  453. renderTrees();
  454. }
  455. }, hasChildren ? (collapsed ? '▶' : '▼') : '·');
  456. line.appendChild(caret);
  457. let label = node.name;
  458. if (node.is_suggested && !node.is_hit) label += ' ✦';
  459. if (node.is_inferred) label += ' (推断)';
  460. line.appendChild(el('span', {class:'tname'}, label));
  461. line.appendChild(el('span', {class:'tcount'}, String(c)));
  462. line.addEventListener('click', () => {
  463. if (c === 0 && selPath !== node.path) return;
  464. const k = (key === 'shizhi') ? 'shizhiPath' : 'xingshiPath';
  465. state[k] = (state[k] === node.path) ? null : node.path;
  466. renderAll();
  467. });
  468. tnode.appendChild(line);
  469. for (const ch of node.children || []) renderNode(ch, tnode);
  470. parent.appendChild(tnode);
  471. }
  472. for (const r of roots) renderNode(r, wrap);
  473. }
  474. function renderTrees(){
  475. renderTreeFacet(data.subtree.shizhi || [], 'shizhi', 'shizhiTree', state.shizhiPath, shiDesc);
  476. renderTreeFacet(data.subtree.xingshi || [], 'xingshi', 'xingshiTree', state.xingshiPath, xingDesc);
  477. }
  478. function pathLeafAndParent(p){
  479. const parts = p.split('/').filter(Boolean);
  480. if (!parts.length) return {leaf:p, parent:''};
  481. return {leaf: parts[parts.length-1], parent: '/' + parts.slice(0,-1).join('/')};
  482. }
  483. function renderItemsets(){
  484. const wrap = $('isList'); wrap.innerHTML = '';
  485. const visibleIdxSet = new Set();
  486. data.itemsets.forEach((it, i) => { if (it.size >= 2) visibleIdxSet.add(i); });
  487. $('isParams').textContent = `${visibleIdxSet.size} closed · k≥2 · min_support=${data.itemsetsParams.min_support}, k≤${data.itemsetsParams.max_k}, leaf-only`;
  488. const baseFiltered = applyFilters('scope');
  489. const roots = [];
  490. for (const i of visibleIdxSet){
  491. const ps = (data.itemsetParents[i] || []).filter(p => visibleIdxSet.has(p));
  492. if (!ps.length) roots.push(i);
  493. }
  494. roots.sort((a, b) => {
  495. const A = data.itemsets[a], B = data.itemsets[b];
  496. return (B.support - A.support) || (B.size - A.size) || a - b;
  497. });
  498. for (const idx of roots){
  499. renderItemsetNode(idx, -1, wrap, baseFiltered, visibleIdxSet, new Set());
  500. }
  501. if (visibleIdxSet.size === 0){
  502. wrap.appendChild(el('div', {class:'empty-msg'}, '没有符合 k≥2 的项集'));
  503. }
  504. }
  505. function renderItemsetNode(idx, parentIdx, mountEl, baseFiltered, visibleIdxSet, ancestorSet){
  506. const iset = data.itemsets[idx];
  507. const isActive = (state.itemsetIdx === idx);
  508. const occKey = parentIdx + ':' + idx;
  509. const childIdxs = (data.itemsetChildren[idx] || []).filter(c => visibleIdxSet.has(c) && !ancestorSet.has(c));
  510. const isExpanded = state.isetExpanded.has(occKey);
  511. const supportNow = baseFiltered.filter(f => fragMatchesItemset(f, iset)).length;
  512. const cls = ['iset'];
  513. if (isActive) cls.push('active');
  514. if (supportNow === 0 && !isActive) cls.push('disabled');
  515. const node = el('div', {
  516. class: cls.join(' '),
  517. onClick: () => {
  518. if (supportNow === 0 && !isActive) return;
  519. state.itemsetIdx = isActive ? null : idx;
  520. renderAll();
  521. }
  522. });
  523. const caret = el('span', {
  524. class: 'iset-caret' + (childIdxs.length === 0 ? ' invis' : ''),
  525. title: childIdxs.length ? (isExpanded ? '折叠' : '展开 ' + childIdxs.length + ' 个超集') : '',
  526. onClick: (ev) => {
  527. ev.stopPropagation();
  528. if (!childIdxs.length) return;
  529. if (state.isetExpanded.has(occKey)) state.isetExpanded.delete(occKey);
  530. else state.isetExpanded.add(occKey);
  531. renderItemsets();
  532. }
  533. }, childIdxs.length ? (isExpanded ? '▼' : '▶') : '·');
  534. const head = el('div', {class:'iset-head'},
  535. caret,
  536. el('span', {class:'iset-sup', title:'当前筛选下的支持度'}, '×'+supportNow),
  537. el('span', {class:'iset-k'}, 'k='+iset.size),
  538. childIdxs.length ? el('span', {class:'iset-childcnt'}, '⊃'+childIdxs.length) : null,
  539. el('span', {class:'iset-meta'}, '原始 sup ' + iset.support)
  540. );
  541. node.appendChild(head);
  542. const paths = el('div', {class:'iset-paths'});
  543. for (const p of iset.items){
  544. const facet = data.pathToFacet[p] || '?';
  545. const dot =
  546. facet === '实质' ? el('span', {class:'iset-fdot shi', title:'实质'}, '实') :
  547. facet === '形式' ? el('span', {class:'iset-fdot xing', title:'形式'}, '形') :
  548. el('span', {class:'iset-fdot both', title:'两侧都有'}, '双');
  549. const lp = pathLeafAndParent(p);
  550. const row = el('div', {class:'iset-path', title: p},
  551. dot,
  552. el('div', null,
  553. el('span', {class:'iset-leaf'}, lp.leaf),
  554. el('div', {class:'iset-parent'}, lp.parent)
  555. )
  556. );
  557. paths.appendChild(row);
  558. }
  559. node.appendChild(paths);
  560. mountEl.appendChild(node);
  561. if (isExpanded && childIdxs.length){
  562. const childWrap = el('div', {class:'iset-children'});
  563. const newAnc = new Set(ancestorSet); newAnc.add(idx);
  564. const sortedChildren = [...childIdxs].sort((a, b) => {
  565. const A = data.itemsets[a], B = data.itemsets[b];
  566. return (B.support - A.support) || (B.size - A.size) || a - b;
  567. });
  568. for (const c of sortedChildren){
  569. renderItemsetNode(c, idx, childWrap, baseFiltered, visibleIdxSet, newAnc);
  570. }
  571. mountEl.appendChild(childWrap);
  572. }
  573. }
  574. function renderScope(){
  575. $('tabNode').classList.toggle('active', state.scopeMode === 'node');
  576. $('tabPattern').classList.toggle('active', state.scopeMode === 'pattern');
  577. $('paneNode').classList.toggle('active', state.scopeMode === 'node');
  578. $('panePattern').classList.toggle('active', state.scopeMode === 'pattern');
  579. if (state.scopeMode === 'node') {
  580. $('scopeStat').textContent = '路径树(双 facet)';
  581. } else {
  582. const k2 = data.itemsets.filter(i => i.size >= 2).length;
  583. $('scopeStat').textContent = `${k2} 项集 · k≥2`;
  584. }
  585. $('scopeClear').classList.toggle('show', scopeFilterActive());
  586. if (state.scopeMode === 'node') renderTrees();
  587. else renderItemsets();
  588. }
  589. function sigPassesChipFilter(sig){
  590. const p = sigParts(sig);
  591. for (const m of state.modInFilter) if (!p.in.has(m)) return false;
  592. for (const m of state.modOutFilter) if (!p.out.has(m)) return false;
  593. return true;
  594. }
  595. function renderModalityChips(){
  596. const renderSide = (mountId, palette, selectedSet, side) => {
  597. const wrap = $(mountId); wrap.innerHTML = '';
  598. for (const m of palette){
  599. const trial = new Set(selectedSet);
  600. if (!trial.has(m)) trial.add(m);
  601. let c = 0;
  602. for (const sig of data.modalities){
  603. const p = sigParts(sig.sig);
  604. const inOK = side === 'in'
  605. ? [...trial].every(x => p.in.has(x)) && [...state.modOutFilter].every(x => p.out.has(x))
  606. : [...state.modInFilter].every(x => p.in.has(x)) && [...trial].every(x => p.out.has(x));
  607. if (inOK) c++;
  608. }
  609. const isActive = selectedSet.has(m);
  610. const chip = el('span', {
  611. class: 'mod-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  612. title: m + (c === 0 && !isActive ? ' (无匹配)' : ''),
  613. onClick: () => {
  614. if (c === 0 && !isActive) return;
  615. if (selectedSet.has(m)) selectedSet.delete(m);
  616. else selectedSet.add(m);
  617. if (state.modSig && !sigPassesChipFilter(state.modSig)) state.modSig = null;
  618. renderAll();
  619. }
  620. }, m);
  621. wrap.appendChild(chip);
  622. }
  623. };
  624. renderSide('modInChips', SIDE_MODS.in, state.modInFilter, 'in');
  625. renderSide('modOutChips', SIDE_MODS.out, state.modOutFilter, 'out');
  626. }
  627. function renderModalities(){
  628. const baseFiltered = applyFilters('mod');
  629. const counts = {};
  630. for (const f of baseFiltered) counts[f.modality_signature] = (counts[f.modality_signature]||0)+1;
  631. renderModalityChips();
  632. const wrap = $('modList'); wrap.innerHTML = '';
  633. const visible = data.modalities.filter(m => sigPassesChipFilter(m.sig));
  634. const sorted = [...visible].sort((a,b) => (counts[b.sig]||0) - (counts[a.sig]||0));
  635. if (sorted.length === 0){
  636. wrap.appendChild(el('div', {class:'empty-msg'}, '当前 chip 筛选下没有匹配签名'));
  637. }
  638. for (const m of sorted){
  639. const c = counts[m.sig] || 0;
  640. const node = el('div', {
  641. class: 'item' + (state.modSig === m.sig ? ' active' : '') + (c === 0 && state.modSig !== m.sig ? ' disabled' : ''),
  642. onClick: () => {
  643. if (c === 0 && state.modSig !== m.sig) return;
  644. state.modSig = (state.modSig === m.sig) ? null : m.sig;
  645. renderAll();
  646. }
  647. },
  648. el('span', {class:'item-name'}, m.sig),
  649. el('span', {class:'item-count'}, String(c))
  650. );
  651. wrap.appendChild(node);
  652. }
  653. $('modClear').classList.toggle('show', !!state.modSig || state.modInFilter.size > 0 || state.modOutFilter.size > 0);
  654. }
  655. function makeFragBodyRow(f){
  656. const k = fragKey(f);
  657. const head = el('div', {class:'frag-head'},
  658. el('span', {class:'case-badge'}, '案例'+f.case_index),
  659. el('span', {class:'frag-badge'}, f.fragment_id),
  660. el('span', {class:'act-badge'}, f.action)
  661. );
  662. const sig = el('div', {class:'frag-sig'}, f.modality_signature);
  663. const body = el('div', {class:'frag-body'}, f.body || '');
  664. return el('div', {
  665. class: 'frag' + (state.fragKey === k ? ' active' : ''),
  666. onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
  667. }, head, sig, body);
  668. }
  669. function makeExcerptRow(frag, side, entry){
  670. const k = fragKey(frag);
  671. const head = el('div', {class:'frag-head'},
  672. el('span', {class:'case-badge'}, '案例'+frag.case_index),
  673. el('span', {class:'frag-badge'}, frag.fragment_id),
  674. el('span', {class:'act-badge'}, frag.action),
  675. el('span', {class:'side-badge ' + (side==='实质'?'shi':'xing')}, side),
  676. entry.source === 'suggest' ? el('span', {class:'src-badge'}, '建议') : null
  677. );
  678. const node = el('div', {
  679. class: 'excerpt-row' + (state.fragKey === k ? ' active' : ''),
  680. onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
  681. });
  682. node.appendChild(head);
  683. node.appendChild(el('div', {class:'excerpt-path'}, entry.category_path));
  684. if (entry.body_excerpt){
  685. node.appendChild(el('div', {class:'excerpt-main'}, entry.body_excerpt));
  686. }
  687. if (entry.body_excerpt_note){
  688. node.appendChild(el('div', {class:'excerpt-note'}, entry.body_excerpt_note));
  689. }
  690. if (frag.body){
  691. node.appendChild(el('div', {class:'excerpt-body'}, frag.body));
  692. }
  693. return node;
  694. }
  695. function renderFragmentsNode(filtered, wrap){
  696. const hasNodeSel = !!(state.shizhiPath || state.xingshiPath);
  697. if (!hasNodeSel){
  698. $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
  699. if (!filtered.length){
  700. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
  701. return;
  702. }
  703. for (const f of filtered) wrap.appendChild(makeFragBodyRow(f));
  704. return;
  705. }
  706. const rows = [];
  707. for (const f of filtered){
  708. if (state.shizhiPath){
  709. const allowed = new Set(shiDesc.get(state.shizhiPath) || [state.shizhiPath]);
  710. for (const e of (f.apply_shizhi || [])){
  711. if (allowed.has(e.category_path)) rows.push({frag:f, side:'实质', entry:e});
  712. }
  713. }
  714. if (state.xingshiPath){
  715. const allowed = new Set(xingDesc.get(state.xingshiPath) || [state.xingshiPath]);
  716. for (const e of (f.apply_xingshi || [])){
  717. if (allowed.has(e.category_path)) rows.push({frag:f, side:'形式', entry:e});
  718. }
  719. }
  720. }
  721. $('fragCnt').textContent = rows.length + ' 条 · ' + filtered.length + ' 能力';
  722. if (!rows.length){
  723. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的 excerpt'));
  724. return;
  725. }
  726. for (const r of rows) wrap.appendChild(makeExcerptRow(r.frag, r.side, r.entry));
  727. }
  728. function renderFragmentsPatternRaw(filtered, wrap){
  729. $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
  730. if (!filtered.length){
  731. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
  732. return;
  733. }
  734. for (const f of filtered) wrap.appendChild(makeFragBodyRow(f));
  735. }
  736. function renderFragmentsPatternStructured(filtered, wrap){
  737. $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
  738. const sel = (state.itemsetIdx != null) ? data.itemsets[state.itemsetIdx] : null;
  739. if (!sel){
  740. wrap.appendChild(el('div', {class:'empty-msg'}, '请先在第二列选择一个项集'));
  741. return;
  742. }
  743. if (!filtered.length){
  744. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
  745. return;
  746. }
  747. const itemSet = new Set(sel.items);
  748. for (const f of filtered){
  749. const k = fragKey(f);
  750. const excerpts = [];
  751. const seen = new Set();
  752. const addFrom = (list) => {
  753. for (const e of (list || [])){
  754. if (!itemSet.has(e.category_path)) continue;
  755. if (!e.body_excerpt) continue;
  756. if (seen.has(e.body_excerpt)) continue;
  757. seen.add(e.body_excerpt);
  758. excerpts.push(e.body_excerpt);
  759. }
  760. };
  761. addFrom(f.apply_shizhi);
  762. addFrom(f.apply_xingshi);
  763. const head = el('div', {class:'frag-head'},
  764. el('span', {class:'case-badge'}, '案例'+f.case_index),
  765. el('span', {class:'frag-badge'}, f.fragment_id),
  766. el('span', {class:'act-badge'}, f.action)
  767. );
  768. const node = el('div', {
  769. class: 'frag' + (state.fragKey === k ? ' active' : ''),
  770. onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
  771. });
  772. node.appendChild(head);
  773. if (excerpts.length){
  774. const wrapEx = el('div', {class:'frag-excerpts'});
  775. for (const ex of excerpts) wrapEx.appendChild(el('div', {class:'ex'}, ex));
  776. node.appendChild(wrapEx);
  777. } else {
  778. node.appendChild(el('div', {class:'frag-excerpts empty'}, '(本能力无 excerpt 命中该项集)'));
  779. }
  780. wrap.appendChild(node);
  781. }
  782. }
  783. function renderFragments(){
  784. const filtered = applyFilters(null);
  785. const wrap = $('fragList'); wrap.innerHTML = '';
  786. $('fragColTitle').textContent = '④ ' + (state.scopeMode === 'pattern' ? '分子能力' : '原子能力');
  787. $('fvTabs').style.display = (state.scopeMode === 'pattern') ? 'flex' : 'none';
  788. if (state.scopeMode === 'pattern'){
  789. $('fvRaw').classList.toggle('active', state.patternView === 'raw');
  790. $('fvStructured').classList.toggle('active', state.patternView === 'structured');
  791. }
  792. if (state.scopeMode === 'node'){
  793. renderFragmentsNode(filtered, wrap);
  794. } else {
  795. if (state.patternView === 'structured') renderFragmentsPatternStructured(filtered, wrap);
  796. else renderFragmentsPatternRaw(filtered, wrap);
  797. }
  798. }
  799. function renderCaseDetail(wrap, caseIdx){
  800. const c = casesByIndex[caseIdx];
  801. if (!c) return;
  802. const det = el('details', {class:'case-detail'});
  803. const sumLabel = '📄 源: ' + (c.title || '案例 ' + caseIdx);
  804. det.appendChild(el('summary', null, sumLabel));
  805. const content = el('div', {class:'case-content'});
  806. if (c.author) {
  807. content.appendChild(el('div', {class:'case-author'}, '作者:' + c.author));
  808. }
  809. if (c.feedback && (c.feedback.like_count || c.feedback.comment_count || c.feedback.collect_count || c.feedback.share_count)){
  810. const fb = c.feedback;
  811. const row = el('div', {class:'case-feedback'});
  812. if (fb.like_count != null) row.appendChild(el('span', {class:'fb'}, '👍 ' + fb.like_count));
  813. if (fb.comment_count != null) row.appendChild(el('span', {class:'fb'}, '💬 ' + fb.comment_count));
  814. if (fb.collect_count != null) row.appendChild(el('span', {class:'fb'}, '⭐ ' + fb.collect_count));
  815. if (fb.share_count != null) row.appendChild(el('span', {class:'fb'}, '↗ ' + fb.share_count));
  816. content.appendChild(row);
  817. }
  818. const imgs = (c.images && c.images.length) ? c.images : (c.cover ? [c.cover] : []);
  819. if (imgs.length){
  820. const wrapImg = el('div', {class:'case-images'});
  821. for (const src of imgs) wrapImg.appendChild(el('img', {src, loading:'lazy'}));
  822. content.appendChild(wrapImg);
  823. }
  824. if (c.body){
  825. content.appendChild(el('div', {class:'case-body'}, c.body));
  826. }
  827. if (c.url){
  828. content.appendChild(el('a', {href:c.url, target:'_blank', rel:'noopener', class:'case-link'}, '🔗 访问原始链接'));
  829. }
  830. det.appendChild(content);
  831. wrap.appendChild(det);
  832. }
  833. function renderDetail(){
  834. const wrap = $('detailBody'); wrap.innerHTML = '';
  835. const f = data.fragments.find(x => fragKey(x) === state.fragKey);
  836. if (!f){ wrap.appendChild(el('div', {class:'detail-empty'}, '点击左侧能力查看详情')); return; }
  837. wrap.appendChild(el('h2', null,
  838. el('span', {class:'case-badge'}, '案例 '+f.case_index),
  839. el('span', {class:'frag-badge'}, f.fragment_id),
  840. el('span', {class:'act-badge'}, f.action),
  841. f.workflow_step_ref && f.workflow_step_ref.step_id ? el('span', {class:'pill'}, 'step '+f.workflow_step_ref.step_id) : null
  842. ));
  843. renderCaseDetail(wrap, f.case_index);
  844. const ioSec = el('div', {class:'detail-section'}, el('h3', null, 'I/O 模态'));
  845. ioSec.appendChild(el('div', {class:'frag-sig', style:'font-size:13px;margin-bottom:6px'}, f.modality_signature));
  846. const ioRow = (lbl, arr, kind) => {
  847. const row = el('div', {class:'io-row'}, el('span', {class:'lbl'}, lbl));
  848. if (!arr || !arr.length){ row.appendChild(el('span', {style:'color:var(--muted)'}, '(无)')); return row; }
  849. for (const x of arr){
  850. const isCfg = x.modality === '模型' || x.modality === '参数';
  851. const txt = (x.description||'') + (x.modality?'['+x.modality+']':'') + (x.relation?' '+x.relation:'');
  852. row.appendChild(el('span', {class:'pill ' + (isCfg ? 'cfg' : kind)}, txt));
  853. }
  854. return row;
  855. };
  856. ioSec.appendChild(ioRow('IN', f.inputs, 'in'));
  857. ioSec.appendChild(ioRow('OUT', f.outputs, 'out'));
  858. wrap.appendChild(ioSec);
  859. if (f.body){
  860. wrap.appendChild(el('div', {class:'detail-section'},
  861. el('h3', null, 'Body'),
  862. el('div', {class:'body-text'}, f.body)
  863. ));
  864. }
  865. let hotPaths = new Set();
  866. if (state.scopeMode === 'pattern' && state.itemsetIdx != null){
  867. const sel = data.itemsets[state.itemsetIdx];
  868. if (sel) for (const p of sel.items) hotPaths.add(p);
  869. }
  870. const renderApply = (label, arr) => {
  871. const sec = el('div', {class:'detail-section'}, el('h3', null, 'Apply to · ' + label));
  872. if (!arr || !arr.length){ sec.appendChild(el('div', {class:'empty-msg', style:'padding:8px'}, '无')); return sec; }
  873. for (const e of arr){
  874. let isHighlighted = false;
  875. if (state.scopeMode === 'node'){
  876. isHighlighted =
  877. (label === '实质' && state.shizhiPath && (shiDesc.get(state.shizhiPath)||[]).includes(e.category_path)) ||
  878. (label === '形式' && state.xingshiPath && (xingDesc.get(state.xingshiPath)||[]).includes(e.category_path));
  879. } else {
  880. isHighlighted = hotPaths.has(e.category_path);
  881. }
  882. const isSuggest = e.source === 'suggest';
  883. const pl = el('div', {class:'pathline' + (isSuggest ? ' suggest' : ''), style: isHighlighted ? 'color:#ca8a04;font-weight:600' : ''},
  884. e.category_path + (isSuggest ? ' ✦' : ''));
  885. sec.appendChild(pl);
  886. if (e.body_excerpt){
  887. sec.appendChild(el('div', {class:'excerpt-line'}, el('span', {class:'em'}, e.body_excerpt)));
  888. }
  889. if (e.body_excerpt_note){
  890. sec.appendChild(el('div', {class:'rationale'}, '— ' + e.body_excerpt_note));
  891. }
  892. if (e.rationale){
  893. sec.appendChild(el('div', {class:'rationale'}, '· ' + e.rationale));
  894. }
  895. }
  896. return sec;
  897. };
  898. wrap.appendChild(renderApply('实质', f.apply_shizhi));
  899. wrap.appendChild(renderApply('形式', f.apply_xingshi));
  900. if (f.effects && f.effects.length){
  901. const sec = el('div', {class:'detail-section'}, el('h3', null, 'Effects'));
  902. f.effects.forEach((e,i)=>{
  903. const card = el('div', {class:'effect-card'});
  904. card.appendChild(el('div', {class:'effect-stmt'}, '#'+i+' ' + (e.statement || '-')));
  905. if (e.criteria) card.appendChild(el('div', {class:'effect-meta'}, '判定标准:'+e.criteria));
  906. if (e.judge_method) card.appendChild(el('div', {class:'effect-meta'}, '判定方式:'+e.judge_method));
  907. if (e.negative_examples && e.negative_examples.length){
  908. const ne = el('div', {class:'effect-meta'}, '反例:');
  909. for (const n of e.negative_examples) ne.appendChild(el('div', {style:'margin-left:8px'}, '· '+n));
  910. card.appendChild(ne);
  911. }
  912. sec.appendChild(card);
  913. });
  914. wrap.appendChild(sec);
  915. }
  916. const miscRows = [];
  917. if (f.tools && f.tools.length) miscRows.push(['tools', f.tools.join(', ')]);
  918. if (f.artifact_type) miscRows.push(['artifact_type', f.artifact_type]);
  919. if (f.control_target && f.control_target.length) miscRows.push(['control_target', f.control_target.join(', ')]);
  920. if (f.is_alternative_to && f.is_alternative_to.length) miscRows.push(['alt_to', f.is_alternative_to.join(', ')]);
  921. if (miscRows.length){
  922. const sec = el('div', {class:'detail-section'}, el('h3', null, '其他'));
  923. for (const [k2,v2] of miscRows){
  924. const row = el('div', {style:'font-size:11px;margin-bottom:3px'},
  925. el('span', {style:'color:var(--muted);margin-right:8px'}, k2),
  926. el('span', null, v2)
  927. );
  928. sec.appendChild(row);
  929. }
  930. wrap.appendChild(sec);
  931. }
  932. wrap.appendChild(el('details', null,
  933. el('summary', null, '查看原始能力 JSON'),
  934. el('pre', {class:'json-raw'}, JSON.stringify(f, null, 2))
  935. ));
  936. }
  937. function renderChips(){
  938. const c = $('chips'); c.innerHTML = '';
  939. let any = false;
  940. const mk = (lbl, val, onX) => {
  941. any = true;
  942. c.appendChild(el('span', {class:'filter-chip'},
  943. el('span', {class:'lbl'}, lbl),
  944. el('span', null, val),
  945. el('span', {class:'x', onClick: () => { onX(); renderAll(); }}, '×')
  946. ));
  947. };
  948. if (state.action) mk('动作', state.action, () => { state.action = null; });
  949. if (state.scopeMode === 'node'){
  950. if (state.shizhiPath) mk('实质', state.shizhiPath.split('/').pop() || state.shizhiPath, () => { state.shizhiPath = null; });
  951. if (state.xingshiPath) mk('形式', state.xingshiPath.split('/').pop() || state.xingshiPath, () => { state.xingshiPath = null; });
  952. } else if (state.scopeMode === 'pattern' && state.itemsetIdx != null){
  953. const sel = data.itemsets[state.itemsetIdx];
  954. const desc = sel.items.map(p => p.split('/').pop()).join(' + ');
  955. mk('项集 k='+sel.size, desc, () => { state.itemsetIdx = null; });
  956. }
  957. if (state.modSig) mk('模态', state.modSig, () => { state.modSig = null; });
  958. $('clearAll').classList.toggle('active', any);
  959. }
  960. function renderStats(){}
  961. function renderAll(){
  962. renderActions();
  963. renderScope();
  964. renderModalities();
  965. renderFragments();
  966. renderDetail();
  967. renderChips();
  968. }
  969. function init(){
  970. for (const c of (data.cases || [])) casesByIndex[c.index] = c;
  971. SIDE_MODS = (() => {
  972. const inSet = new Set(), outSet = new Set();
  973. for (const m of data.modalities){
  974. const p = sigParts(m.sig);
  975. for (const x of p.in) inSet.add(x);
  976. for (const x of p.out) outSet.add(x);
  977. }
  978. return { in: [...inSet].sort(), out: [...outSet].sort() };
  979. })();
  980. shiDesc = buildDescMap(data.subtree.shizhi || []);
  981. xingDesc = buildDescMap(data.subtree.xingshi || []);
  982. for (const f of data.fragments) fragPaths.set(fragKey(f), fragPathSet(f));
  983. $('loading').style.display = 'none';
  984. $('appRoot').style.display = '';
  985. $('clearAll').addEventListener('click', () => {
  986. state.action = null; state.shizhiPath = null; state.xingshiPath = null;
  987. state.itemsetIdx = null; state.modSig = null;
  988. state.modInFilter.clear(); state.modOutFilter.clear();
  989. renderAll();
  990. });
  991. $('scopeClear').addEventListener('click', () => {
  992. if (state.scopeMode === 'node'){ state.shizhiPath = null; state.xingshiPath = null; }
  993. else { state.itemsetIdx = null; }
  994. renderAll();
  995. });
  996. $('tabNode').addEventListener('click', () => { state.scopeMode = 'node'; renderAll(); });
  997. $('tabPattern').addEventListener('click', () => { state.scopeMode = 'pattern'; renderAll(); });
  998. $('fvRaw').addEventListener('click', () => { state.patternView = 'raw'; renderFragments(); });
  999. $('fvStructured').addEventListener('click', () => { state.patternView = 'structured'; renderFragments(); });
  1000. $('modClear').addEventListener('click', () => {
  1001. state.modSig = null; state.modInFilter.clear(); state.modOutFilter.clear();
  1002. renderAll();
  1003. });
  1004. renderStats();
  1005. renderAll();
  1006. }
  1007. fetch('/api/viz/data/capability')
  1008. .then(r => {
  1009. if (!r.ok) throw new Error('fetch failed: ' + r.status);
  1010. return r.json();
  1011. })
  1012. .then(d => { data = d; init(); })
  1013. .catch(err => {
  1014. $('loading').textContent = '加载失败:' + err.message + '。请先上传数据:POST /api/viz/data/capability';
  1015. });
  1016. </script>
  1017. </body>
  1018. </html>