viz_workflow.html 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  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. --split-w:8px;
  16. --left-pct:60%;
  17. }
  18. *{box-sizing:border-box}
  19. html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);
  20. font:13px/1.55 -apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",sans-serif}
  21. header{display:flex;align-items:center;gap:12px;padding:8px 14px;
  22. border-bottom:1px solid var(--border);background:var(--panel);
  23. position:sticky;top:0;z-index:5;flex-wrap:wrap;height:41px}
  24. h1{font-size:14px;margin:0;font-weight:600}
  25. .clear-all{margin-left:auto;font-size:11px;color:var(--muted);cursor:pointer;
  26. padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:transparent}
  27. .clear-all:hover{color:var(--fg);border-color:var(--accent)}
  28. .clear-all.active{color:var(--warn);border-color:var(--warn)}
  29. /* two-panel layout with draggable splitter */
  30. .app{display:grid;
  31. grid-template-columns: minmax(0, var(--left-pct)) var(--split-w) minmax(0, 1fr);
  32. height:calc(100vh - 41px);
  33. overflow:hidden}
  34. .selection{overflow:hidden;display:flex;flex-direction:column;background:var(--panel);
  35. min-width:0}
  36. .splitter{background:var(--border);cursor:col-resize;position:relative;user-select:none}
  37. .splitter::before{content:"";position:absolute;left:50%;top:50%;
  38. transform:translate(-50%,-50%);width:2px;height:30px;background:var(--muted);
  39. border-radius:1px;opacity:.6}
  40. .splitter:hover{background:var(--accent)}
  41. .splitter:hover::before{background:#fff;opacity:1}
  42. .splitter.dragging{background:var(--accent)}
  43. .splitter.dragging::before{background:#fff;opacity:1}
  44. .detail-area{overflow:auto;background:var(--panel);padding:0}
  45. .preset-btns{display:flex;gap:6px;margin-right:10px}
  46. .preset-btn{font-size:11px;padding:3px 9px;border:1px solid var(--border);
  47. border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
  48. .preset-btn:hover{color:var(--fg);border-color:var(--accent)}
  49. .preset-btn.active{color:var(--accent);border-color:var(--accent);background:#eff6ff}
  50. /* selection 4-column grid */
  51. .sel-cols{display:grid;
  52. grid-template-columns:minmax(180px,1fr) minmax(220px,1.1fr) minmax(190px,1fr) minmax(280px,1.6fr);
  53. height:100%;overflow-x:auto;overflow-y:hidden}
  54. section{overflow:auto;border-right:1px solid var(--border);padding:8px 10px;min-width:0}
  55. section:last-child{border-right:none}
  56. section.fac1{background:var(--panel)}
  57. section.fac2{background:#fafbfd}
  58. section.fac3{background:var(--panel)}
  59. section.fac4{background:#fafbfd}
  60. .col-title{font-size:10px;text-transform:uppercase;letter-spacing:.6px;
  61. color:var(--muted);margin:2px 4px 8px;display:flex;justify-content:space-between;align-items:baseline}
  62. .col-count{font-size:10px;color:var(--muted)}
  63. .col-clear{font-size:10px;color:var(--accent);cursor:pointer;display:none}
  64. .col-clear.show{display:inline}
  65. .item{padding:6px 8px;border-radius:5px;cursor:pointer;margin-bottom:3px;
  66. border:1px solid transparent;display:flex;justify-content:space-between;align-items:center;gap:6px}
  67. .item:hover:not(.disabled):not(.active){background:var(--panel2)}
  68. .item.active{background:var(--tag-bg-active);color:var(--tag-fg-active);border-color:var(--tag-bg-active)}
  69. .item.disabled{opacity:.42;cursor:not-allowed}
  70. .item-name{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  71. .item-count{font-size:10px;background:var(--bg);padding:1px 5px;border-radius:8px;
  72. color:var(--muted);min-width:20px;text-align:center;flex-shrink:0}
  73. .item.active .item-count{background:rgba(255,255,255,.3);color:#fff}
  74. .item-def{display:block;font-size:10px;color:var(--muted);margin-top:2px;line-height:1.4}
  75. .item.active .item-def{color:rgba(255,255,255,.85)}
  76. .item-stack{flex:1;min-width:0}
  77. /* verb-filter (collapsible chip list inside col-1) */
  78. .verb-filter{margin:0 0 8px;border:1px solid var(--border);border-radius:5px;
  79. background:var(--code)}
  80. .verb-filter > summary{cursor:pointer;padding:6px 9px;font-size:11px;
  81. color:var(--muted);user-select:none;display:flex;align-items:center;gap:6px;outline:none}
  82. .verb-filter > summary:hover{color:var(--fg)}
  83. .verb-filter > summary .vf-count{margin-left:auto;font-size:10px;color:var(--accent)}
  84. .verb-filter[open] > summary{border-bottom:1px solid var(--border)}
  85. .verb-chips{display:flex;flex-wrap:wrap;gap:4px;padding:7px 7px 5px}
  86. .verb-chip{font-size:11px;padding:2px 8px;background:var(--tag-bg);border-radius:8px;
  87. cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent;
  88. display:inline-flex;align-items:center;gap:4px}
  89. .verb-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
  90. .verb-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  91. .verb-chip.disabled{opacity:.42;cursor:not-allowed}
  92. .verb-chip .cnt{font-size:9px;color:var(--muted);background:var(--bg);
  93. padding:0 4px;border-radius:6px}
  94. .verb-chip.active .cnt{background:rgba(255,255,255,.3);color:#fff}
  95. /* action sequence list */
  96. .actseq{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
  97. border:1px solid var(--border);background:var(--panel);font-size:11px;line-height:1.45}
  98. .actseq:hover:not(.disabled):not(.active){border-color:var(--accent)}
  99. .actseq.active{border-color:var(--accent);background:#eff6ff;
  100. box-shadow:0 0 0 1px var(--accent) inset}
  101. .actseq.disabled{opacity:.42;cursor:not-allowed}
  102. .actseq-count{float:right;font-size:10px;color:var(--muted);
  103. background:var(--bg);padding:1px 5px;border-radius:8px}
  104. .actseq.active .actseq-count{background:rgba(59,130,246,.15);color:var(--accent)}
  105. .actseq-chip{display:inline-block;background:var(--tag-bg);padding:1px 6px;
  106. border-radius:3px;font-size:10px;margin:1px 2px;font-weight:600;color:var(--fg)}
  107. .actseq.active .actseq-chip{background:#dbeafe;color:#1e40af}
  108. .actseq-sep{color:var(--muted);font-size:9px;margin:0 1px}
  109. /* tree */
  110. .tnode{margin-left:0}
  111. .tnode .tnode{margin-left:12px;border-left:1px dashed var(--border);padding-left:6px}
  112. .tline{display:flex;align-items:center;gap:4px;padding:3px 4px;border-radius:4px;
  113. cursor:pointer;font-size:12px;line-height:1.35}
  114. .tline:hover:not(.disabled):not(.active){background:var(--panel2)}
  115. .tline.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  116. .tline.disabled{opacity:.42;cursor:not-allowed}
  117. .tline.suggested .tname{color:var(--suggest);font-style:italic}
  118. .tline.suggested.active .tname{color:#fff}
  119. .tline.inferred .tname{color:var(--inferred);font-style:italic}
  120. .tline.inferred.active .tname{color:#fff}
  121. .tcaret{width:12px;display:inline-block;text-align:center;color:var(--muted);
  122. cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
  123. .tcaret.invis{visibility:hidden}
  124. .tname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  125. .tcount{font-size:10px;color:var(--muted);background:var(--bg);padding:1px 5px;border-radius:8px;flex-shrink:0}
  126. .tline.active .tcount{background:rgba(255,255,255,.3);color:#fff}
  127. .tcollapsed > .tnode{display:none}
  128. .facet-title{margin-top:10px;padding:4px 6px;font-size:11px;color:var(--muted);
  129. font-weight:600;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.5px}
  130. .facet-title:first-child{margin-top:0}
  131. /* workflow card */
  132. .wf{padding:9px 10px;border-radius:6px;cursor:pointer;margin-bottom:6px;
  133. border:1px solid var(--border);background:var(--panel)}
  134. .wf:hover:not(.active){background:var(--panel2)}
  135. .wf.active{border-color:var(--accent);background:#eff6ff;
  136. box-shadow:0 0 0 1px var(--accent) inset}
  137. .wf-head{display:flex;align-items:center;gap:6px;margin-bottom:5px;flex-wrap:wrap}
  138. .case-badge{background:var(--accent);color:#fff;padding:1px 5px;border-radius:3px;
  139. font-weight:600;font-size:10px}
  140. .wf-badge{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;
  141. font-weight:600;font-size:10px}
  142. .wf-meta{font-size:10px;color:var(--muted);margin-left:auto}
  143. .wf-sig{font-size:10px;color:var(--muted);margin:3px 0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  144. .wf-seq{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px;align-items:center}
  145. .wf-row{font-size:10px;margin-top:4px;display:flex;align-items:baseline;gap:6px;line-height:1.4}
  146. .wf-row .wf-lbl{color:var(--muted);text-transform:uppercase;letter-spacing:.4px;flex-shrink:0;font-size:9px;min-width:28px}
  147. .wf-row .wf-val{color:#334155;flex:1;min-width:0}
  148. .wf-scope-chip{display:inline-block;background:#e0e7ff;color:#3730a3;padding:1px 6px;
  149. border-radius:3px;font-size:10px;margin:1px 2px;font-weight:600}
  150. .wf-scope-chip.suggest{background:#ede9fe;color:#6d28d9;font-style:italic}
  151. .wf-scope-more{font-size:9px;color:var(--muted);margin-left:3px}
  152. .vchip{font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600;line-height:1.3;
  153. border:1px solid transparent;white-space:nowrap;background:var(--tag-bg);color:var(--fg)}
  154. .vsep{color:var(--muted);font-size:9px;line-height:1.3}
  155. /* modality side filter */
  156. .mod-filter{margin:0 4px 8px;padding:6px 6px 4px;background:var(--code);
  157. border:1px solid var(--border);border-radius:5px}
  158. .mod-filter-row{display:flex;align-items:center;gap:6px;margin-bottom:4px}
  159. .mod-filter-row:last-child{margin-bottom:0}
  160. .mod-filter-lbl{font-size:10px;color:var(--muted);min-width:42px;flex-shrink:0}
  161. .mod-chips{display:flex;gap:4px;flex-wrap:wrap}
  162. .mod-chip{font-size:11px;padding:1px 7px;background:var(--tag-bg);border-radius:8px;
  163. cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent}
  164. .mod-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
  165. .mod-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  166. .mod-chip.disabled{opacity:.42;cursor:not-allowed}
  167. /* detail */
  168. .detail-empty{color:var(--muted);text-align:center;padding:60px 20px;font-size:13px}
  169. .detail-pad{padding:14px 18px}
  170. .detail h2{font-size:14px;margin:0 0 8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
  171. .detail-section{margin:12px 0}
  172. .detail-section h3{font-size:10px;color:var(--muted);text-transform:uppercase;
  173. letter-spacing:.6px;margin:0 0 6px;font-weight:600}
  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. /* case-detail (original-post) section */
  179. .case-detail{margin:0 0 8px;padding-bottom:8px;border-bottom:1px solid var(--border)}
  180. .case-detail summary{font-weight:bold;color:var(--accent);padding:6px 8px;
  181. background:var(--panel2);border-radius:4px;font-size:11px;cursor:pointer;outline:none;margin:0}
  182. .case-content{padding:10px;background:var(--code);border-radius:5px;
  183. margin-top:4px;border:1px solid var(--border)}
  184. .case-images{display:flex;gap:8px;overflow-x:auto;margin-bottom:8px;padding-bottom:4px}
  185. .case-images img{height:160px;border-radius:5px;object-fit:contain;
  186. background:var(--panel2);border:1px solid var(--border)}
  187. .case-body{font-size:12px;line-height:1.6;color:var(--fg);white-space:pre-wrap;
  188. margin-bottom:10px;max-height:250px;overflow-y:auto;padding-right:4px}
  189. .case-link{font-size:11px;color:var(--accent);display:inline-block;
  190. background:var(--panel2);padding:4px 8px;border-radius:4px;text-decoration:none}
  191. .case-feedback{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;gap:12px;flex-wrap:wrap}
  192. .case-feedback .fb{display:inline-flex;gap:3px;align-items:center}
  193. .case-author{font-size:11px;color:var(--muted);margin-bottom:6px}
  194. /* capability table — light, compact, white-on-gray */
  195. .cap-table{width:100%;border-collapse:collapse;background:var(--panel);
  196. font-size:11px;border:1px solid var(--border);table-layout:auto}
  197. .cap-table th, .cap-table td{
  198. border:1px solid var(--border);padding:5px 8px;vertical-align:top;text-align:left;
  199. line-height:1.5;background:var(--panel)}
  200. .cap-table thead th{background:#f8fafc;font-weight:600;color:var(--muted);
  201. position:sticky;top:0;z-index:2;font-size:10px;text-transform:uppercase;
  202. letter-spacing:.4px;text-align:left}
  203. .cap-table tbody tr.cap-first td{border-top:1px solid var(--border)}
  204. .cap-table tbody tr.step-first td{border-top:2px solid var(--accent)}
  205. .cap-table tbody tr:first-child td{border-top:none}
  206. .cell-step{text-align:center;font-weight:600;color:#334155;font-size:12px;
  207. background:#fafbfd}
  208. .cell-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
  209. color:var(--muted);white-space:nowrap;background:#fafbfd}
  210. .cell-action{font-weight:600;color:#334155;background:#fafbfd}
  211. .cell-atom-action{color:#334155;font-weight:500}
  212. .cell-scope{color:#334155}
  213. .cell-scope.suggest{color:var(--suggest);font-style:italic}
  214. .cell-tool{font-size:10px;color:#475569;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  215. .cell-type{font-size:10px;color:var(--muted)}
  216. .cell-impl{color:#334155;max-width:260px;font-size:11px;line-height:1.5}
  217. .cell-io{background:#fafbfd}
  218. .cell-io .mod-pill{display:inline-block;padding:1px 7px;border-radius:3px;
  219. font-size:10px;margin:1px 2px;font-weight:600;
  220. background:#e0e7ff;color:#3730a3}
  221. .cell-io .mod-pill.output{background:#e0e7ff;color:#3730a3}
  222. .cell-dash{color:var(--inferred)}
  223. .scope-leaf{display:inline-block;padding:1px 6px;background:#e0e7ff;
  224. color:#3730a3;border-radius:3px;font-size:11px;font-weight:600}
  225. .scope-leaf.suggest{background:#ede9fe;color:#6d28d9}
  226. .scope-facet{font-size:9px;color:var(--muted);margin-left:5px;text-transform:uppercase;letter-spacing:.3px}
  227. .scope-path{font-size:9px;color:var(--muted);margin-top:3px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  228. .scope-mark{font-size:10px;color:var(--suggest);margin-left:3px}
  229. .empty-msg{color:var(--muted);text-align:center;padding:30px 10px;font-size:11px;font-style:italic}
  230. .filter-chip{font-size:11px;padding:3px 8px;background:var(--tag-bg);border-radius:10px;
  231. color:var(--fg);display:inline-flex;align-items:center;gap:6px;margin-right:4px}
  232. .filter-chip .x{color:var(--muted);cursor:pointer;font-weight:bold}
  233. .filter-chip .x:hover{color:var(--warn)}
  234. .filter-chip .lbl{color:var(--muted);font-size:9px;text-transform:uppercase}
  235. .loading{display:flex;align-items:center;justify-content:center;
  236. height:100vh;font-size:14px;color:var(--muted)}
  237. </style>
  238. </head>
  239. <body>
  240. <div id="loading" class="loading">加载工序数据中…</div>
  241. <div id="appRoot" style="display:none">
  242. <header>
  243. <h1>工序</h1>
  244. <span id="chips"></span>
  245. <div class="preset-btns" style="margin-left:auto">
  246. <button class="preset-btn" id="presetLeft" title="选择区为主">◧ 选择</button>
  247. <button class="preset-btn" id="presetEven" title="均分">⊟ 均分</button>
  248. <button class="preset-btn" id="presetRight" title="详情区为主">◨ 详情</button>
  249. </div>
  250. <button class="clear-all" id="clearAll">清除筛选</button>
  251. </header>
  252. <div class="app" id="app">
  253. <div class="selection">
  254. <div class="sel-cols">
  255. <section class="fac1" id="actCol">
  256. <div class="col-title">
  257. <span>① 动作</span>
  258. <span class="col-clear" id="actClear" style="margin-left:auto">清除</span>
  259. </div>
  260. <details class="verb-filter" id="verbFilter">
  261. <summary>
  262. <span>动词表</span>
  263. <span id="vfSummary" class="vf-count"></span>
  264. </summary>
  265. <div class="verb-chips" id="verbChips"></div>
  266. </details>
  267. <div id="actSeqList"></div>
  268. </section>
  269. <section class="fac2" id="scopeCol">
  270. <div class="col-title">
  271. <span>② 作用域</span>
  272. <span class="col-count" id="scopeStat"></span>
  273. <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
  274. </div>
  275. <div class="facet-title">实质</div>
  276. <div id="shizhiTree"></div>
  277. <div class="facet-title">形式</div>
  278. <div id="xingshiTree"></div>
  279. </section>
  280. <section class="fac3" id="modCol">
  281. <div class="col-title"><span>③ 输入 → 输出模态</span><span class="col-clear" id="modClear">清除</span></div>
  282. <div class="mod-filter">
  283. <div class="mod-filter-row">
  284. <span class="mod-filter-lbl">输入含</span>
  285. <span class="mod-chips" id="modInChips"></span>
  286. </div>
  287. <div class="mod-filter-row">
  288. <span class="mod-filter-lbl">输出含</span>
  289. <span class="mod-chips" id="modOutChips"></span>
  290. </div>
  291. </div>
  292. <div id="modList"></div>
  293. </section>
  294. <section class="fac4" id="wfCol">
  295. <div class="col-title"><span>④ 工序</span><span class="col-count" id="wfCnt"></span></div>
  296. <div id="wfList"></div>
  297. </section>
  298. </div>
  299. </div>
  300. <div class="splitter" id="splitter"></div>
  301. <div class="detail-area">
  302. <div class="detail-pad" id="detailBody">
  303. <div class="detail-empty">点击左侧工序查看详情</div>
  304. </div>
  305. </div>
  306. </div>
  307. </div>
  308. <script>
  309. let data = null;
  310. let casesByIndex = {};
  311. let SIDE_MODS = null;
  312. let shiDesc = null;
  313. let xingDesc = null;
  314. const fragByKey = new Map();
  315. const state = {
  316. actionSeqSig: null, // selected action-sequence signature
  317. actionsSelected: new Set(),// selected verbs (AND filter)
  318. shizhiPath: null,
  319. xingshiPath: null,
  320. modSig: null,
  321. modInFilter: new Set(),
  322. modOutFilter: new Set(),
  323. wfKey: null,
  324. collapsed: new Set(),
  325. };
  326. const PHASE_IGNORE = new Set(["非制作"]);
  327. function $(id){return document.getElementById(id)}
  328. function el(tag, attrs, ...kids){
  329. const e = document.createElement(tag);
  330. if (attrs) for (const k in attrs){
  331. if (k === 'class') e.className = attrs[k];
  332. else if (k === 'onClick') e.addEventListener('click', attrs[k]);
  333. else if (k.startsWith('data-')) e.setAttribute(k, attrs[k]);
  334. else if (k === 'title') e.title = attrs[k];
  335. else if (k === 'style') e.setAttribute('style', attrs[k]);
  336. else e[k] = attrs[k];
  337. }
  338. for (const kid of kids){
  339. if (kid == null || kid === false) continue;
  340. if (typeof kid === 'string') e.appendChild(document.createTextNode(kid));
  341. else e.appendChild(kid);
  342. }
  343. return e;
  344. }
  345. function sigParts(sig){
  346. const [inS, outS] = sig.split(' → ');
  347. const parse = s => new Set(s.split('+').filter(x => x && x !== '∅'));
  348. return { in: parse(inS), out: parse(outS) };
  349. }
  350. function buildDescMap(roots){
  351. const map = new Map();
  352. function walk(node){
  353. const all = [node.path];
  354. for (const c of node.children || []){
  355. const sub = walk(c);
  356. for (const p of sub) all.push(p);
  357. }
  358. map.set(node.path, all);
  359. return all;
  360. }
  361. for (const r of roots) walk(r);
  362. return map;
  363. }
  364. function workflowMatchesPath(w, key, selPath, descMap){
  365. if (!selPath) return true;
  366. const allowed = new Set(descMap.get(selPath) || [selPath]);
  367. const list = (key === 'shizhi') ? w.apply_shizhi_paths : w.apply_xingshi_paths;
  368. for (const p of (list || [])) if (allowed.has(p)) return true;
  369. return false;
  370. }
  371. // action sequence of a workflow, ignoring 非制作 steps
  372. function actionSeqOfWorkflow(w){
  373. const seq = [];
  374. for (const s of w.steps){
  375. if (PHASE_IGNORE.has(s.phase)) continue;
  376. for (const fk of s.fragment_keys){
  377. const f = fragByKey.get(fk);
  378. if (f && f.action) seq.push(f.action);
  379. }
  380. }
  381. return seq;
  382. }
  383. function actionSigOfWorkflow(w){
  384. return actionSeqOfWorkflow(w).join(' → ');
  385. }
  386. // action set ignoring 非制作 (for verb filter)
  387. function actionSetOfWorkflow(w){
  388. return new Set(actionSeqOfWorkflow(w));
  389. }
  390. function actionFilterActive(){
  391. return !!state.actionSeqSig || state.actionsSelected.size > 0;
  392. }
  393. function scopeFilterActive(){
  394. return !!(state.shizhiPath || state.xingshiPath);
  395. }
  396. function applyFilters(except){
  397. return data.workflows.filter(w => {
  398. if (except !== 'action' && actionFilterActive()){
  399. if (state.actionSeqSig){
  400. if (actionSigOfWorkflow(w) !== state.actionSeqSig) return false;
  401. }
  402. if (state.actionsSelected.size > 0){
  403. const set = actionSetOfWorkflow(w);
  404. for (const v of state.actionsSelected) if (!set.has(v)) return false;
  405. }
  406. }
  407. if (except !== 'scope' && scopeFilterActive()){
  408. if (state.shizhiPath && !workflowMatchesPath(w, 'shizhi', state.shizhiPath, shiDesc)) return false;
  409. if (state.xingshiPath && !workflowMatchesPath(w, 'xingshi', state.xingshiPath, xingDesc)) return false;
  410. }
  411. if (except !== 'mod' && state.modSig && w.io_signature !== state.modSig) return false;
  412. return true;
  413. });
  414. }
  415. function renderVerbFilter(){
  416. const baseFiltered = applyFilters('action');
  417. const counts = {};
  418. for (const w of baseFiltered) for (const v of actionSetOfWorkflow(w)) counts[v] = (counts[v]||0)+1;
  419. const wrap = $('verbChips'); wrap.innerHTML = '';
  420. for (const a of data.actions){
  421. const c = counts[a.verb] || 0;
  422. const isActive = state.actionsSelected.has(a.verb);
  423. const chip = el('span', {
  424. class: 'verb-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  425. title: a.verb + (a.definition ? '\n\n' + a.definition : ''),
  426. onClick: () => {
  427. if (c === 0 && !isActive) return;
  428. if (state.actionsSelected.has(a.verb)) state.actionsSelected.delete(a.verb);
  429. else state.actionsSelected.add(a.verb);
  430. renderAll();
  431. }
  432. },
  433. el('span', null, a.verb),
  434. el('span', {class:'cnt'}, String(c))
  435. );
  436. wrap.appendChild(chip);
  437. }
  438. const n = state.actionsSelected.size;
  439. $('vfSummary').textContent = n > 0 ? (n + ' 项已选') : (data.actions.length + ' 个动词');
  440. if (n > 0) $('verbFilter').setAttribute('open', '');
  441. }
  442. function renderActionSeqs(){
  443. // collect all distinct action signatures across the filtered workflow set
  444. const baseFiltered = applyFilters('action');
  445. const counts = {};
  446. const examples = {};
  447. for (const w of data.workflows){
  448. const sig = actionSigOfWorkflow(w);
  449. if (!sig) continue;
  450. if (!(sig in counts)) { counts[sig] = 0; examples[sig] = actionSeqOfWorkflow(w); }
  451. }
  452. // count under current other filters
  453. const filteredCount = {};
  454. for (const w of baseFiltered){
  455. const sig = actionSigOfWorkflow(w);
  456. if (!sig) continue;
  457. filteredCount[sig] = (filteredCount[sig]||0)+1;
  458. }
  459. const wrap = $('actSeqList'); wrap.innerHTML = '';
  460. const sigs = Object.keys(counts).sort((a,b) => (filteredCount[b]||0) - (filteredCount[a]||0) || a.localeCompare(b));
  461. if (!sigs.length){
  462. wrap.appendChild(el('div', {class:'empty-msg'}, '没有动作序列'));
  463. return;
  464. }
  465. for (const sig of sigs){
  466. const c = filteredCount[sig] || 0;
  467. const isActive = state.actionSeqSig === sig;
  468. const node = el('div', {
  469. class: 'actseq' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  470. onClick: () => {
  471. if (c === 0 && !isActive) return;
  472. state.actionSeqSig = isActive ? null : sig;
  473. renderAll();
  474. }
  475. });
  476. node.appendChild(el('span', {class:'actseq-count'}, String(c)));
  477. const seq = examples[sig];
  478. seq.forEach((v, i) => {
  479. if (i > 0) node.appendChild(el('span', {class:'actseq-sep'}, ' › '));
  480. node.appendChild(el('span', {class:'actseq-chip'}, v));
  481. });
  482. wrap.appendChild(node);
  483. }
  484. }
  485. function renderActions(){
  486. $('actClear').classList.toggle('show', actionFilterActive());
  487. renderVerbFilter();
  488. renderActionSeqs();
  489. }
  490. function renderTreeFacet(roots, key, mountId, selPath, descMap){
  491. const wrap = $(mountId); wrap.innerHTML = '';
  492. const baseFiltered = applyFilters('scope');
  493. const hitCounts = new Map();
  494. for (const w of baseFiltered){
  495. const list = (key === 'shizhi') ? w.apply_shizhi_paths : w.apply_xingshi_paths;
  496. const seen = new Set();
  497. for (const p of (list || [])){
  498. const parts = (p || '').split('/').filter(Boolean);
  499. for (let i = 1; i <= parts.length; i++){
  500. const pp = '/' + parts.slice(0,i).join('/');
  501. if (seen.has(pp)) continue;
  502. seen.add(pp);
  503. }
  504. }
  505. for (const pp of seen) hitCounts.set(pp, (hitCounts.get(pp)||0)+1);
  506. }
  507. function renderNode(node, parent){
  508. const c = hitCounts.get(node.path) || 0;
  509. const collapsed = state.collapsed.has(node.path);
  510. const hasChildren = (node.children || []).length > 0;
  511. const tnode = el('div', {class: 'tnode' + (collapsed ? ' tcollapsed' : '')});
  512. const cls = ['tline'];
  513. if (selPath === node.path) cls.push('active');
  514. if (c === 0 && selPath !== node.path) cls.push('disabled');
  515. if (node.is_suggested && !node.is_hit) cls.push('suggested');
  516. if (node.is_inferred) cls.push('inferred');
  517. const line = el('div', {class: cls.join(' '), title: node.path + (node.description ? '\n\n' + node.description : '')});
  518. const caret = el('span', {
  519. class: 'tcaret' + (hasChildren ? '' : ' invis'),
  520. onClick: (ev) => {
  521. ev.stopPropagation();
  522. if (state.collapsed.has(node.path)) state.collapsed.delete(node.path);
  523. else state.collapsed.add(node.path);
  524. renderTrees();
  525. }
  526. }, hasChildren ? (collapsed ? '▶' : '▼') : '·');
  527. line.appendChild(caret);
  528. let label = node.name;
  529. if (node.is_suggested && !node.is_hit) label += ' ✦';
  530. if (node.is_inferred) label += ' (推断)';
  531. line.appendChild(el('span', {class:'tname'}, label));
  532. line.appendChild(el('span', {class:'tcount'}, String(c)));
  533. line.addEventListener('click', () => {
  534. if (c === 0 && selPath !== node.path) return;
  535. const k = (key === 'shizhi') ? 'shizhiPath' : 'xingshiPath';
  536. state[k] = (state[k] === node.path) ? null : node.path;
  537. renderAll();
  538. });
  539. tnode.appendChild(line);
  540. for (const ch of node.children || []) renderNode(ch, tnode);
  541. parent.appendChild(tnode);
  542. }
  543. for (const r of roots) renderNode(r, wrap);
  544. }
  545. function renderTrees(){
  546. renderTreeFacet(data.subtree.shizhi || [], 'shizhi', 'shizhiTree', state.shizhiPath, shiDesc);
  547. renderTreeFacet(data.subtree.xingshi || [], 'xingshi', 'xingshiTree', state.xingshiPath, xingDesc);
  548. }
  549. function renderScope(){
  550. $('scopeStat').textContent = '路径树(双 facet)';
  551. $('scopeClear').classList.toggle('show', scopeFilterActive());
  552. renderTrees();
  553. }
  554. function sigPassesChipFilter(sig){
  555. const p = sigParts(sig);
  556. for (const m of state.modInFilter) if (!p.in.has(m)) return false;
  557. for (const m of state.modOutFilter) if (!p.out.has(m)) return false;
  558. return true;
  559. }
  560. function renderModalityChips(){
  561. const renderSide = (mountId, palette, selectedSet, side) => {
  562. const wrap = $(mountId); wrap.innerHTML = '';
  563. for (const m of palette){
  564. const trial = new Set(selectedSet);
  565. if (!trial.has(m)) trial.add(m);
  566. let c = 0;
  567. for (const sig of data.modalities){
  568. const p = sigParts(sig.sig);
  569. const inOK = side === 'in'
  570. ? [...trial].every(x => p.in.has(x)) && [...state.modOutFilter].every(x => p.out.has(x))
  571. : [...state.modInFilter].every(x => p.in.has(x)) && [...trial].every(x => p.out.has(x));
  572. if (inOK) c++;
  573. }
  574. const isActive = selectedSet.has(m);
  575. const chip = el('span', {
  576. class: 'mod-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  577. title: m + (c === 0 && !isActive ? ' (无匹配)' : ''),
  578. onClick: () => {
  579. if (c === 0 && !isActive) return;
  580. if (selectedSet.has(m)) selectedSet.delete(m);
  581. else selectedSet.add(m);
  582. if (state.modSig && !sigPassesChipFilter(state.modSig)) state.modSig = null;
  583. renderAll();
  584. }
  585. }, m);
  586. wrap.appendChild(chip);
  587. }
  588. };
  589. renderSide('modInChips', SIDE_MODS.in, state.modInFilter, 'in');
  590. renderSide('modOutChips', SIDE_MODS.out, state.modOutFilter, 'out');
  591. }
  592. function renderModalities(){
  593. const baseFiltered = applyFilters('mod');
  594. const counts = {};
  595. for (const w of baseFiltered) counts[w.io_signature] = (counts[w.io_signature]||0)+1;
  596. renderModalityChips();
  597. const wrap = $('modList'); wrap.innerHTML = '';
  598. const visible = data.modalities.filter(m => sigPassesChipFilter(m.sig));
  599. const sorted = [...visible].sort((a,b) => (counts[b.sig]||0) - (counts[a.sig]||0));
  600. if (sorted.length === 0){
  601. wrap.appendChild(el('div', {class:'empty-msg'}, '当前 chip 筛选下没有匹配签名'));
  602. }
  603. for (const m of sorted){
  604. const c = counts[m.sig] || 0;
  605. const node = el('div', {
  606. class: 'item' + (state.modSig === m.sig ? ' active' : '') + (c === 0 && state.modSig !== m.sig ? ' disabled' : ''),
  607. onClick: () => {
  608. if (c === 0 && state.modSig !== m.sig) return;
  609. state.modSig = (state.modSig === m.sig) ? null : m.sig;
  610. renderAll();
  611. }
  612. },
  613. el('span', {class:'item-name'}, m.sig),
  614. el('span', {class:'item-count'}, String(c))
  615. );
  616. wrap.appendChild(node);
  617. }
  618. $('modClear').classList.toggle('show', !!state.modSig || state.modInFilter.size > 0 || state.modOutFilter.size > 0);
  619. }
  620. function renderWorkflowCard(w){
  621. const isActive = state.wfKey === w.workflow_key;
  622. const node = el('div', {
  623. class: 'wf' + (isActive ? ' active' : ''),
  624. onClick: () => { state.wfKey = isActive ? null : w.workflow_key; renderWorkflows(); renderDetail(); }
  625. });
  626. const head = el('div', {class:'wf-head'},
  627. el('span', {class:'wf-meta', style:'margin-left:0'}, w.step_count + ' step · ' + w.capability_count + ' cap')
  628. );
  629. node.appendChild(head);
  630. // 动作序列
  631. const seq = el('div', {class:'wf-seq'});
  632. const visibleSeq = actionSeqOfWorkflow(w);
  633. if (visibleSeq.length){
  634. visibleSeq.forEach((v, i) => {
  635. if (i > 0) seq.appendChild(el('span', {class:'vsep'}, '›'));
  636. seq.appendChild(el('span', {class:'vchip'}, v));
  637. });
  638. } else {
  639. seq.appendChild(el('span', {style:'font-size:10px;color:var(--muted)'}, '—'));
  640. }
  641. node.appendChild(seq);
  642. // 作用域 (leaf names from apply paths)
  643. const scopeLeaves = [];
  644. const seenLeaf = new Set();
  645. const addLeaves = (paths, facet) => {
  646. for (const p of (paths || [])){
  647. const leaf = (p || '').split('/').filter(Boolean).pop();
  648. if (!leaf) continue;
  649. const key = facet + ':' + leaf;
  650. if (seenLeaf.has(key)) continue;
  651. seenLeaf.add(key);
  652. scopeLeaves.push({leaf, facet, path:p});
  653. }
  654. };
  655. addLeaves(w.apply_shizhi_paths, '实质');
  656. addLeaves(w.apply_xingshi_paths, '形式');
  657. const scopeRow = el('div', {class:'wf-row'},
  658. el('span', {class:'wf-lbl'}, '作用域'));
  659. const scopeVal = el('span', {class:'wf-val'});
  660. if (scopeLeaves.length){
  661. const MAX = 6;
  662. scopeLeaves.slice(0, MAX).forEach(s => {
  663. scopeVal.appendChild(el('span', {class:'wf-scope-chip', title:s.path}, s.leaf));
  664. });
  665. if (scopeLeaves.length > MAX){
  666. scopeVal.appendChild(el('span', {class:'wf-scope-more'}, '+'+(scopeLeaves.length - MAX)));
  667. }
  668. } else {
  669. scopeVal.appendChild(el('span', {style:'color:var(--muted)'}, '—'));
  670. }
  671. scopeRow.appendChild(scopeVal);
  672. node.appendChild(scopeRow);
  673. // IO
  674. const ioRow = el('div', {class:'wf-row'},
  675. el('span', {class:'wf-lbl'}, 'IO'),
  676. el('span', {class:'wf-val', style:'font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)'}, w.io_signature)
  677. );
  678. node.appendChild(ioRow);
  679. return node;
  680. }
  681. function renderWorkflows(){
  682. const filtered = applyFilters(null);
  683. const wrap = $('wfList'); wrap.innerHTML = '';
  684. $('wfCnt').textContent = filtered.length + ' / ' + data.workflows.length;
  685. if (!filtered.length){
  686. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的工序'));
  687. return;
  688. }
  689. for (const w of filtered) wrap.appendChild(renderWorkflowCard(w));
  690. }
  691. function renderCaseDetail(wrap, caseIdx){
  692. const c = casesByIndex[caseIdx];
  693. if (!c) return;
  694. const det = el('details', {class:'case-detail'});
  695. const sumLabel = '📄 源: ' + (c.title || '案例 ' + caseIdx);
  696. det.appendChild(el('summary', null, sumLabel));
  697. const content = el('div', {class:'case-content'});
  698. if (c.author) {
  699. content.appendChild(el('div', {class:'case-author'}, '作者:' + c.author));
  700. }
  701. if (c.feedback && (c.feedback.like_count || c.feedback.comment_count || c.feedback.collect_count || c.feedback.share_count)){
  702. const fb = c.feedback;
  703. const row = el('div', {class:'case-feedback'});
  704. if (fb.like_count != null) row.appendChild(el('span', {class:'fb'}, '👍 ' + fb.like_count));
  705. if (fb.comment_count != null) row.appendChild(el('span', {class:'fb'}, '💬 ' + fb.comment_count));
  706. if (fb.collect_count != null) row.appendChild(el('span', {class:'fb'}, '⭐ ' + fb.collect_count));
  707. if (fb.share_count != null) row.appendChild(el('span', {class:'fb'}, '↗ ' + fb.share_count));
  708. content.appendChild(row);
  709. }
  710. const imgs = (c.images && c.images.length) ? c.images : (c.cover ? [c.cover] : []);
  711. if (imgs.length){
  712. const wrapImg = el('div', {class:'case-images'});
  713. for (const src of imgs) wrapImg.appendChild(el('img', {src, loading:'lazy'}));
  714. content.appendChild(wrapImg);
  715. }
  716. if (c.body){
  717. content.appendChild(el('div', {class:'case-body'}, c.body));
  718. }
  719. if (c.url){
  720. content.appendChild(el('a', {href:c.url, target:'_blank', rel:'noopener', class:'case-link'}, '🔗 访问原始链接'));
  721. }
  722. det.appendChild(content);
  723. wrap.appendChild(det);
  724. }
  725. // build atomic rows from a fragment: apply_shizhi + apply_xingshi entries, with markers
  726. function buildAtomicRows(f){
  727. const rows = [];
  728. for (const e of (f.apply_shizhi || [])){
  729. rows.push({
  730. facet: '实质',
  731. category_path: e.category_path,
  732. source: e.source,
  733. leaf: (e.category_path || '').split('/').filter(Boolean).pop() || '—',
  734. });
  735. }
  736. for (const e of (f.apply_xingshi || [])){
  737. rows.push({
  738. facet: '形式',
  739. category_path: e.category_path,
  740. source: e.source,
  741. leaf: (e.category_path || '').split('/').filter(Boolean).pop() || '—',
  742. });
  743. }
  744. if (!rows.length) rows.push({ facet:'—', category_path:'', source:'apply', leaf:'—', empty:true });
  745. return rows;
  746. }
  747. function renderCapTable(w){
  748. const table = el('table', {class:'cap-table'});
  749. const thead = el('thead');
  750. thead.appendChild(el('tr', null,
  751. el('th', null, '步骤'),
  752. el('th', null, '能力动作'),
  753. el('th', null, '原子能力动作'),
  754. el('th', null, '原子能力作用域'),
  755. el('th', null, '原子能力Tool'),
  756. el('th', null, '原子能力类型'),
  757. el('th', null, '原子能力实现'),
  758. el('th', null, '能力输入'),
  759. el('th', null, '能力输出')
  760. ));
  761. table.appendChild(thead);
  762. const tbody = el('tbody');
  763. // pre-compute atom rows per cap to know rowspan totals per step
  764. const stepRows = []; // [{step, caps: [{f, atoms}], totalRows}]
  765. for (const s of w.steps){
  766. const caps = [];
  767. let total = 0;
  768. for (const fk of s.fragment_keys){
  769. const f = fragByKey.get(fk);
  770. if (!f) continue;
  771. const atoms = buildAtomicRows(f);
  772. caps.push({f, atoms});
  773. total += atoms.length;
  774. }
  775. if (caps.length === 0) continue;
  776. stepRows.push({step:s, caps, totalRows: total});
  777. }
  778. let stepIdx = 0;
  779. stepRows.forEach((sr, si) => {
  780. stepIdx += 1;
  781. sr.caps.forEach((cp, ci) => {
  782. const {f, atoms} = cp;
  783. const N = atoms.length;
  784. const tools = (f.tools && f.tools.length) ? f.tools.join(' / ') : '—';
  785. const inMods = (f.in_modalities && f.in_modalities.length) ? f.in_modalities : [];
  786. const outMods = (f.out_modalities && f.out_modalities.length) ? f.out_modalities : [];
  787. const renderIO = (mods, kind) => {
  788. if (!mods.length) return el('span', {class:'cell-dash'}, '—');
  789. const box = el('div');
  790. for (const m of mods) box.appendChild(el('span', {class:'mod-pill ' + kind}, m));
  791. return box;
  792. };
  793. atoms.forEach((a, ai) => {
  794. const isStepFirst = ai === 0 && ci === 0;
  795. const isCapFirst = ai === 0;
  796. const trClass = isStepFirst ? 'step-first' : (isCapFirst ? 'cap-first' : '');
  797. const tr = el('tr', trClass ? {class: trClass} : null);
  798. if (isStepFirst){
  799. tr.appendChild(el('td', {class:'cell-step', rowSpan: sr.totalRows}, String(stepIdx)));
  800. }
  801. if (isCapFirst){
  802. tr.appendChild(el('td', {class:'cell-action', rowSpan:N, title:f.fragment_id}, f.action || '—'));
  803. }
  804. // 原子能力动作: fallback to cap.action
  805. tr.appendChild(el('td', {class:'cell-atom-action'},
  806. a.empty ? el('span', {class:'cell-dash'}, '—') : (f.action || '—')));
  807. // 原子能力作用域
  808. if (a.empty){
  809. tr.appendChild(el('td', {class:'cell-dash'}, '—'));
  810. } else {
  811. const cell = el('td', {
  812. class:'cell-scope' + (a.source === 'suggest' ? ' suggest' : ''),
  813. title: a.category_path
  814. });
  815. const labelLine = el('div');
  816. const leaf = el('span', {class:'scope-leaf' + (a.source === 'suggest' ? ' suggest' : '')}, a.leaf);
  817. labelLine.appendChild(leaf);
  818. if (a.source === 'suggest') labelLine.appendChild(el('span', {class:'scope-mark'}, '✦'));
  819. labelLine.appendChild(el('span', {class:'scope-facet'}, a.facet));
  820. cell.appendChild(labelLine);
  821. cell.appendChild(el('div', {class:'scope-path'}, a.category_path));
  822. tr.appendChild(cell);
  823. }
  824. // 原子能力Tool
  825. tr.appendChild(el('td', {class:'cell-tool'}, tools));
  826. // 原子能力类型
  827. tr.appendChild(el('td', {class:'cell-type'}, 'prompt'));
  828. // 原子能力实现
  829. tr.appendChild(el('td', {class:'cell-impl'}, el('span', {class:'cell-dash'}, '—')));
  830. if (isCapFirst){
  831. tr.appendChild(el('td', {class:'cell-io', rowSpan:N}, renderIO(inMods, 'input')));
  832. tr.appendChild(el('td', {class:'cell-io', rowSpan:N}, renderIO(outMods, 'output')));
  833. }
  834. tbody.appendChild(tr);
  835. });
  836. });
  837. });
  838. table.appendChild(tbody);
  839. return table;
  840. }
  841. function renderDetail(){
  842. const wrap = $('detailBody'); wrap.innerHTML = '';
  843. const w = data.workflows.find(x => x.workflow_key === state.wfKey);
  844. if (!w){ wrap.appendChild(el('div', {class:'detail-empty'}, '点击左侧工序查看详情')); return; }
  845. wrap.appendChild(el('h2', null,
  846. el('span', {class:'case-badge'}, '案例 '+w.case_index),
  847. el('span', {class:'wf-badge'}, w.workflow_id),
  848. el('span', {class:'pill'}, w.step_count + ' step'),
  849. el('span', {class:'pill'}, w.capability_count + ' cap')
  850. ));
  851. renderCaseDetail(wrap, w.case_index);
  852. const sigSec = el('div', {class:'detail-section'}, el('h3', null, '签名'));
  853. sigSec.appendChild(el('div', {style:'font-size:12px;margin-bottom:4px'},
  854. el('span', {style:'color:var(--muted);margin-right:6px'}, 'IO'),
  855. el('span', null, w.io_signature)));
  856. sigSec.appendChild(el('div', {style:'font-size:12px;margin-bottom:4px'},
  857. el('span', {style:'color:var(--muted);margin-right:6px'}, '动作序列'),
  858. el('span', null, actionSigOfWorkflow(w) || '—')));
  859. wrap.appendChild(sigSec);
  860. const tableSec = el('div', {class:'detail-section'}, el('h3', null, '能力 / 原子能力'));
  861. tableSec.appendChild(renderCapTable(w));
  862. wrap.appendChild(tableSec);
  863. }
  864. function renderChips(){
  865. const c = $('chips'); c.innerHTML = '';
  866. let any = false;
  867. const mk = (lbl, val, onX) => {
  868. any = true;
  869. c.appendChild(el('span', {class:'filter-chip'},
  870. el('span', {class:'lbl'}, lbl),
  871. el('span', null, val),
  872. el('span', {class:'x', onClick: () => { onX(); renderAll(); }}, '×')
  873. ));
  874. };
  875. if (state.actionSeqSig) mk('动作序列', state.actionSeqSig, () => { state.actionSeqSig = null; });
  876. if (state.actionsSelected.size) mk('含动词', [...state.actionsSelected].join(' + '), () => { state.actionsSelected.clear(); });
  877. if (state.shizhiPath) mk('实质', state.shizhiPath.split('/').pop() || state.shizhiPath, () => { state.shizhiPath = null; });
  878. if (state.xingshiPath) mk('形式', state.xingshiPath.split('/').pop() || state.xingshiPath, () => { state.xingshiPath = null; });
  879. if (state.modSig) mk('模态', state.modSig, () => { state.modSig = null; });
  880. $('clearAll').classList.toggle('active', any);
  881. }
  882. function renderAll(){
  883. renderActions();
  884. renderScope();
  885. renderModalities();
  886. renderWorkflows();
  887. renderDetail();
  888. renderChips();
  889. }
  890. // splitter
  891. function setupSplitter(){
  892. const sp = $('splitter');
  893. let dragging = false;
  894. const setPct = (pct) => {
  895. pct = Math.max(4, Math.min(96, pct));
  896. document.documentElement.style.setProperty('--left-pct', pct + '%');
  897. // update preset active state
  898. let activeId = null;
  899. if (pct < 20) activeId = 'presetRight';
  900. else if (pct > 80) activeId = 'presetLeft';
  901. else if (pct >= 45 && pct <= 55) activeId = 'presetEven';
  902. for (const id of ['presetLeft','presetEven','presetRight']){
  903. $(id).classList.toggle('active', id === activeId);
  904. }
  905. };
  906. sp.addEventListener('mousedown', e => {
  907. dragging = true; sp.classList.add('dragging');
  908. document.body.style.cursor = 'col-resize';
  909. document.body.style.userSelect = 'none';
  910. e.preventDefault();
  911. });
  912. document.addEventListener('mousemove', e => {
  913. if (!dragging) return;
  914. const w = window.innerWidth;
  915. setPct((e.clientX / w) * 100);
  916. });
  917. document.addEventListener('mouseup', () => {
  918. if (!dragging) return;
  919. dragging = false; sp.classList.remove('dragging');
  920. document.body.style.cursor = ''; document.body.style.userSelect = '';
  921. });
  922. $('presetLeft').addEventListener('click', () => setPct(94));
  923. $('presetEven').addEventListener('click', () => setPct(50));
  924. $('presetRight').addEventListener('click', () => setPct(28));
  925. setPct(40);
  926. }
  927. function init(){
  928. for (const c of (data.cases || [])) casesByIndex[c.index] = c;
  929. for (const f of data.fragments) fragByKey.set(f.case_index + ':' + f.fragment_id, f);
  930. SIDE_MODS = (() => {
  931. const inSet = new Set(), outSet = new Set();
  932. for (const m of data.modalities){
  933. const p = sigParts(m.sig);
  934. for (const x of p.in) inSet.add(x);
  935. for (const x of p.out) outSet.add(x);
  936. }
  937. return { in: [...inSet].sort(), out: [...outSet].sort() };
  938. })();
  939. shiDesc = buildDescMap(data.subtree.shizhi || []);
  940. xingDesc = buildDescMap(data.subtree.xingshi || []);
  941. $('loading').style.display = 'none';
  942. $('appRoot').style.display = '';
  943. $('clearAll').addEventListener('click', () => {
  944. state.actionSeqSig = null;
  945. state.actionsSelected.clear();
  946. state.shizhiPath = null; state.xingshiPath = null;
  947. state.modSig = null;
  948. state.modInFilter.clear(); state.modOutFilter.clear();
  949. renderAll();
  950. });
  951. $('actClear').addEventListener('click', () => {
  952. state.actionSeqSig = null;
  953. state.actionsSelected.clear();
  954. renderAll();
  955. });
  956. $('scopeClear').addEventListener('click', () => {
  957. state.shizhiPath = null; state.xingshiPath = null;
  958. renderAll();
  959. });
  960. $('modClear').addEventListener('click', () => {
  961. state.modSig = null; state.modInFilter.clear(); state.modOutFilter.clear();
  962. renderAll();
  963. });
  964. setupSplitter();
  965. renderAll();
  966. }
  967. fetch('/api/viz/data/workflow')
  968. .then(r => {
  969. if (!r.ok) throw new Error('fetch failed: ' + r.status);
  970. return r.json();
  971. })
  972. .then(d => { data = d; init(); })
  973. .catch(err => {
  974. $('loading').textContent = '加载失败:' + err.message + '。请先上传数据:POST /api/viz/data/workflow';
  975. });
  976. </script>
  977. </body>
  978. </html>