viz_workflow.html 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  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:200px 260px 220px 380px 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-1 action tabs */
  55. .act-tabs{display:flex;gap:4px;margin:0 0 8px;border-bottom:1px solid var(--border);padding-bottom:5px}
  56. .act-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. .act-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
  60. .act-tab:hover:not(.active){color:var(--fg)}
  61. .act-pane{display:none}
  62. .act-pane.active{display:block}
  63. .act-meta{font-size:10px;color:var(--muted);margin:0 4px 8px;line-height:1.4}
  64. /* phase signature list */
  65. .phasesig{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
  66. border:1px solid var(--border);background:var(--panel);font-size:11px;line-height:1.4}
  67. .phasesig:hover:not(.disabled):not(.active){border-color:var(--accent)}
  68. .phasesig.active{border-color:var(--accent);background:#eff6ff;
  69. box-shadow:0 0 0 1px var(--accent) inset}
  70. .phasesig.disabled{opacity:.42;cursor:not-allowed}
  71. .phasesig-count{float:right;font-size:10px;color:var(--muted);
  72. background:var(--bg);padding:1px 5px;border-radius:8px}
  73. .phasesig.active .phasesig-count{background:rgba(255,255,255,.3);color:var(--accent)}
  74. /* tree */
  75. .tnode{margin-left:0}
  76. .tnode .tnode{margin-left:12px;border-left:1px dashed var(--border);padding-left:6px}
  77. .tline{display:flex;align-items:center;gap:4px;padding:3px 4px;border-radius:4px;
  78. cursor:pointer;font-size:12px;line-height:1.35}
  79. .tline:hover:not(.disabled):not(.active){background:var(--panel2)}
  80. .tline.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  81. .tline.disabled{opacity:.42;cursor:not-allowed}
  82. .tline.suggested .tname{color:var(--suggest);font-style:italic}
  83. .tline.suggested.active .tname{color:#fff}
  84. .tline.inferred .tname{color:var(--inferred);font-style:italic}
  85. .tline.inferred.active .tname{color:#fff}
  86. .tcaret{width:12px;display:inline-block;text-align:center;color:var(--muted);
  87. cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
  88. .tcaret.invis{visibility:hidden}
  89. .tname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  90. .tcount{font-size:10px;color:var(--muted);background:var(--bg);padding:1px 5px;border-radius:8px;flex-shrink:0}
  91. .tline.active .tcount{background:rgba(255,255,255,.3);color:#fff}
  92. .tcollapsed > .tnode{display:none}
  93. .facet-title{margin-top:10px;padding:4px 6px;font-size:11px;color:var(--muted);
  94. font-weight:600;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.5px}
  95. .facet-title:first-child{margin-top:0}
  96. /* workflow card */
  97. .wf{padding:9px 10px;border-radius:6px;cursor:pointer;margin-bottom:6px;
  98. border:1px solid var(--border);background:var(--panel)}
  99. .wf:hover:not(.active){background:var(--panel2)}
  100. .wf.active{border-color:var(--accent);background:#eff6ff;
  101. box-shadow:0 0 0 1px var(--accent) inset}
  102. .wf-head{display:flex;align-items:center;gap:6px;margin-bottom:5px;flex-wrap:wrap}
  103. .case-badge{background:var(--accent);color:#fff;padding:1px 5px;border-radius:3px;
  104. font-weight:600;font-size:10px}
  105. .wf-badge{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;
  106. font-weight:600;font-size:10px}
  107. .wf-meta{font-size:10px;color:var(--muted);margin-left:auto}
  108. .wf-title{font-size:12px;color:#334155;margin:3px 0;line-height:1.4;
  109. display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
  110. .wf-sig{font-size:10px;color:var(--muted);margin:3px 0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
  111. .wf-seq{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px;align-items:center}
  112. .vchip{font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600;line-height:1.3;
  113. border:1px solid transparent;white-space:nowrap}
  114. .vsep{color:var(--muted);font-size:9px;line-height:1.3}
  115. /* modality side filter */
  116. .mod-filter{margin:0 4px 8px;padding:6px 6px 4px;background:var(--code);
  117. border:1px solid var(--border);border-radius:5px}
  118. .mod-filter-row{display:flex;align-items:center;gap:6px;margin-bottom:4px}
  119. .mod-filter-row:last-child{margin-bottom:0}
  120. .mod-filter-lbl{font-size:10px;color:var(--muted);min-width:42px;flex-shrink:0}
  121. .mod-chips{display:flex;gap:4px;flex-wrap:wrap}
  122. .mod-chip{font-size:11px;padding:1px 7px;background:var(--tag-bg);border-radius:8px;
  123. cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent}
  124. .mod-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
  125. .mod-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
  126. .mod-chip.disabled{opacity:.42;cursor:not-allowed}
  127. /* detail */
  128. .detail-empty{color:var(--muted);text-align:center;padding:60px 20px;font-size:13px}
  129. .detail h2{font-size:14px;margin:0 0 8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
  130. .detail-section{margin:12px 0}
  131. .detail-section h3{font-size:10px;color:var(--muted);text-transform:uppercase;
  132. letter-spacing:.6px;margin:0 0 6px;font-weight:600}
  133. .body-text{font-size:12px;line-height:1.6;color:#334155;background:var(--code);
  134. padding:8px 10px;border-radius:5px;white-space:pre-wrap}
  135. .pill{display:inline-block;padding:1px 6px;background:var(--tag-bg);border-radius:8px;
  136. font-size:11px;margin-right:3px;margin-bottom:3px}
  137. .pill.in{background:var(--shi-bg);color:var(--shi-fg)}
  138. .pill.out{background:var(--xing-bg);color:var(--xing-fg)}
  139. .pill.cfg{background:var(--both-bg);color:var(--both-fg)}
  140. .io-row{margin-bottom:4px;font-size:11px}
  141. .io-row .lbl{color:var(--muted);font-size:10px;margin-right:6px;text-transform:uppercase}
  142. .pathline{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
  143. color:var(--accent);margin-bottom:2px;word-break:break-all}
  144. .pathline.suggest{color:var(--suggest)}
  145. .rationale{color:var(--muted);font-size:11px;margin-bottom:3px;margin-left:4px;line-height:1.45}
  146. .excerpt-line{color:var(--muted);font-size:11px;margin-left:4px;line-height:1.45;margin-bottom:6px;
  147. border-left:2px solid var(--border);padding-left:6px}
  148. .excerpt-line .em{color:#334155}
  149. .json-raw{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
  150. background:var(--code);padding:8px;border-radius:5px;white-space:pre-wrap;
  151. color:#334155;max-height:240px;overflow:auto}
  152. details summary{cursor:pointer;color:var(--muted);font-size:10px;margin:6px 0;user-select:none}
  153. details summary:hover{color:var(--fg)}
  154. /* case-detail (original-post) section */
  155. .case-detail{margin:8px 0;padding-bottom:8px;border-bottom:1px solid var(--border)}
  156. .case-detail summary{font-weight:bold;color:var(--accent);padding:6px 8px;
  157. background:var(--panel2);border-radius:4px;font-size:11px;cursor:pointer;outline:none;margin:0}
  158. .case-content{padding:10px;background:var(--code);border-radius:5px;
  159. margin-top:4px;border:1px solid var(--border)}
  160. .case-images{display:flex;gap:8px;overflow-x:auto;margin-bottom:8px;padding-bottom:4px}
  161. .case-images img{height:160px;border-radius:5px;object-fit:contain;
  162. background:var(--panel2);border:1px solid var(--border)}
  163. .case-body{font-size:12px;line-height:1.6;color:var(--fg);white-space:pre-wrap;
  164. margin-bottom:10px;max-height:250px;overflow-y:auto;padding-right:4px}
  165. .case-link{font-size:11px;color:var(--accent);display:inline-block;
  166. background:var(--panel2);padding:4px 8px;border-radius:4px;text-decoration:none}
  167. .case-feedback{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;gap:12px;flex-wrap:wrap}
  168. .case-feedback .fb{display:inline-flex;gap:3px;align-items:center}
  169. .case-author{font-size:11px;color:var(--muted);margin-bottom:6px}
  170. /* step section */
  171. .step-sec{margin:10px 0;border:1px solid var(--border);border-radius:6px;background:var(--panel)}
  172. .step-head{display:flex;align-items:center;gap:8px;padding:6px 10px;
  173. border-bottom:1px solid var(--border);font-size:11px;background:var(--panel2)}
  174. .step-id{background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;font-weight:600;font-size:10px}
  175. .step-phase{padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600}
  176. .step-order{color:var(--muted);font-size:10px}
  177. .step-altnote{margin-left:auto;font-size:10px;color:var(--warn)}
  178. .cap{padding:8px 10px;border-top:1px dashed var(--border)}
  179. .cap:first-child{border-top:none}
  180. .cap-head{display:flex;align-items:center;gap:6px;margin-bottom:4px;flex-wrap:wrap}
  181. .cap-id{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
  182. .cap-act{background:var(--warn);color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
  183. .cap-sig{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--muted)}
  184. .cap-body{font-size:12px;line-height:1.55;color:#334155;background:var(--code);
  185. padding:6px 9px;border-radius:4px;white-space:pre-wrap;margin:4px 0}
  186. .cap-meta{font-size:10px;color:var(--muted);margin-top:3px}
  187. .empty-msg{color:var(--muted);text-align:center;padding:30px 10px;font-size:11px;font-style:italic}
  188. .filter-chip{font-size:11px;padding:3px 8px;background:var(--tag-bg);border-radius:10px;
  189. color:var(--fg);display:inline-flex;align-items:center;gap:6px;margin-right:4px}
  190. .filter-chip .x{color:var(--muted);cursor:pointer;font-weight:bold}
  191. .filter-chip .x:hover{color:var(--warn)}
  192. .filter-chip .lbl{color:var(--muted);font-size:9px;text-transform:uppercase}
  193. .loading{display:flex;align-items:center;justify-content:center;
  194. height:100vh;font-size:14px;color:var(--muted)}
  195. </style>
  196. </head>
  197. <body>
  198. <div id="loading" class="loading">加载工序数据中…</div>
  199. <div id="appRoot" style="display:none">
  200. <header>
  201. <h1>工序</h1>
  202. <span id="chips"></span>
  203. <button class="clear-all" id="clearAll">清除全部筛选</button>
  204. </header>
  205. <main>
  206. <section class="fac1" id="actCol">
  207. <div class="col-title">
  208. <span>① 动作</span>
  209. <span class="col-clear" id="actClear" style="margin-left:auto">清除</span>
  210. </div>
  211. <div class="act-tabs">
  212. <div class="act-tab" data-mode="multiset" id="tabMulti">动作</div>
  213. <div class="act-tab" data-mode="phase" id="tabPhase">结构</div>
  214. </div>
  215. <div class="act-pane" id="paneMulti">
  216. <div class="act-meta">多选 · 工序须包含所有选中动作(AND)</div>
  217. <div id="actList"></div>
  218. </div>
  219. <div class="act-pane" id="panePhase">
  220. <div class="act-meta">单选 · 完整 phase 序列</div>
  221. <div id="phaseList"></div>
  222. </div>
  223. </section>
  224. <section class="fac2" id="scopeCol">
  225. <div class="col-title">
  226. <span>② 作用域</span>
  227. <span class="col-count" id="scopeStat"></span>
  228. <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
  229. </div>
  230. <div class="facet-title">实质</div>
  231. <div id="shizhiTree"></div>
  232. <div class="facet-title">形式</div>
  233. <div id="xingshiTree"></div>
  234. </section>
  235. <section class="fac3" id="modCol">
  236. <div class="col-title"><span>③ 输入 → 输出模态</span><span class="col-clear" id="modClear">清除</span></div>
  237. <div class="mod-filter">
  238. <div class="mod-filter-row">
  239. <span class="mod-filter-lbl">输入含</span>
  240. <span class="mod-chips" id="modInChips"></span>
  241. </div>
  242. <div class="mod-filter-row">
  243. <span class="mod-filter-lbl">输出含</span>
  244. <span class="mod-chips" id="modOutChips"></span>
  245. </div>
  246. </div>
  247. <div id="modList"></div>
  248. </section>
  249. <section class="fac4" id="wfCol">
  250. <div class="col-title"><span>④ 工序</span><span class="col-count" id="wfCnt"></span></div>
  251. <div id="wfList"></div>
  252. </section>
  253. <section class="detail" id="detailCol">
  254. <div class="col-title"><span>⑤ 工序详情</span></div>
  255. <div id="detailBody"><div class="detail-empty">点击左侧工序查看详情</div></div>
  256. </section>
  257. </main>
  258. </div>
  259. <script>
  260. let data = null;
  261. let casesByIndex = {};
  262. let SIDE_MODS = null;
  263. let shiDesc = null;
  264. let xingDesc = null;
  265. const fragByKey = new Map();
  266. let PHASE_COLORS = {};
  267. let PHASE_FG = {};
  268. const state = {
  269. actionMode: 'multiset',
  270. actionsSelected: new Set(),
  271. phaseSig: null,
  272. shizhiPath: null,
  273. xingshiPath: null,
  274. modSig: null,
  275. modInFilter: new Set(),
  276. modOutFilter: new Set(),
  277. wfKey: null,
  278. collapsed: new Set(),
  279. };
  280. function $(id){return document.getElementById(id)}
  281. function el(tag, attrs, ...kids){
  282. const e = document.createElement(tag);
  283. if (attrs) for (const k in attrs){
  284. if (k === 'class') e.className = attrs[k];
  285. else if (k === 'onClick') e.addEventListener('click', attrs[k]);
  286. else if (k.startsWith('data-')) e.setAttribute(k, attrs[k]);
  287. else if (k === 'title') e.title = attrs[k];
  288. else if (k === 'style') e.setAttribute('style', attrs[k]);
  289. else e[k] = attrs[k];
  290. }
  291. for (const kid of kids){
  292. if (kid == null || kid === false) continue;
  293. if (typeof kid === 'string') e.appendChild(document.createTextNode(kid));
  294. else e.appendChild(kid);
  295. }
  296. return e;
  297. }
  298. function sigParts(sig){
  299. const [inS, outS] = sig.split(' → ');
  300. const parse = s => new Set(s.split('+').filter(x => x && x !== '∅'));
  301. return { in: parse(inS), out: parse(outS) };
  302. }
  303. function buildDescMap(roots){
  304. const map = new Map();
  305. function walk(node){
  306. const all = [node.path];
  307. for (const c of node.children || []){
  308. const sub = walk(c);
  309. for (const p of sub) all.push(p);
  310. }
  311. map.set(node.path, all);
  312. return all;
  313. }
  314. for (const r of roots) walk(r);
  315. return map;
  316. }
  317. function workflowMatchesPath(w, key, selPath, descMap){
  318. if (!selPath) return true;
  319. const allowed = new Set(descMap.get(selPath) || [selPath]);
  320. const list = (key === 'shizhi') ? w.apply_shizhi_paths : w.apply_xingshi_paths;
  321. for (const p of (list || [])) if (allowed.has(p)) return true;
  322. return false;
  323. }
  324. function actionFilterActive(){
  325. if (state.actionMode === 'multiset') return state.actionsSelected.size > 0;
  326. return !!state.phaseSig;
  327. }
  328. function scopeFilterActive(){
  329. return !!(state.shizhiPath || state.xingshiPath);
  330. }
  331. function applyFilters(except){
  332. return data.workflows.filter(w => {
  333. if (except !== 'action' && actionFilterActive()){
  334. if (state.actionMode === 'multiset'){
  335. const set = new Set(w.actions_set);
  336. for (const v of state.actionsSelected) if (!set.has(v)) return false;
  337. } else {
  338. if (w.phase_signature !== state.phaseSig) return false;
  339. }
  340. }
  341. if (except !== 'scope' && scopeFilterActive()){
  342. if (state.shizhiPath && !workflowMatchesPath(w, 'shizhi', state.shizhiPath, shiDesc)) return false;
  343. if (state.xingshiPath && !workflowMatchesPath(w, 'xingshi', state.xingshiPath, xingDesc)) return false;
  344. }
  345. if (except !== 'mod' && state.modSig && w.io_signature !== state.modSig) return false;
  346. return true;
  347. });
  348. }
  349. function renderActionMultiset(){
  350. const baseFiltered = applyFilters('action');
  351. const counts = {};
  352. for (const w of baseFiltered) for (const v of w.actions_set) counts[v] = (counts[v]||0)+1;
  353. const wrap = $('actList'); wrap.innerHTML = '';
  354. for (const a of data.actions){
  355. const c = counts[a.verb] || 0;
  356. const isActive = state.actionsSelected.has(a.verb);
  357. const node = el('div', {
  358. class: 'item' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  359. onClick: () => {
  360. if (c === 0 && !isActive) return;
  361. if (state.actionsSelected.has(a.verb)) state.actionsSelected.delete(a.verb);
  362. else state.actionsSelected.add(a.verb);
  363. renderAll();
  364. }
  365. });
  366. const stack = el('div', {class:'item-stack'},
  367. el('div', {class:'item-name'}, a.verb),
  368. a.definition ? el('div', {class:'item-def'}, a.definition) : null);
  369. node.appendChild(stack);
  370. node.appendChild(el('span', {class:'item-count'}, String(c)));
  371. wrap.appendChild(node);
  372. }
  373. }
  374. function renderActionPhase(){
  375. const baseFiltered = applyFilters('action');
  376. const counts = {};
  377. for (const w of baseFiltered) counts[w.phase_signature] = (counts[w.phase_signature]||0)+1;
  378. const wrap = $('phaseList'); wrap.innerHTML = '';
  379. const sigs = [...data.phaseSignatures].sort((a, b) => (counts[b]||0) - (counts[a]||0) || a.localeCompare(b));
  380. for (const sig of sigs){
  381. const c = counts[sig] || 0;
  382. const isActive = state.phaseSig === sig;
  383. const node = el('div', {
  384. class: 'phasesig' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  385. onClick: () => {
  386. if (c === 0 && !isActive) return;
  387. state.phaseSig = isActive ? null : sig;
  388. renderAll();
  389. }
  390. });
  391. node.appendChild(el('span', {class:'phasesig-count'}, String(c)));
  392. node.appendChild(document.createTextNode(sig));
  393. wrap.appendChild(node);
  394. }
  395. }
  396. function renderActions(){
  397. $('tabMulti').classList.toggle('active', state.actionMode === 'multiset');
  398. $('tabPhase').classList.toggle('active', state.actionMode === 'phase');
  399. $('paneMulti').classList.toggle('active', state.actionMode === 'multiset');
  400. $('panePhase').classList.toggle('active', state.actionMode === 'phase');
  401. $('actClear').classList.toggle('show', actionFilterActive());
  402. if (state.actionMode === 'multiset') renderActionMultiset();
  403. else renderActionPhase();
  404. }
  405. function renderTreeFacet(roots, key, mountId, selPath, descMap){
  406. const wrap = $(mountId); wrap.innerHTML = '';
  407. const baseFiltered = applyFilters('scope');
  408. const hitCounts = new Map();
  409. for (const w of baseFiltered){
  410. const list = (key === 'shizhi') ? w.apply_shizhi_paths : w.apply_xingshi_paths;
  411. const seen = new Set();
  412. for (const p of (list || [])){
  413. const parts = (p || '').split('/').filter(Boolean);
  414. for (let i = 1; i <= parts.length; i++){
  415. const pp = '/' + parts.slice(0,i).join('/');
  416. if (seen.has(pp)) continue;
  417. seen.add(pp);
  418. }
  419. }
  420. for (const pp of seen) hitCounts.set(pp, (hitCounts.get(pp)||0)+1);
  421. }
  422. function renderNode(node, parent){
  423. const c = hitCounts.get(node.path) || 0;
  424. const collapsed = state.collapsed.has(node.path);
  425. const hasChildren = (node.children || []).length > 0;
  426. const tnode = el('div', {class: 'tnode' + (collapsed ? ' tcollapsed' : '')});
  427. const cls = ['tline'];
  428. if (selPath === node.path) cls.push('active');
  429. if (c === 0 && selPath !== node.path) cls.push('disabled');
  430. if (node.is_suggested && !node.is_hit) cls.push('suggested');
  431. if (node.is_inferred) cls.push('inferred');
  432. const line = el('div', {class: cls.join(' '), title: node.path + (node.description ? '\n\n' + node.description : '')});
  433. const caret = el('span', {
  434. class: 'tcaret' + (hasChildren ? '' : ' invis'),
  435. onClick: (ev) => {
  436. ev.stopPropagation();
  437. if (state.collapsed.has(node.path)) state.collapsed.delete(node.path);
  438. else state.collapsed.add(node.path);
  439. renderTrees();
  440. }
  441. }, hasChildren ? (collapsed ? '▶' : '▼') : '·');
  442. line.appendChild(caret);
  443. let label = node.name;
  444. if (node.is_suggested && !node.is_hit) label += ' ✦';
  445. if (node.is_inferred) label += ' (推断)';
  446. line.appendChild(el('span', {class:'tname'}, label));
  447. line.appendChild(el('span', {class:'tcount'}, String(c)));
  448. line.addEventListener('click', () => {
  449. if (c === 0 && selPath !== node.path) return;
  450. const k = (key === 'shizhi') ? 'shizhiPath' : 'xingshiPath';
  451. state[k] = (state[k] === node.path) ? null : node.path;
  452. renderAll();
  453. });
  454. tnode.appendChild(line);
  455. for (const ch of node.children || []) renderNode(ch, tnode);
  456. parent.appendChild(tnode);
  457. }
  458. for (const r of roots) renderNode(r, wrap);
  459. }
  460. function renderTrees(){
  461. renderTreeFacet(data.subtree.shizhi || [], 'shizhi', 'shizhiTree', state.shizhiPath, shiDesc);
  462. renderTreeFacet(data.subtree.xingshi || [], 'xingshi', 'xingshiTree', state.xingshiPath, xingDesc);
  463. }
  464. function renderScope(){
  465. $('scopeStat').textContent = '路径树(双 facet)';
  466. $('scopeClear').classList.toggle('show', scopeFilterActive());
  467. renderTrees();
  468. }
  469. function sigPassesChipFilter(sig){
  470. const p = sigParts(sig);
  471. for (const m of state.modInFilter) if (!p.in.has(m)) return false;
  472. for (const m of state.modOutFilter) if (!p.out.has(m)) return false;
  473. return true;
  474. }
  475. function renderModalityChips(){
  476. const renderSide = (mountId, palette, selectedSet, side) => {
  477. const wrap = $(mountId); wrap.innerHTML = '';
  478. for (const m of palette){
  479. const trial = new Set(selectedSet);
  480. if (!trial.has(m)) trial.add(m);
  481. let c = 0;
  482. for (const sig of data.modalities){
  483. const p = sigParts(sig.sig);
  484. const inOK = side === 'in'
  485. ? [...trial].every(x => p.in.has(x)) && [...state.modOutFilter].every(x => p.out.has(x))
  486. : [...state.modInFilter].every(x => p.in.has(x)) && [...trial].every(x => p.out.has(x));
  487. if (inOK) c++;
  488. }
  489. const isActive = selectedSet.has(m);
  490. const chip = el('span', {
  491. class: 'mod-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
  492. title: m + (c === 0 && !isActive ? ' (无匹配)' : ''),
  493. onClick: () => {
  494. if (c === 0 && !isActive) return;
  495. if (selectedSet.has(m)) selectedSet.delete(m);
  496. else selectedSet.add(m);
  497. if (state.modSig && !sigPassesChipFilter(state.modSig)) state.modSig = null;
  498. renderAll();
  499. }
  500. }, m);
  501. wrap.appendChild(chip);
  502. }
  503. };
  504. renderSide('modInChips', SIDE_MODS.in, state.modInFilter, 'in');
  505. renderSide('modOutChips', SIDE_MODS.out, state.modOutFilter, 'out');
  506. }
  507. function renderModalities(){
  508. const baseFiltered = applyFilters('mod');
  509. const counts = {};
  510. for (const w of baseFiltered) counts[w.io_signature] = (counts[w.io_signature]||0)+1;
  511. renderModalityChips();
  512. const wrap = $('modList'); wrap.innerHTML = '';
  513. const visible = data.modalities.filter(m => sigPassesChipFilter(m.sig));
  514. const sorted = [...visible].sort((a,b) => (counts[b.sig]||0) - (counts[a.sig]||0));
  515. if (sorted.length === 0){
  516. wrap.appendChild(el('div', {class:'empty-msg'}, '当前 chip 筛选下没有匹配签名'));
  517. }
  518. for (const m of sorted){
  519. const c = counts[m.sig] || 0;
  520. const node = el('div', {
  521. class: 'item' + (state.modSig === m.sig ? ' active' : '') + (c === 0 && state.modSig !== m.sig ? ' disabled' : ''),
  522. onClick: () => {
  523. if (c === 0 && state.modSig !== m.sig) return;
  524. state.modSig = (state.modSig === m.sig) ? null : m.sig;
  525. renderAll();
  526. }
  527. },
  528. el('span', {class:'item-name'}, m.sig),
  529. el('span', {class:'item-count'}, String(c))
  530. );
  531. wrap.appendChild(node);
  532. }
  533. $('modClear').classList.toggle('show', !!state.modSig || state.modInFilter.size > 0 || state.modOutFilter.size > 0);
  534. }
  535. function vchipStyle(phase){
  536. // Neutral chip styling (color no longer encodes phase).
  537. return 'background:var(--tag-bg);color:var(--fg);border-color:var(--border)';
  538. }
  539. function renderWorkflowCard(w){
  540. const isActive = state.wfKey === w.workflow_key;
  541. const node = el('div', {
  542. class: 'wf' + (isActive ? ' active' : ''),
  543. onClick: () => { state.wfKey = isActive ? null : w.workflow_key; renderWorkflows(); renderDetail(); }
  544. });
  545. const head = el('div', {class:'wf-head'},
  546. el('span', {class:'case-badge'}, '案例'+w.case_index),
  547. el('span', {class:'wf-badge'}, w.workflow_id),
  548. el('span', {class:'wf-meta'}, w.step_count + ' step · ' + w.capability_count + ' cap')
  549. );
  550. node.appendChild(head);
  551. if (w.case_title){
  552. node.appendChild(el('div', {class:'wf-title'}, w.case_title));
  553. }
  554. node.appendChild(el('div', {class:'wf-sig'}, w.io_signature));
  555. node.appendChild(el('div', {class:'wf-sig'}, w.phase_signature));
  556. const seq = el('div', {class:'wf-seq'});
  557. let prevStepIdx = -1;
  558. w.steps.forEach((s, si) => {
  559. if (!s.fragment_keys.length) return;
  560. if (prevStepIdx >= 0) seq.appendChild(el('span', {class:'vsep'}, '›'));
  561. prevStepIdx = si;
  562. s.fragment_keys.forEach((fk, ci) => {
  563. const f = fragByKey.get(fk);
  564. if (!f) return;
  565. if (ci > 0) seq.appendChild(el('span', {class:'vsep'}, '·'));
  566. seq.appendChild(el('span', {
  567. class:'vchip',
  568. style: vchipStyle(s.phase),
  569. title: 'step ' + s.step_id + ' / ' + s.phase + ' / ' + (f.action || '')
  570. }, f.action || '?'));
  571. });
  572. });
  573. node.appendChild(seq);
  574. return node;
  575. }
  576. function renderWorkflows(){
  577. const filtered = applyFilters(null);
  578. const wrap = $('wfList'); wrap.innerHTML = '';
  579. $('wfCnt').textContent = filtered.length + ' / ' + data.workflows.length;
  580. if (!filtered.length){
  581. wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的工序'));
  582. return;
  583. }
  584. for (const w of filtered) wrap.appendChild(renderWorkflowCard(w));
  585. }
  586. function renderCaseDetail(wrap, caseIdx){
  587. const c = casesByIndex[caseIdx];
  588. if (!c) return;
  589. const det = el('details', {class:'case-detail'});
  590. const sumLabel = '📄 源: ' + (c.title || '案例 ' + caseIdx);
  591. det.appendChild(el('summary', null, sumLabel));
  592. const content = el('div', {class:'case-content'});
  593. if (c.author) {
  594. content.appendChild(el('div', {class:'case-author'}, '作者:' + c.author));
  595. }
  596. if (c.feedback && (c.feedback.like_count || c.feedback.comment_count || c.feedback.collect_count || c.feedback.share_count)){
  597. const fb = c.feedback;
  598. const row = el('div', {class:'case-feedback'});
  599. if (fb.like_count != null) row.appendChild(el('span', {class:'fb'}, '👍 ' + fb.like_count));
  600. if (fb.comment_count != null) row.appendChild(el('span', {class:'fb'}, '💬 ' + fb.comment_count));
  601. if (fb.collect_count != null) row.appendChild(el('span', {class:'fb'}, '⭐ ' + fb.collect_count));
  602. if (fb.share_count != null) row.appendChild(el('span', {class:'fb'}, '↗ ' + fb.share_count));
  603. content.appendChild(row);
  604. }
  605. const imgs = (c.images && c.images.length) ? c.images : (c.cover ? [c.cover] : []);
  606. if (imgs.length){
  607. const wrapImg = el('div', {class:'case-images'});
  608. for (const src of imgs) wrapImg.appendChild(el('img', {src, loading:'lazy'}));
  609. content.appendChild(wrapImg);
  610. }
  611. if (c.body){
  612. content.appendChild(el('div', {class:'case-body'}, c.body));
  613. }
  614. if (c.url){
  615. content.appendChild(el('a', {href:c.url, target:'_blank', rel:'noopener', class:'case-link'}, '🔗 访问原始链接'));
  616. }
  617. det.appendChild(content);
  618. wrap.appendChild(det);
  619. }
  620. function renderCap(f){
  621. const cap = el('div', {class:'cap'});
  622. cap.appendChild(el('div', {class:'cap-head'},
  623. el('span', {class:'cap-id'}, f.fragment_id),
  624. el('span', {class:'cap-act'}, f.action || '?'),
  625. el('span', {class:'cap-sig'}, f.modality_signature)
  626. ));
  627. if (f.body) cap.appendChild(el('div', {class:'cap-body'}, f.body));
  628. const ioPills = el('div', {style:'margin-top:4px'});
  629. const ioRow = (lbl, arr, kind) => {
  630. const row = el('div', {class:'io-row'}, el('span', {class:'lbl'}, lbl));
  631. if (!arr || !arr.length){ row.appendChild(el('span', {style:'color:var(--muted)'}, '(无)')); return row; }
  632. for (const x of arr){
  633. const isCfg = x.modality === '模型' || x.modality === '参数';
  634. const txt = (x.description||'') + (x.modality?'['+x.modality+']':'') + (x.relation?' '+x.relation:'');
  635. row.appendChild(el('span', {class:'pill ' + (isCfg ? 'cfg' : kind)}, txt));
  636. }
  637. return row;
  638. };
  639. ioPills.appendChild(ioRow('IN', f.inputs, 'in'));
  640. ioPills.appendChild(ioRow('OUT', f.outputs, 'out'));
  641. cap.appendChild(ioPills);
  642. const renderApply = (label, arr) => {
  643. if (!arr || !arr.length) return null;
  644. const sec = el('div', {style:'margin-top:6px'},
  645. el('div', {style:'font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px'},
  646. 'Apply · ' + label));
  647. for (const e of arr){
  648. const isHighlighted =
  649. (label === '实质' && state.shizhiPath && (shiDesc.get(state.shizhiPath)||[]).includes(e.category_path)) ||
  650. (label === '形式' && state.xingshiPath && (xingDesc.get(state.xingshiPath)||[]).includes(e.category_path));
  651. const isSuggest = e.source === 'suggest';
  652. sec.appendChild(el('div', {
  653. class:'pathline' + (isSuggest ? ' suggest' : ''),
  654. style: isHighlighted ? 'color:#ca8a04;font-weight:600' : ''
  655. }, e.category_path + (isSuggest ? ' ✦' : '')));
  656. if (e.body_excerpt){
  657. sec.appendChild(el('div', {class:'excerpt-line'}, el('span', {class:'em'}, e.body_excerpt)));
  658. }
  659. if (e.body_excerpt_note){
  660. sec.appendChild(el('div', {class:'rationale'}, '— ' + e.body_excerpt_note));
  661. }
  662. if (e.rationale){
  663. sec.appendChild(el('div', {class:'rationale'}, '· ' + e.rationale));
  664. }
  665. }
  666. return sec;
  667. };
  668. const shi = renderApply('实质', f.apply_shizhi);
  669. if (shi) cap.appendChild(shi);
  670. const xing = renderApply('形式', f.apply_xingshi);
  671. if (xing) cap.appendChild(xing);
  672. if (f.tools && f.tools.length){
  673. cap.appendChild(el('div', {class:'cap-meta'}, 'tools: ' + f.tools.join(', ')));
  674. }
  675. if (f.is_alternative_to && f.is_alternative_to.length){
  676. cap.appendChild(el('div', {class:'cap-meta'}, 'alt_to: ' + f.is_alternative_to.join(', ')));
  677. }
  678. return cap;
  679. }
  680. function renderDetail(){
  681. const wrap = $('detailBody'); wrap.innerHTML = '';
  682. const w = data.workflows.find(x => x.workflow_key === state.wfKey);
  683. if (!w){ wrap.appendChild(el('div', {class:'detail-empty'}, '点击左侧工序查看详情')); return; }
  684. wrap.appendChild(el('h2', null,
  685. el('span', {class:'case-badge'}, '案例 '+w.case_index),
  686. el('span', {class:'wf-badge'}, w.workflow_id),
  687. el('span', {class:'pill'}, w.step_count + ' step'),
  688. el('span', {class:'pill'}, w.capability_count + ' cap')
  689. ));
  690. renderCaseDetail(wrap, w.case_index);
  691. const sigSec = el('div', {class:'detail-section'}, el('h3', null, '签名'));
  692. sigSec.appendChild(el('div', {style:'font-size:12px;margin-bottom:4px'},
  693. el('span', {style:'color:var(--muted);margin-right:6px'}, 'IO'),
  694. el('span', null, w.io_signature)));
  695. sigSec.appendChild(el('div', {style:'font-size:12px;margin-bottom:4px'},
  696. el('span', {style:'color:var(--muted);margin-right:6px'}, 'phase'),
  697. el('span', null, w.phase_signature)));
  698. sigSec.appendChild(el('div', {style:'font-size:12px'},
  699. el('span', {style:'color:var(--muted);margin-right:6px'}, 'actions'),
  700. el('span', null, Object.entries(w.actions_multiset).map(([v,c])=>v+(c>1?'×'+c:'')).join(' · '))));
  701. wrap.appendChild(sigSec);
  702. const stepsSec = el('div', {class:'detail-section'}, el('h3', null, '工序步骤'));
  703. w.steps.forEach(s => {
  704. const sec = el('div', {class:'step-sec'});
  705. const head = el('div', {class:'step-head'},
  706. el('span', {class:'step-id'}, 'step '+s.step_id),
  707. el('span', {class:'step-phase', style: vchipStyle(s.phase)}, s.phase || '—'),
  708. el('span', {class:'step-order'}, 'order '+s.order),
  709. s.fragment_keys.length > 1 ? el('span', {class:'step-altnote'}, '⇆ '+s.fragment_keys.length+' 个替代方案') : null
  710. );
  711. sec.appendChild(head);
  712. if (!s.fragment_keys.length){
  713. sec.appendChild(el('div', {class:'empty-msg', style:'padding:10px'}, '(无 capability)'));
  714. } else {
  715. s.fragment_keys.forEach(fk => {
  716. const f = fragByKey.get(fk);
  717. if (!f) return;
  718. sec.appendChild(renderCap(f));
  719. });
  720. }
  721. stepsSec.appendChild(sec);
  722. });
  723. wrap.appendChild(stepsSec);
  724. wrap.appendChild(el('details', null,
  725. el('summary', null, '查看 workflow JSON'),
  726. el('pre', {class:'json-raw'}, JSON.stringify(w, null, 2))
  727. ));
  728. }
  729. function renderChips(){
  730. const c = $('chips'); c.innerHTML = '';
  731. let any = false;
  732. const mk = (lbl, val, onX) => {
  733. any = true;
  734. c.appendChild(el('span', {class:'filter-chip'},
  735. el('span', {class:'lbl'}, lbl),
  736. el('span', null, val),
  737. el('span', {class:'x', onClick: () => { onX(); renderAll(); }}, '×')
  738. ));
  739. };
  740. if (state.actionMode === 'multiset' && state.actionsSelected.size){
  741. mk('动作', [...state.actionsSelected].join(' + '), () => { state.actionsSelected.clear(); });
  742. } else if (state.actionMode === 'phase' && state.phaseSig){
  743. mk('结构', state.phaseSig, () => { state.phaseSig = null; });
  744. }
  745. if (state.shizhiPath) mk('实质', state.shizhiPath.split('/').pop() || state.shizhiPath, () => { state.shizhiPath = null; });
  746. if (state.xingshiPath) mk('形式', state.xingshiPath.split('/').pop() || state.xingshiPath, () => { state.xingshiPath = null; });
  747. if (state.modSig) mk('模态', state.modSig, () => { state.modSig = null; });
  748. $('clearAll').classList.toggle('active', any);
  749. }
  750. function renderStats(){}
  751. function renderAll(){
  752. renderActions();
  753. renderScope();
  754. renderModalities();
  755. renderWorkflows();
  756. renderDetail();
  757. renderChips();
  758. }
  759. function init(){
  760. for (const c of (data.cases || [])) casesByIndex[c.index] = c;
  761. for (const f of data.fragments) fragByKey.set(f.case_index + ':' + f.fragment_id, f);
  762. PHASE_COLORS = data.phaseColors || {};
  763. PHASE_FG = data.phaseFg || {};
  764. SIDE_MODS = (() => {
  765. const inSet = new Set(), outSet = new Set();
  766. for (const m of data.modalities){
  767. const p = sigParts(m.sig);
  768. for (const x of p.in) inSet.add(x);
  769. for (const x of p.out) outSet.add(x);
  770. }
  771. return { in: [...inSet].sort(), out: [...outSet].sort() };
  772. })();
  773. shiDesc = buildDescMap(data.subtree.shizhi || []);
  774. xingDesc = buildDescMap(data.subtree.xingshi || []);
  775. $('loading').style.display = 'none';
  776. $('appRoot').style.display = '';
  777. $('clearAll').addEventListener('click', () => {
  778. state.actionsSelected.clear(); state.phaseSig = null;
  779. state.shizhiPath = null; state.xingshiPath = null;
  780. state.modSig = null;
  781. state.modInFilter.clear(); state.modOutFilter.clear();
  782. renderAll();
  783. });
  784. $('actClear').addEventListener('click', () => {
  785. if (state.actionMode === 'multiset') state.actionsSelected.clear();
  786. else state.phaseSig = null;
  787. renderAll();
  788. });
  789. $('scopeClear').addEventListener('click', () => {
  790. state.shizhiPath = null; state.xingshiPath = null;
  791. renderAll();
  792. });
  793. $('tabMulti').addEventListener('click', () => { state.actionMode = 'multiset'; renderAll(); });
  794. $('tabPhase').addEventListener('click', () => { state.actionMode = 'phase'; renderAll(); });
  795. $('modClear').addEventListener('click', () => {
  796. state.modSig = null; state.modInFilter.clear(); state.modOutFilter.clear();
  797. renderAll();
  798. });
  799. renderStats();
  800. renderAll();
  801. }
  802. fetch('/api/viz/data/workflow')
  803. .then(r => {
  804. if (!r.ok) throw new Error('fetch failed: ' + r.status);
  805. return r.json();
  806. })
  807. .then(d => { data = d; init(); })
  808. .catch(err => {
  809. $('loading').textContent = '加载失败:' + err.message + '。请先上传数据:POST /api/viz/data/workflow';
  810. });
  811. </script>
  812. </body>
  813. </html>