|
|
@@ -2,7 +2,7 @@
|
|
|
<html lang="zh">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
-<title>工序 · 五层筛选</title>
|
|
|
+<title>工序</title>
|
|
|
<style>
|
|
|
:root{
|
|
|
--bg:#f8fafc; --panel:#ffffff; --panel2:#f1f5f9; --border:#e2e8f0;
|
|
|
@@ -12,29 +12,53 @@
|
|
|
--shi-bg:#dbeafe; --shi-fg:#1e40af;
|
|
|
--xing-bg:#ede9fe; --xing-fg:#6d28d9;
|
|
|
--both-bg:#fef3c7; --both-fg:#92400e;
|
|
|
+ --split-w:8px;
|
|
|
+ --left-pct:60%;
|
|
|
}
|
|
|
*{box-sizing:border-box}
|
|
|
html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);
|
|
|
font:13px/1.55 -apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",sans-serif}
|
|
|
header{display:flex;align-items:center;gap:12px;padding:8px 14px;
|
|
|
border-bottom:1px solid var(--border);background:var(--panel);
|
|
|
- position:sticky;top:0;z-index:5;flex-wrap:wrap}
|
|
|
+ position:sticky;top:0;z-index:5;flex-wrap:wrap;height:41px}
|
|
|
h1{font-size:14px;margin:0;font-weight:600}
|
|
|
- .stats{color:var(--muted);font-size:11px}
|
|
|
.clear-all{margin-left:auto;font-size:11px;color:var(--muted);cursor:pointer;
|
|
|
padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:transparent}
|
|
|
.clear-all:hover{color:var(--fg);border-color:var(--accent)}
|
|
|
.clear-all.active{color:var(--warn);border-color:var(--warn)}
|
|
|
|
|
|
- main{display:grid;grid-template-columns:200px 260px 220px 380px 1fr;
|
|
|
- height:calc(100vh - 41px);min-width:1520px}
|
|
|
- section{overflow:auto;border-right:1px solid var(--border);padding:8px 10px}
|
|
|
+ /* two-panel layout with draggable splitter */
|
|
|
+ .app{display:grid;
|
|
|
+ grid-template-columns: minmax(0, var(--left-pct)) var(--split-w) minmax(0, 1fr);
|
|
|
+ height:calc(100vh - 41px);
|
|
|
+ overflow:hidden}
|
|
|
+ .selection{overflow:hidden;display:flex;flex-direction:column;background:var(--panel);
|
|
|
+ min-width:0}
|
|
|
+ .splitter{background:var(--border);cursor:col-resize;position:relative;user-select:none}
|
|
|
+ .splitter::before{content:"";position:absolute;left:50%;top:50%;
|
|
|
+ transform:translate(-50%,-50%);width:2px;height:30px;background:var(--muted);
|
|
|
+ border-radius:1px;opacity:.6}
|
|
|
+ .splitter:hover{background:var(--accent)}
|
|
|
+ .splitter:hover::before{background:#fff;opacity:1}
|
|
|
+ .splitter.dragging{background:var(--accent)}
|
|
|
+ .splitter.dragging::before{background:#fff;opacity:1}
|
|
|
+ .detail-area{overflow:auto;background:var(--panel);padding:0}
|
|
|
+ .preset-btns{display:flex;gap:6px;margin-right:10px}
|
|
|
+ .preset-btn{font-size:11px;padding:3px 9px;border:1px solid var(--border);
|
|
|
+ border-radius:4px;background:transparent;color:var(--muted);cursor:pointer}
|
|
|
+ .preset-btn:hover{color:var(--fg);border-color:var(--accent)}
|
|
|
+ .preset-btn.active{color:var(--accent);border-color:var(--accent);background:#eff6ff}
|
|
|
+
|
|
|
+ /* selection 4-column grid */
|
|
|
+ .sel-cols{display:grid;
|
|
|
+ grid-template-columns:minmax(180px,1fr) minmax(220px,1.1fr) minmax(190px,1fr) minmax(280px,1.6fr);
|
|
|
+ height:100%;overflow-x:auto;overflow-y:hidden}
|
|
|
+ section{overflow:auto;border-right:1px solid var(--border);padding:8px 10px;min-width:0}
|
|
|
section:last-child{border-right:none}
|
|
|
section.fac1{background:var(--panel)}
|
|
|
section.fac2{background:#fafbfd}
|
|
|
section.fac3{background:var(--panel)}
|
|
|
section.fac4{background:#fafbfd}
|
|
|
- section.detail{background:var(--panel)}
|
|
|
|
|
|
.col-title{font-size:10px;text-transform:uppercase;letter-spacing:.6px;
|
|
|
color:var(--muted);margin:2px 4px 8px;display:flex;justify-content:space-between;align-items:baseline}
|
|
|
@@ -55,27 +79,39 @@
|
|
|
.item.active .item-def{color:rgba(255,255,255,.85)}
|
|
|
.item-stack{flex:1;min-width:0}
|
|
|
|
|
|
- /* col-1 action tabs */
|
|
|
- .act-tabs{display:flex;gap:4px;margin:0 0 8px;border-bottom:1px solid var(--border);padding-bottom:5px}
|
|
|
- .act-tab{padding:4px 10px;border-radius:5px 5px 0 0;cursor:pointer;
|
|
|
- font-size:12px;color:var(--muted);background:transparent;
|
|
|
- border:1px solid transparent;border-bottom:none;user-select:none}
|
|
|
- .act-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
|
|
|
- .act-tab:hover:not(.active){color:var(--fg)}
|
|
|
- .act-pane{display:none}
|
|
|
- .act-pane.active{display:block}
|
|
|
- .act-meta{font-size:10px;color:var(--muted);margin:0 4px 8px;line-height:1.4}
|
|
|
-
|
|
|
- /* phase signature list */
|
|
|
- .phasesig{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
|
|
|
- border:1px solid var(--border);background:var(--panel);font-size:11px;line-height:1.4}
|
|
|
- .phasesig:hover:not(.disabled):not(.active){border-color:var(--accent)}
|
|
|
- .phasesig.active{border-color:var(--accent);background:#eff6ff;
|
|
|
+ /* verb-filter (collapsible chip list inside col-1) */
|
|
|
+ .verb-filter{margin:0 0 8px;border:1px solid var(--border);border-radius:5px;
|
|
|
+ background:var(--code)}
|
|
|
+ .verb-filter > summary{cursor:pointer;padding:6px 9px;font-size:11px;
|
|
|
+ color:var(--muted);user-select:none;display:flex;align-items:center;gap:6px;outline:none}
|
|
|
+ .verb-filter > summary:hover{color:var(--fg)}
|
|
|
+ .verb-filter > summary .vf-count{margin-left:auto;font-size:10px;color:var(--accent)}
|
|
|
+ .verb-filter[open] > summary{border-bottom:1px solid var(--border)}
|
|
|
+ .verb-chips{display:flex;flex-wrap:wrap;gap:4px;padding:7px 7px 5px}
|
|
|
+ .verb-chip{font-size:11px;padding:2px 8px;background:var(--tag-bg);border-radius:8px;
|
|
|
+ cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent;
|
|
|
+ display:inline-flex;align-items:center;gap:4px}
|
|
|
+ .verb-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
|
|
|
+ .verb-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
|
|
|
+ .verb-chip.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+ .verb-chip .cnt{font-size:9px;color:var(--muted);background:var(--bg);
|
|
|
+ padding:0 4px;border-radius:6px}
|
|
|
+ .verb-chip.active .cnt{background:rgba(255,255,255,.3);color:#fff}
|
|
|
+
|
|
|
+ /* action sequence list */
|
|
|
+ .actseq{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
|
|
|
+ border:1px solid var(--border);background:var(--panel);font-size:11px;line-height:1.45}
|
|
|
+ .actseq:hover:not(.disabled):not(.active){border-color:var(--accent)}
|
|
|
+ .actseq.active{border-color:var(--accent);background:#eff6ff;
|
|
|
box-shadow:0 0 0 1px var(--accent) inset}
|
|
|
- .phasesig.disabled{opacity:.42;cursor:not-allowed}
|
|
|
- .phasesig-count{float:right;font-size:10px;color:var(--muted);
|
|
|
+ .actseq.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+ .actseq-count{float:right;font-size:10px;color:var(--muted);
|
|
|
background:var(--bg);padding:1px 5px;border-radius:8px}
|
|
|
- .phasesig.active .phasesig-count{background:rgba(255,255,255,.3);color:var(--accent)}
|
|
|
+ .actseq.active .actseq-count{background:rgba(59,130,246,.15);color:var(--accent)}
|
|
|
+ .actseq-chip{display:inline-block;background:var(--tag-bg);padding:1px 6px;
|
|
|
+ border-radius:3px;font-size:10px;margin:1px 2px;font-weight:600;color:var(--fg)}
|
|
|
+ .actseq.active .actseq-chip{background:#dbeafe;color:#1e40af}
|
|
|
+ .actseq-sep{color:var(--muted);font-size:9px;margin:0 1px}
|
|
|
|
|
|
/* tree */
|
|
|
.tnode{margin-left:0}
|
|
|
@@ -112,12 +148,17 @@
|
|
|
.wf-badge{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;
|
|
|
font-weight:600;font-size:10px}
|
|
|
.wf-meta{font-size:10px;color:var(--muted);margin-left:auto}
|
|
|
- .wf-title{font-size:12px;color:#334155;margin:3px 0;line-height:1.4;
|
|
|
- display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
|
.wf-sig{font-size:10px;color:var(--muted);margin:3px 0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
|
.wf-seq{display:flex;flex-wrap:wrap;gap:3px;margin-top:5px;align-items:center}
|
|
|
+ .wf-row{font-size:10px;margin-top:4px;display:flex;align-items:baseline;gap:6px;line-height:1.4}
|
|
|
+ .wf-row .wf-lbl{color:var(--muted);text-transform:uppercase;letter-spacing:.4px;flex-shrink:0;font-size:9px;min-width:28px}
|
|
|
+ .wf-row .wf-val{color:#334155;flex:1;min-width:0}
|
|
|
+ .wf-scope-chip{display:inline-block;background:#e0e7ff;color:#3730a3;padding:1px 6px;
|
|
|
+ border-radius:3px;font-size:10px;margin:1px 2px;font-weight:600}
|
|
|
+ .wf-scope-chip.suggest{background:#ede9fe;color:#6d28d9;font-style:italic}
|
|
|
+ .wf-scope-more{font-size:9px;color:var(--muted);margin-left:3px}
|
|
|
.vchip{font-size:10px;padding:2px 6px;border-radius:3px;font-weight:600;line-height:1.3;
|
|
|
- border:1px solid transparent;white-space:nowrap}
|
|
|
+ border:1px solid transparent;white-space:nowrap;background:var(--tag-bg);color:var(--fg)}
|
|
|
.vsep{color:var(--muted);font-size:9px;line-height:1.3}
|
|
|
|
|
|
/* modality side filter */
|
|
|
@@ -135,34 +176,18 @@
|
|
|
|
|
|
/* detail */
|
|
|
.detail-empty{color:var(--muted);text-align:center;padding:60px 20px;font-size:13px}
|
|
|
+ .detail-pad{padding:14px 18px}
|
|
|
.detail h2{font-size:14px;margin:0 0 8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
|
.detail-section{margin:12px 0}
|
|
|
.detail-section h3{font-size:10px;color:var(--muted);text-transform:uppercase;
|
|
|
letter-spacing:.6px;margin:0 0 6px;font-weight:600}
|
|
|
- .body-text{font-size:12px;line-height:1.6;color:#334155;background:var(--code);
|
|
|
- padding:8px 10px;border-radius:5px;white-space:pre-wrap}
|
|
|
.pill{display:inline-block;padding:1px 6px;background:var(--tag-bg);border-radius:8px;
|
|
|
font-size:11px;margin-right:3px;margin-bottom:3px}
|
|
|
.pill.in{background:var(--shi-bg);color:var(--shi-fg)}
|
|
|
.pill.out{background:var(--xing-bg);color:var(--xing-fg)}
|
|
|
- .pill.cfg{background:var(--both-bg);color:var(--both-fg)}
|
|
|
- .io-row{margin-bottom:4px;font-size:11px}
|
|
|
- .io-row .lbl{color:var(--muted);font-size:10px;margin-right:6px;text-transform:uppercase}
|
|
|
- .pathline{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;
|
|
|
- color:var(--accent);margin-bottom:2px;word-break:break-all}
|
|
|
- .pathline.suggest{color:var(--suggest)}
|
|
|
- .rationale{color:var(--muted);font-size:11px;margin-bottom:3px;margin-left:4px;line-height:1.45}
|
|
|
- .excerpt-line{color:var(--muted);font-size:11px;margin-left:4px;line-height:1.45;margin-bottom:6px;
|
|
|
- border-left:2px solid var(--border);padding-left:6px}
|
|
|
- .excerpt-line .em{color:#334155}
|
|
|
- .json-raw{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
|
|
|
- background:var(--code);padding:8px;border-radius:5px;white-space:pre-wrap;
|
|
|
- color:#334155;max-height:240px;overflow:auto}
|
|
|
- details summary{cursor:pointer;color:var(--muted);font-size:10px;margin:6px 0;user-select:none}
|
|
|
- details summary:hover{color:var(--fg)}
|
|
|
|
|
|
/* case-detail (original-post) section */
|
|
|
- .case-detail{margin:8px 0;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
|
|
+ .case-detail{margin:0 0 8px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
|
|
.case-detail summary{font-weight:bold;color:var(--accent);padding:6px 8px;
|
|
|
background:var(--panel2);border-radius:4px;font-size:11px;cursor:pointer;outline:none;margin:0}
|
|
|
.case-content{padding:10px;background:var(--code);border-radius:5px;
|
|
|
@@ -178,23 +203,41 @@
|
|
|
.case-feedback .fb{display:inline-flex;gap:3px;align-items:center}
|
|
|
.case-author{font-size:11px;color:var(--muted);margin-bottom:6px}
|
|
|
|
|
|
- /* step section */
|
|
|
- .step-sec{margin:10px 0;border:1px solid var(--border);border-radius:6px;background:var(--panel)}
|
|
|
- .step-head{display:flex;align-items:center;gap:8px;padding:6px 10px;
|
|
|
- border-bottom:1px solid var(--border);font-size:11px;background:var(--panel2)}
|
|
|
- .step-id{background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;font-weight:600;font-size:10px}
|
|
|
- .step-phase{padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600}
|
|
|
- .step-order{color:var(--muted);font-size:10px}
|
|
|
- .step-altnote{margin-left:auto;font-size:10px;color:var(--warn)}
|
|
|
- .cap{padding:8px 10px;border-top:1px dashed var(--border)}
|
|
|
- .cap:first-child{border-top:none}
|
|
|
- .cap-head{display:flex;align-items:center;gap:6px;margin-bottom:4px;flex-wrap:wrap}
|
|
|
- .cap-id{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
|
- .cap-act{background:var(--warn);color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
|
- .cap-sig{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--muted)}
|
|
|
- .cap-body{font-size:12px;line-height:1.55;color:#334155;background:var(--code);
|
|
|
- padding:6px 9px;border-radius:4px;white-space:pre-wrap;margin:4px 0}
|
|
|
- .cap-meta{font-size:10px;color:var(--muted);margin-top:3px}
|
|
|
+ /* capability table — light, compact, white-on-gray */
|
|
|
+ .cap-table{width:100%;border-collapse:collapse;background:var(--panel);
|
|
|
+ font-size:11px;border:1px solid var(--border);table-layout:auto}
|
|
|
+ .cap-table th, .cap-table td{
|
|
|
+ border:1px solid var(--border);padding:5px 8px;vertical-align:top;text-align:left;
|
|
|
+ line-height:1.5;background:var(--panel)}
|
|
|
+ .cap-table thead th{background:#f8fafc;font-weight:600;color:var(--muted);
|
|
|
+ position:sticky;top:0;z-index:2;font-size:10px;text-transform:uppercase;
|
|
|
+ letter-spacing:.4px;text-align:left}
|
|
|
+ .cap-table tbody tr.cap-first td{border-top:1px solid var(--border)}
|
|
|
+ .cap-table tbody tr.step-first td{border-top:2px solid var(--accent)}
|
|
|
+ .cap-table tbody tr:first-child td{border-top:none}
|
|
|
+ .cell-step{text-align:center;font-weight:600;color:#334155;font-size:12px;
|
|
|
+ background:#fafbfd}
|
|
|
+ .cell-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
|
|
|
+ color:var(--muted);white-space:nowrap;background:#fafbfd}
|
|
|
+ .cell-action{font-weight:600;color:#334155;background:#fafbfd}
|
|
|
+ .cell-atom-action{color:#334155;font-weight:500}
|
|
|
+ .cell-scope{color:#334155}
|
|
|
+ .cell-scope.suggest{color:var(--suggest);font-style:italic}
|
|
|
+ .cell-tool{font-size:10px;color:#475569;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
|
+ .cell-type{font-size:10px;color:var(--muted)}
|
|
|
+ .cell-impl{color:#334155;max-width:260px;font-size:11px;line-height:1.5}
|
|
|
+ .cell-io{background:#fafbfd}
|
|
|
+ .cell-io .mod-pill{display:inline-block;padding:1px 7px;border-radius:3px;
|
|
|
+ font-size:10px;margin:1px 2px;font-weight:600;
|
|
|
+ background:#e0e7ff;color:#3730a3}
|
|
|
+ .cell-io .mod-pill.output{background:#e0e7ff;color:#3730a3}
|
|
|
+ .cell-dash{color:var(--inferred)}
|
|
|
+ .scope-leaf{display:inline-block;padding:1px 6px;background:#e0e7ff;
|
|
|
+ color:#3730a3;border-radius:3px;font-size:11px;font-weight:600}
|
|
|
+ .scope-leaf.suggest{background:#ede9fe;color:#6d28d9}
|
|
|
+ .scope-facet{font-size:9px;color:var(--muted);margin-left:5px;text-transform:uppercase;letter-spacing:.3px}
|
|
|
+ .scope-path{font-size:9px;color:var(--muted);margin-top:3px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
|
+ .scope-mark{font-size:10px;color:var(--suggest);margin-left:3px}
|
|
|
|
|
|
.empty-msg{color:var(--muted);text-align:center;padding:30px 10px;font-size:11px;font-style:italic}
|
|
|
|
|
|
@@ -216,66 +259,74 @@
|
|
|
<header>
|
|
|
<h1>工序</h1>
|
|
|
<span id="chips"></span>
|
|
|
- <button class="clear-all" id="clearAll">清除全部筛选</button>
|
|
|
+ <div class="preset-btns" style="margin-left:auto">
|
|
|
+ <button class="preset-btn" id="presetLeft" title="选择区为主">◧ 选择</button>
|
|
|
+ <button class="preset-btn" id="presetEven" title="均分">⊟ 均分</button>
|
|
|
+ <button class="preset-btn" id="presetRight" title="详情区为主">◨ 详情</button>
|
|
|
+ </div>
|
|
|
+ <button class="clear-all" id="clearAll">清除筛选</button>
|
|
|
</header>
|
|
|
|
|
|
-<main>
|
|
|
- <section class="fac1" id="actCol">
|
|
|
- <div class="col-title">
|
|
|
- <span>① 动作</span>
|
|
|
- <span class="col-clear" id="actClear" style="margin-left:auto">清除</span>
|
|
|
+<div class="app" id="app">
|
|
|
+ <div class="selection">
|
|
|
+ <div class="sel-cols">
|
|
|
+ <section class="fac1" id="actCol">
|
|
|
+ <div class="col-title">
|
|
|
+ <span>① 动作</span>
|
|
|
+ <span class="col-clear" id="actClear" style="margin-left:auto">清除</span>
|
|
|
+ </div>
|
|
|
+ <details class="verb-filter" id="verbFilter">
|
|
|
+ <summary>
|
|
|
+ <span>动词表</span>
|
|
|
+ <span id="vfSummary" class="vf-count"></span>
|
|
|
+ </summary>
|
|
|
+ <div class="verb-chips" id="verbChips"></div>
|
|
|
+ </details>
|
|
|
+ <div id="actSeqList"></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="fac2" id="scopeCol">
|
|
|
+ <div class="col-title">
|
|
|
+ <span>② 作用域</span>
|
|
|
+ <span class="col-count" id="scopeStat"></span>
|
|
|
+ <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
|
|
|
+ </div>
|
|
|
+ <div class="facet-title">实质</div>
|
|
|
+ <div id="shizhiTree"></div>
|
|
|
+ <div class="facet-title">形式</div>
|
|
|
+ <div id="xingshiTree"></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="fac3" id="modCol">
|
|
|
+ <div class="col-title"><span>③ 输入 → 输出模态</span><span class="col-clear" id="modClear">清除</span></div>
|
|
|
+ <div class="mod-filter">
|
|
|
+ <div class="mod-filter-row">
|
|
|
+ <span class="mod-filter-lbl">输入含</span>
|
|
|
+ <span class="mod-chips" id="modInChips"></span>
|
|
|
+ </div>
|
|
|
+ <div class="mod-filter-row">
|
|
|
+ <span class="mod-filter-lbl">输出含</span>
|
|
|
+ <span class="mod-chips" id="modOutChips"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div id="modList"></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="fac4" id="wfCol">
|
|
|
+ <div class="col-title"><span>④ 工序</span><span class="col-count" id="wfCnt"></span></div>
|
|
|
+ <div id="wfList"></div>
|
|
|
+ </section>
|
|
|
</div>
|
|
|
- <div class="act-tabs">
|
|
|
- <div class="act-tab" data-mode="multiset" id="tabMulti">动作</div>
|
|
|
- <div class="act-tab" data-mode="phase" id="tabPhase">结构</div>
|
|
|
- </div>
|
|
|
- <div class="act-pane" id="paneMulti">
|
|
|
- <div class="act-meta">多选 · 工序须包含所有选中动作(AND)</div>
|
|
|
- <div id="actList"></div>
|
|
|
- </div>
|
|
|
- <div class="act-pane" id="panePhase">
|
|
|
- <div class="act-meta">单选 · 完整 phase 序列</div>
|
|
|
- <div id="phaseList"></div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
+ </div>
|
|
|
|
|
|
- <section class="fac2" id="scopeCol">
|
|
|
- <div class="col-title">
|
|
|
- <span>② 作用域</span>
|
|
|
- <span class="col-count" id="scopeStat"></span>
|
|
|
- <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
|
|
|
- </div>
|
|
|
- <div class="facet-title">实质</div>
|
|
|
- <div id="shizhiTree"></div>
|
|
|
- <div class="facet-title">形式</div>
|
|
|
- <div id="xingshiTree"></div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="fac3" id="modCol">
|
|
|
- <div class="col-title"><span>③ 输入 → 输出模态</span><span class="col-clear" id="modClear">清除</span></div>
|
|
|
- <div class="mod-filter">
|
|
|
- <div class="mod-filter-row">
|
|
|
- <span class="mod-filter-lbl">输入含</span>
|
|
|
- <span class="mod-chips" id="modInChips"></span>
|
|
|
- </div>
|
|
|
- <div class="mod-filter-row">
|
|
|
- <span class="mod-filter-lbl">输出含</span>
|
|
|
- <span class="mod-chips" id="modOutChips"></span>
|
|
|
- </div>
|
|
|
+ <div class="splitter" id="splitter"></div>
|
|
|
+
|
|
|
+ <div class="detail-area">
|
|
|
+ <div class="detail-pad" id="detailBody">
|
|
|
+ <div class="detail-empty">点击左侧工序查看详情</div>
|
|
|
</div>
|
|
|
- <div id="modList"></div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="fac4" id="wfCol">
|
|
|
- <div class="col-title"><span>④ 工序</span><span class="col-count" id="wfCnt"></span></div>
|
|
|
- <div id="wfList"></div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="detail" id="detailCol">
|
|
|
- <div class="col-title"><span>⑤ 工序详情</span></div>
|
|
|
- <div id="detailBody"><div class="detail-empty">点击左侧工序查看详情</div></div>
|
|
|
- </section>
|
|
|
-</main>
|
|
|
+ </div>
|
|
|
+</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
@@ -285,13 +336,10 @@ let SIDE_MODS = null;
|
|
|
let shiDesc = null;
|
|
|
let xingDesc = null;
|
|
|
const fragByKey = new Map();
|
|
|
-let PHASE_COLORS = {};
|
|
|
-let PHASE_FG = {};
|
|
|
|
|
|
const state = {
|
|
|
- actionMode: 'multiset',
|
|
|
- actionsSelected: new Set(),
|
|
|
- phaseSig: null,
|
|
|
+ actionSeqSig: null, // selected action-sequence signature
|
|
|
+ actionsSelected: new Set(),// selected verbs (AND filter)
|
|
|
shizhiPath: null,
|
|
|
xingshiPath: null,
|
|
|
modSig: null,
|
|
|
@@ -301,6 +349,8 @@ const state = {
|
|
|
collapsed: new Set(),
|
|
|
};
|
|
|
|
|
|
+const PHASE_IGNORE = new Set(["非制作"]);
|
|
|
+
|
|
|
function $(id){return document.getElementById(id)}
|
|
|
function el(tag, attrs, ...kids){
|
|
|
const e = document.createElement(tag);
|
|
|
@@ -349,9 +399,28 @@ function workflowMatchesPath(w, key, selPath, descMap){
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+// action sequence of a workflow, ignoring 非制作 steps
|
|
|
+function actionSeqOfWorkflow(w){
|
|
|
+ const seq = [];
|
|
|
+ for (const s of w.steps){
|
|
|
+ if (PHASE_IGNORE.has(s.phase)) continue;
|
|
|
+ for (const fk of s.fragment_keys){
|
|
|
+ const f = fragByKey.get(fk);
|
|
|
+ if (f && f.action) seq.push(f.action);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return seq;
|
|
|
+}
|
|
|
+function actionSigOfWorkflow(w){
|
|
|
+ return actionSeqOfWorkflow(w).join(' → ');
|
|
|
+}
|
|
|
+// action set ignoring 非制作 (for verb filter)
|
|
|
+function actionSetOfWorkflow(w){
|
|
|
+ return new Set(actionSeqOfWorkflow(w));
|
|
|
+}
|
|
|
+
|
|
|
function actionFilterActive(){
|
|
|
- if (state.actionMode === 'multiset') return state.actionsSelected.size > 0;
|
|
|
- return !!state.phaseSig;
|
|
|
+ return !!state.actionSeqSig || state.actionsSelected.size > 0;
|
|
|
}
|
|
|
function scopeFilterActive(){
|
|
|
return !!(state.shizhiPath || state.xingshiPath);
|
|
|
@@ -360,11 +429,12 @@ function scopeFilterActive(){
|
|
|
function applyFilters(except){
|
|
|
return data.workflows.filter(w => {
|
|
|
if (except !== 'action' && actionFilterActive()){
|
|
|
- if (state.actionMode === 'multiset'){
|
|
|
- const set = new Set(w.actions_set);
|
|
|
+ if (state.actionSeqSig){
|
|
|
+ if (actionSigOfWorkflow(w) !== state.actionSeqSig) return false;
|
|
|
+ }
|
|
|
+ if (state.actionsSelected.size > 0){
|
|
|
+ const set = actionSetOfWorkflow(w);
|
|
|
for (const v of state.actionsSelected) if (!set.has(v)) return false;
|
|
|
- } else {
|
|
|
- if (w.phase_signature !== state.phaseSig) return false;
|
|
|
}
|
|
|
}
|
|
|
if (except !== 'scope' && scopeFilterActive()){
|
|
|
@@ -376,63 +446,82 @@ function applyFilters(except){
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function renderActionMultiset(){
|
|
|
+function renderVerbFilter(){
|
|
|
const baseFiltered = applyFilters('action');
|
|
|
const counts = {};
|
|
|
- for (const w of baseFiltered) for (const v of w.actions_set) counts[v] = (counts[v]||0)+1;
|
|
|
- const wrap = $('actList'); wrap.innerHTML = '';
|
|
|
+ for (const w of baseFiltered) for (const v of actionSetOfWorkflow(w)) counts[v] = (counts[v]||0)+1;
|
|
|
+ const wrap = $('verbChips'); wrap.innerHTML = '';
|
|
|
for (const a of data.actions){
|
|
|
const c = counts[a.verb] || 0;
|
|
|
const isActive = state.actionsSelected.has(a.verb);
|
|
|
- const node = el('div', {
|
|
|
- class: 'item' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
|
|
|
+ const chip = el('span', {
|
|
|
+ class: 'verb-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
|
|
|
+ title: a.verb + (a.definition ? '\n\n' + a.definition : ''),
|
|
|
onClick: () => {
|
|
|
if (c === 0 && !isActive) return;
|
|
|
if (state.actionsSelected.has(a.verb)) state.actionsSelected.delete(a.verb);
|
|
|
else state.actionsSelected.add(a.verb);
|
|
|
renderAll();
|
|
|
}
|
|
|
- });
|
|
|
- const stack = el('div', {class:'item-stack'},
|
|
|
- el('div', {class:'item-name'}, a.verb),
|
|
|
- a.definition ? el('div', {class:'item-def'}, a.definition) : null);
|
|
|
- node.appendChild(stack);
|
|
|
- node.appendChild(el('span', {class:'item-count'}, String(c)));
|
|
|
- wrap.appendChild(node);
|
|
|
+ },
|
|
|
+ el('span', null, a.verb),
|
|
|
+ el('span', {class:'cnt'}, String(c))
|
|
|
+ );
|
|
|
+ wrap.appendChild(chip);
|
|
|
}
|
|
|
+ const n = state.actionsSelected.size;
|
|
|
+ $('vfSummary').textContent = n > 0 ? (n + ' 项已选') : (data.actions.length + ' 个动词');
|
|
|
+ if (n > 0) $('verbFilter').setAttribute('open', '');
|
|
|
}
|
|
|
|
|
|
-function renderActionPhase(){
|
|
|
+function renderActionSeqs(){
|
|
|
+ // collect all distinct action signatures across the filtered workflow set
|
|
|
const baseFiltered = applyFilters('action');
|
|
|
const counts = {};
|
|
|
- for (const w of baseFiltered) counts[w.phase_signature] = (counts[w.phase_signature]||0)+1;
|
|
|
- const wrap = $('phaseList'); wrap.innerHTML = '';
|
|
|
- const sigs = [...data.phaseSignatures].sort((a, b) => (counts[b]||0) - (counts[a]||0) || a.localeCompare(b));
|
|
|
+ const examples = {};
|
|
|
+ for (const w of data.workflows){
|
|
|
+ const sig = actionSigOfWorkflow(w);
|
|
|
+ if (!sig) continue;
|
|
|
+ if (!(sig in counts)) { counts[sig] = 0; examples[sig] = actionSeqOfWorkflow(w); }
|
|
|
+ }
|
|
|
+ // count under current other filters
|
|
|
+ const filteredCount = {};
|
|
|
+ for (const w of baseFiltered){
|
|
|
+ const sig = actionSigOfWorkflow(w);
|
|
|
+ if (!sig) continue;
|
|
|
+ filteredCount[sig] = (filteredCount[sig]||0)+1;
|
|
|
+ }
|
|
|
+ const wrap = $('actSeqList'); wrap.innerHTML = '';
|
|
|
+ const sigs = Object.keys(counts).sort((a,b) => (filteredCount[b]||0) - (filteredCount[a]||0) || a.localeCompare(b));
|
|
|
+ if (!sigs.length){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '没有动作序列'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
for (const sig of sigs){
|
|
|
- const c = counts[sig] || 0;
|
|
|
- const isActive = state.phaseSig === sig;
|
|
|
+ const c = filteredCount[sig] || 0;
|
|
|
+ const isActive = state.actionSeqSig === sig;
|
|
|
const node = el('div', {
|
|
|
- class: 'phasesig' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
|
|
|
+ class: 'actseq' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
|
|
|
onClick: () => {
|
|
|
if (c === 0 && !isActive) return;
|
|
|
- state.phaseSig = isActive ? null : sig;
|
|
|
+ state.actionSeqSig = isActive ? null : sig;
|
|
|
renderAll();
|
|
|
}
|
|
|
});
|
|
|
- node.appendChild(el('span', {class:'phasesig-count'}, String(c)));
|
|
|
- node.appendChild(document.createTextNode(sig));
|
|
|
+ node.appendChild(el('span', {class:'actseq-count'}, String(c)));
|
|
|
+ const seq = examples[sig];
|
|
|
+ seq.forEach((v, i) => {
|
|
|
+ if (i > 0) node.appendChild(el('span', {class:'actseq-sep'}, ' › '));
|
|
|
+ node.appendChild(el('span', {class:'actseq-chip'}, v));
|
|
|
+ });
|
|
|
wrap.appendChild(node);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function renderActions(){
|
|
|
- $('tabMulti').classList.toggle('active', state.actionMode === 'multiset');
|
|
|
- $('tabPhase').classList.toggle('active', state.actionMode === 'phase');
|
|
|
- $('paneMulti').classList.toggle('active', state.actionMode === 'multiset');
|
|
|
- $('panePhase').classList.toggle('active', state.actionMode === 'phase');
|
|
|
$('actClear').classList.toggle('show', actionFilterActive());
|
|
|
- if (state.actionMode === 'multiset') renderActionMultiset();
|
|
|
- else renderActionPhase();
|
|
|
+ renderVerbFilter();
|
|
|
+ renderActionSeqs();
|
|
|
}
|
|
|
|
|
|
function renderTreeFacet(roots, key, mountId, selPath, descMap){
|
|
|
@@ -571,11 +660,6 @@ function renderModalities(){
|
|
|
$('modClear').classList.toggle('show', !!state.modSig || state.modInFilter.size > 0 || state.modOutFilter.size > 0);
|
|
|
}
|
|
|
|
|
|
-function vchipStyle(phase){
|
|
|
- // Neutral chip styling (color no longer encodes phase).
|
|
|
- return 'background:var(--tag-bg);color:var(--fg);border-color:var(--border)';
|
|
|
-}
|
|
|
-
|
|
|
function renderWorkflowCard(w){
|
|
|
const isActive = state.wfKey === w.workflow_key;
|
|
|
const node = el('div', {
|
|
|
@@ -583,35 +667,62 @@ function renderWorkflowCard(w){
|
|
|
onClick: () => { state.wfKey = isActive ? null : w.workflow_key; renderWorkflows(); renderDetail(); }
|
|
|
});
|
|
|
const head = el('div', {class:'wf-head'},
|
|
|
- el('span', {class:'case-badge'}, '案例'+w.case_index),
|
|
|
- el('span', {class:'wf-badge'}, w.workflow_id),
|
|
|
- el('span', {class:'wf-meta'}, w.step_count + ' step · ' + w.capability_count + ' cap')
|
|
|
+ el('span', {class:'wf-meta', style:'margin-left:0'}, w.step_count + ' step · ' + w.capability_count + ' cap')
|
|
|
);
|
|
|
node.appendChild(head);
|
|
|
- if (w.case_title){
|
|
|
- node.appendChild(el('div', {class:'wf-title'}, w.case_title));
|
|
|
- }
|
|
|
- node.appendChild(el('div', {class:'wf-sig'}, w.io_signature));
|
|
|
- node.appendChild(el('div', {class:'wf-sig'}, w.phase_signature));
|
|
|
|
|
|
+ // 动作序列
|
|
|
const seq = el('div', {class:'wf-seq'});
|
|
|
- let prevStepIdx = -1;
|
|
|
- w.steps.forEach((s, si) => {
|
|
|
- if (!s.fragment_keys.length) return;
|
|
|
- if (prevStepIdx >= 0) seq.appendChild(el('span', {class:'vsep'}, '›'));
|
|
|
- prevStepIdx = si;
|
|
|
- s.fragment_keys.forEach((fk, ci) => {
|
|
|
- const f = fragByKey.get(fk);
|
|
|
- if (!f) return;
|
|
|
- if (ci > 0) seq.appendChild(el('span', {class:'vsep'}, '·'));
|
|
|
- seq.appendChild(el('span', {
|
|
|
- class:'vchip',
|
|
|
- style: vchipStyle(s.phase),
|
|
|
- title: 'step ' + s.step_id + ' / ' + s.phase + ' / ' + (f.action || '')
|
|
|
- }, f.action || '?'));
|
|
|
+ const visibleSeq = actionSeqOfWorkflow(w);
|
|
|
+ if (visibleSeq.length){
|
|
|
+ visibleSeq.forEach((v, i) => {
|
|
|
+ if (i > 0) seq.appendChild(el('span', {class:'vsep'}, '›'));
|
|
|
+ seq.appendChild(el('span', {class:'vchip'}, v));
|
|
|
});
|
|
|
- });
|
|
|
+ } else {
|
|
|
+ seq.appendChild(el('span', {style:'font-size:10px;color:var(--muted)'}, '—'));
|
|
|
+ }
|
|
|
node.appendChild(seq);
|
|
|
+
|
|
|
+ // 作用域 (leaf names from apply paths)
|
|
|
+ const scopeLeaves = [];
|
|
|
+ const seenLeaf = new Set();
|
|
|
+ const addLeaves = (paths, facet) => {
|
|
|
+ for (const p of (paths || [])){
|
|
|
+ const leaf = (p || '').split('/').filter(Boolean).pop();
|
|
|
+ if (!leaf) continue;
|
|
|
+ const key = facet + ':' + leaf;
|
|
|
+ if (seenLeaf.has(key)) continue;
|
|
|
+ seenLeaf.add(key);
|
|
|
+ scopeLeaves.push({leaf, facet, path:p});
|
|
|
+ }
|
|
|
+ };
|
|
|
+ addLeaves(w.apply_shizhi_paths, '实质');
|
|
|
+ addLeaves(w.apply_xingshi_paths, '形式');
|
|
|
+ const scopeRow = el('div', {class:'wf-row'},
|
|
|
+ el('span', {class:'wf-lbl'}, '作用域'));
|
|
|
+ const scopeVal = el('span', {class:'wf-val'});
|
|
|
+ if (scopeLeaves.length){
|
|
|
+ const MAX = 6;
|
|
|
+ scopeLeaves.slice(0, MAX).forEach(s => {
|
|
|
+ scopeVal.appendChild(el('span', {class:'wf-scope-chip', title:s.path}, s.leaf));
|
|
|
+ });
|
|
|
+ if (scopeLeaves.length > MAX){
|
|
|
+ scopeVal.appendChild(el('span', {class:'wf-scope-more'}, '+'+(scopeLeaves.length - MAX)));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ scopeVal.appendChild(el('span', {style:'color:var(--muted)'}, '—'));
|
|
|
+ }
|
|
|
+ scopeRow.appendChild(scopeVal);
|
|
|
+ node.appendChild(scopeRow);
|
|
|
+
|
|
|
+ // IO
|
|
|
+ const ioRow = el('div', {class:'wf-row'},
|
|
|
+ el('span', {class:'wf-lbl'}, 'IO'),
|
|
|
+ el('span', {class:'wf-val', style:'font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--muted)'}, w.io_signature)
|
|
|
+ );
|
|
|
+ node.appendChild(ioRow);
|
|
|
+
|
|
|
return node;
|
|
|
}
|
|
|
|
|
|
@@ -663,68 +774,129 @@ function renderCaseDetail(wrap, caseIdx){
|
|
|
wrap.appendChild(det);
|
|
|
}
|
|
|
|
|
|
-function renderCap(f){
|
|
|
- const cap = el('div', {class:'cap'});
|
|
|
- cap.appendChild(el('div', {class:'cap-head'},
|
|
|
- el('span', {class:'cap-id'}, f.fragment_id),
|
|
|
- el('span', {class:'cap-act'}, f.action || '?'),
|
|
|
- el('span', {class:'cap-sig'}, f.modality_signature)
|
|
|
+// build atomic rows from a fragment: apply_shizhi + apply_xingshi entries, with markers
|
|
|
+function buildAtomicRows(f){
|
|
|
+ const rows = [];
|
|
|
+ for (const e of (f.apply_shizhi || [])){
|
|
|
+ rows.push({
|
|
|
+ facet: '实质',
|
|
|
+ category_path: e.category_path,
|
|
|
+ source: e.source,
|
|
|
+ leaf: (e.category_path || '').split('/').filter(Boolean).pop() || '—',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ for (const e of (f.apply_xingshi || [])){
|
|
|
+ rows.push({
|
|
|
+ facet: '形式',
|
|
|
+ category_path: e.category_path,
|
|
|
+ source: e.source,
|
|
|
+ leaf: (e.category_path || '').split('/').filter(Boolean).pop() || '—',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (!rows.length) rows.push({ facet:'—', category_path:'', source:'apply', leaf:'—', empty:true });
|
|
|
+ return rows;
|
|
|
+}
|
|
|
+
|
|
|
+function renderCapTable(w){
|
|
|
+ const table = el('table', {class:'cap-table'});
|
|
|
+ const thead = el('thead');
|
|
|
+ thead.appendChild(el('tr', null,
|
|
|
+ el('th', null, '步骤'),
|
|
|
+ el('th', null, '能力动作'),
|
|
|
+ el('th', null, '原子能力动作'),
|
|
|
+ el('th', null, '原子能力作用域'),
|
|
|
+ el('th', null, '原子能力Tool'),
|
|
|
+ el('th', null, '原子能力类型'),
|
|
|
+ el('th', null, '原子能力实现'),
|
|
|
+ el('th', null, '能力输入'),
|
|
|
+ el('th', null, '能力输出')
|
|
|
));
|
|
|
- if (f.body) cap.appendChild(el('div', {class:'cap-body'}, f.body));
|
|
|
-
|
|
|
- const ioPills = el('div', {style:'margin-top:4px'});
|
|
|
- const ioRow = (lbl, arr, kind) => {
|
|
|
- const row = el('div', {class:'io-row'}, el('span', {class:'lbl'}, lbl));
|
|
|
- if (!arr || !arr.length){ row.appendChild(el('span', {style:'color:var(--muted)'}, '(无)')); return row; }
|
|
|
- for (const x of arr){
|
|
|
- const isCfg = x.modality === '模型' || x.modality === '参数';
|
|
|
- const txt = (x.description||'') + (x.modality?'['+x.modality+']':'') + (x.relation?' '+x.relation:'');
|
|
|
- row.appendChild(el('span', {class:'pill ' + (isCfg ? 'cfg' : kind)}, txt));
|
|
|
- }
|
|
|
- return row;
|
|
|
- };
|
|
|
- ioPills.appendChild(ioRow('IN', f.inputs, 'in'));
|
|
|
- ioPills.appendChild(ioRow('OUT', f.outputs, 'out'));
|
|
|
- cap.appendChild(ioPills);
|
|
|
-
|
|
|
- const renderApply = (label, arr) => {
|
|
|
- if (!arr || !arr.length) return null;
|
|
|
- const sec = el('div', {style:'margin-top:6px'},
|
|
|
- el('div', {style:'font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:3px'},
|
|
|
- 'Apply · ' + label));
|
|
|
- for (const e of arr){
|
|
|
- const isHighlighted =
|
|
|
- (label === '实质' && state.shizhiPath && (shiDesc.get(state.shizhiPath)||[]).includes(e.category_path)) ||
|
|
|
- (label === '形式' && state.xingshiPath && (xingDesc.get(state.xingshiPath)||[]).includes(e.category_path));
|
|
|
- const isSuggest = e.source === 'suggest';
|
|
|
- sec.appendChild(el('div', {
|
|
|
- class:'pathline' + (isSuggest ? ' suggest' : ''),
|
|
|
- style: isHighlighted ? 'color:#ca8a04;font-weight:600' : ''
|
|
|
- }, e.category_path + (isSuggest ? ' ✦' : '')));
|
|
|
- if (e.body_excerpt){
|
|
|
- sec.appendChild(el('div', {class:'excerpt-line'}, el('span', {class:'em'}, e.body_excerpt)));
|
|
|
- }
|
|
|
- if (e.body_excerpt_note){
|
|
|
- sec.appendChild(el('div', {class:'rationale'}, '— ' + e.body_excerpt_note));
|
|
|
- }
|
|
|
- if (e.rationale){
|
|
|
- sec.appendChild(el('div', {class:'rationale'}, '· ' + e.rationale));
|
|
|
- }
|
|
|
- }
|
|
|
- return sec;
|
|
|
- };
|
|
|
- const shi = renderApply('实质', f.apply_shizhi);
|
|
|
- if (shi) cap.appendChild(shi);
|
|
|
- const xing = renderApply('形式', f.apply_xingshi);
|
|
|
- if (xing) cap.appendChild(xing);
|
|
|
+ table.appendChild(thead);
|
|
|
|
|
|
- if (f.tools && f.tools.length){
|
|
|
- cap.appendChild(el('div', {class:'cap-meta'}, 'tools: ' + f.tools.join(', ')));
|
|
|
- }
|
|
|
- if (f.is_alternative_to && f.is_alternative_to.length){
|
|
|
- cap.appendChild(el('div', {class:'cap-meta'}, 'alt_to: ' + f.is_alternative_to.join(', ')));
|
|
|
+ const tbody = el('tbody');
|
|
|
+
|
|
|
+ // pre-compute atom rows per cap to know rowspan totals per step
|
|
|
+ const stepRows = []; // [{step, caps: [{f, atoms}], totalRows}]
|
|
|
+ for (const s of w.steps){
|
|
|
+ const caps = [];
|
|
|
+ let total = 0;
|
|
|
+ for (const fk of s.fragment_keys){
|
|
|
+ const f = fragByKey.get(fk);
|
|
|
+ if (!f) continue;
|
|
|
+ const atoms = buildAtomicRows(f);
|
|
|
+ caps.push({f, atoms});
|
|
|
+ total += atoms.length;
|
|
|
+ }
|
|
|
+ if (caps.length === 0) continue;
|
|
|
+ stepRows.push({step:s, caps, totalRows: total});
|
|
|
}
|
|
|
- return cap;
|
|
|
+
|
|
|
+ let stepIdx = 0;
|
|
|
+ stepRows.forEach((sr, si) => {
|
|
|
+ stepIdx += 1;
|
|
|
+ sr.caps.forEach((cp, ci) => {
|
|
|
+ const {f, atoms} = cp;
|
|
|
+ const N = atoms.length;
|
|
|
+ const tools = (f.tools && f.tools.length) ? f.tools.join(' / ') : '—';
|
|
|
+ const inMods = (f.in_modalities && f.in_modalities.length) ? f.in_modalities : [];
|
|
|
+ const outMods = (f.out_modalities && f.out_modalities.length) ? f.out_modalities : [];
|
|
|
+
|
|
|
+ const renderIO = (mods, kind) => {
|
|
|
+ if (!mods.length) return el('span', {class:'cell-dash'}, '—');
|
|
|
+ const box = el('div');
|
|
|
+ for (const m of mods) box.appendChild(el('span', {class:'mod-pill ' + kind}, m));
|
|
|
+ return box;
|
|
|
+ };
|
|
|
+
|
|
|
+ atoms.forEach((a, ai) => {
|
|
|
+ const isStepFirst = ai === 0 && ci === 0;
|
|
|
+ const isCapFirst = ai === 0;
|
|
|
+ const trClass = isStepFirst ? 'step-first' : (isCapFirst ? 'cap-first' : '');
|
|
|
+ const tr = el('tr', trClass ? {class: trClass} : null);
|
|
|
+
|
|
|
+ if (isStepFirst){
|
|
|
+ tr.appendChild(el('td', {class:'cell-step', rowSpan: sr.totalRows}, String(stepIdx)));
|
|
|
+ }
|
|
|
+ if (isCapFirst){
|
|
|
+ tr.appendChild(el('td', {class:'cell-action', rowSpan:N, title:f.fragment_id}, f.action || '—'));
|
|
|
+ }
|
|
|
+ // 原子能力动作: fallback to cap.action
|
|
|
+ tr.appendChild(el('td', {class:'cell-atom-action'},
|
|
|
+ a.empty ? el('span', {class:'cell-dash'}, '—') : (f.action || '—')));
|
|
|
+ // 原子能力作用域
|
|
|
+ if (a.empty){
|
|
|
+ tr.appendChild(el('td', {class:'cell-dash'}, '—'));
|
|
|
+ } else {
|
|
|
+ const cell = el('td', {
|
|
|
+ class:'cell-scope' + (a.source === 'suggest' ? ' suggest' : ''),
|
|
|
+ title: a.category_path
|
|
|
+ });
|
|
|
+ const labelLine = el('div');
|
|
|
+ const leaf = el('span', {class:'scope-leaf' + (a.source === 'suggest' ? ' suggest' : '')}, a.leaf);
|
|
|
+ labelLine.appendChild(leaf);
|
|
|
+ if (a.source === 'suggest') labelLine.appendChild(el('span', {class:'scope-mark'}, '✦'));
|
|
|
+ labelLine.appendChild(el('span', {class:'scope-facet'}, a.facet));
|
|
|
+ cell.appendChild(labelLine);
|
|
|
+ cell.appendChild(el('div', {class:'scope-path'}, a.category_path));
|
|
|
+ tr.appendChild(cell);
|
|
|
+ }
|
|
|
+ // 原子能力Tool
|
|
|
+ tr.appendChild(el('td', {class:'cell-tool'}, tools));
|
|
|
+ // 原子能力类型
|
|
|
+ tr.appendChild(el('td', {class:'cell-type'}, 'prompt'));
|
|
|
+ // 原子能力实现
|
|
|
+ tr.appendChild(el('td', {class:'cell-impl'}, el('span', {class:'cell-dash'}, '—')));
|
|
|
+
|
|
|
+ if (isCapFirst){
|
|
|
+ tr.appendChild(el('td', {class:'cell-io', rowSpan:N}, renderIO(inMods, 'input')));
|
|
|
+ tr.appendChild(el('td', {class:'cell-io', rowSpan:N}, renderIO(outMods, 'output')));
|
|
|
+ }
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+ table.appendChild(tbody);
|
|
|
+ return table;
|
|
|
}
|
|
|
|
|
|
function renderDetail(){
|
|
|
@@ -746,40 +918,13 @@ function renderDetail(){
|
|
|
el('span', {style:'color:var(--muted);margin-right:6px'}, 'IO'),
|
|
|
el('span', null, w.io_signature)));
|
|
|
sigSec.appendChild(el('div', {style:'font-size:12px;margin-bottom:4px'},
|
|
|
- el('span', {style:'color:var(--muted);margin-right:6px'}, 'phase'),
|
|
|
- el('span', null, w.phase_signature)));
|
|
|
- sigSec.appendChild(el('div', {style:'font-size:12px'},
|
|
|
- el('span', {style:'color:var(--muted);margin-right:6px'}, 'actions'),
|
|
|
- el('span', null, Object.entries(w.actions_multiset).map(([v,c])=>v+(c>1?'×'+c:'')).join(' · '))));
|
|
|
+ el('span', {style:'color:var(--muted);margin-right:6px'}, '动作序列'),
|
|
|
+ el('span', null, actionSigOfWorkflow(w) || '—')));
|
|
|
wrap.appendChild(sigSec);
|
|
|
|
|
|
- const stepsSec = el('div', {class:'detail-section'}, el('h3', null, '工序步骤'));
|
|
|
- w.steps.forEach(s => {
|
|
|
- const sec = el('div', {class:'step-sec'});
|
|
|
- const head = el('div', {class:'step-head'},
|
|
|
- el('span', {class:'step-id'}, 'step '+s.step_id),
|
|
|
- el('span', {class:'step-phase', style: vchipStyle(s.phase)}, s.phase || '—'),
|
|
|
- el('span', {class:'step-order'}, 'order '+s.order),
|
|
|
- s.fragment_keys.length > 1 ? el('span', {class:'step-altnote'}, '⇆ '+s.fragment_keys.length+' 个替代方案') : null
|
|
|
- );
|
|
|
- sec.appendChild(head);
|
|
|
- if (!s.fragment_keys.length){
|
|
|
- sec.appendChild(el('div', {class:'empty-msg', style:'padding:10px'}, '(无 capability)'));
|
|
|
- } else {
|
|
|
- s.fragment_keys.forEach(fk => {
|
|
|
- const f = fragByKey.get(fk);
|
|
|
- if (!f) return;
|
|
|
- sec.appendChild(renderCap(f));
|
|
|
- });
|
|
|
- }
|
|
|
- stepsSec.appendChild(sec);
|
|
|
- });
|
|
|
- wrap.appendChild(stepsSec);
|
|
|
-
|
|
|
- wrap.appendChild(el('details', null,
|
|
|
- el('summary', null, '查看 workflow JSON'),
|
|
|
- el('pre', {class:'json-raw'}, JSON.stringify(w, null, 2))
|
|
|
- ));
|
|
|
+ const tableSec = el('div', {class:'detail-section'}, el('h3', null, '能力 / 原子能力'));
|
|
|
+ tableSec.appendChild(renderCapTable(w));
|
|
|
+ wrap.appendChild(tableSec);
|
|
|
}
|
|
|
|
|
|
function renderChips(){
|
|
|
@@ -793,19 +938,14 @@ function renderChips(){
|
|
|
el('span', {class:'x', onClick: () => { onX(); renderAll(); }}, '×')
|
|
|
));
|
|
|
};
|
|
|
- if (state.actionMode === 'multiset' && state.actionsSelected.size){
|
|
|
- mk('动作', [...state.actionsSelected].join(' + '), () => { state.actionsSelected.clear(); });
|
|
|
- } else if (state.actionMode === 'phase' && state.phaseSig){
|
|
|
- mk('结构', state.phaseSig, () => { state.phaseSig = null; });
|
|
|
- }
|
|
|
+ if (state.actionSeqSig) mk('动作序列', state.actionSeqSig, () => { state.actionSeqSig = null; });
|
|
|
+ if (state.actionsSelected.size) mk('含动词', [...state.actionsSelected].join(' + '), () => { state.actionsSelected.clear(); });
|
|
|
if (state.shizhiPath) mk('实质', state.shizhiPath.split('/').pop() || state.shizhiPath, () => { state.shizhiPath = null; });
|
|
|
if (state.xingshiPath) mk('形式', state.xingshiPath.split('/').pop() || state.xingshiPath, () => { state.xingshiPath = null; });
|
|
|
if (state.modSig) mk('模态', state.modSig, () => { state.modSig = null; });
|
|
|
$('clearAll').classList.toggle('active', any);
|
|
|
}
|
|
|
|
|
|
-function renderStats(){}
|
|
|
-
|
|
|
function renderAll(){
|
|
|
renderActions();
|
|
|
renderScope();
|
|
|
@@ -815,11 +955,47 @@ function renderAll(){
|
|
|
renderChips();
|
|
|
}
|
|
|
|
|
|
+// splitter
|
|
|
+function setupSplitter(){
|
|
|
+ const sp = $('splitter');
|
|
|
+ let dragging = false;
|
|
|
+ const setPct = (pct) => {
|
|
|
+ pct = Math.max(4, Math.min(96, pct));
|
|
|
+ document.documentElement.style.setProperty('--left-pct', pct + '%');
|
|
|
+ // update preset active state
|
|
|
+ let activeId = null;
|
|
|
+ if (pct < 20) activeId = 'presetRight';
|
|
|
+ else if (pct > 80) activeId = 'presetLeft';
|
|
|
+ else if (pct >= 45 && pct <= 55) activeId = 'presetEven';
|
|
|
+ for (const id of ['presetLeft','presetEven','presetRight']){
|
|
|
+ $(id).classList.toggle('active', id === activeId);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ sp.addEventListener('mousedown', e => {
|
|
|
+ dragging = true; sp.classList.add('dragging');
|
|
|
+ document.body.style.cursor = 'col-resize';
|
|
|
+ document.body.style.userSelect = 'none';
|
|
|
+ e.preventDefault();
|
|
|
+ });
|
|
|
+ document.addEventListener('mousemove', e => {
|
|
|
+ if (!dragging) return;
|
|
|
+ const w = window.innerWidth;
|
|
|
+ setPct((e.clientX / w) * 100);
|
|
|
+ });
|
|
|
+ document.addEventListener('mouseup', () => {
|
|
|
+ if (!dragging) return;
|
|
|
+ dragging = false; sp.classList.remove('dragging');
|
|
|
+ document.body.style.cursor = ''; document.body.style.userSelect = '';
|
|
|
+ });
|
|
|
+ $('presetLeft').addEventListener('click', () => setPct(94));
|
|
|
+ $('presetEven').addEventListener('click', () => setPct(50));
|
|
|
+ $('presetRight').addEventListener('click', () => setPct(28));
|
|
|
+ setPct(40);
|
|
|
+}
|
|
|
+
|
|
|
function init(){
|
|
|
for (const c of (data.cases || [])) casesByIndex[c.index] = c;
|
|
|
for (const f of data.fragments) fragByKey.set(f.case_index + ':' + f.fragment_id, f);
|
|
|
- PHASE_COLORS = data.phaseColors || {};
|
|
|
- PHASE_FG = data.phaseFg || {};
|
|
|
|
|
|
SIDE_MODS = (() => {
|
|
|
const inSet = new Set(), outSet = new Set();
|
|
|
@@ -838,29 +1014,28 @@ function init(){
|
|
|
$('appRoot').style.display = '';
|
|
|
|
|
|
$('clearAll').addEventListener('click', () => {
|
|
|
- state.actionsSelected.clear(); state.phaseSig = null;
|
|
|
+ state.actionSeqSig = null;
|
|
|
+ state.actionsSelected.clear();
|
|
|
state.shizhiPath = null; state.xingshiPath = null;
|
|
|
state.modSig = null;
|
|
|
state.modInFilter.clear(); state.modOutFilter.clear();
|
|
|
renderAll();
|
|
|
});
|
|
|
$('actClear').addEventListener('click', () => {
|
|
|
- if (state.actionMode === 'multiset') state.actionsSelected.clear();
|
|
|
- else state.phaseSig = null;
|
|
|
+ state.actionSeqSig = null;
|
|
|
+ state.actionsSelected.clear();
|
|
|
renderAll();
|
|
|
});
|
|
|
$('scopeClear').addEventListener('click', () => {
|
|
|
state.shizhiPath = null; state.xingshiPath = null;
|
|
|
renderAll();
|
|
|
});
|
|
|
- $('tabMulti').addEventListener('click', () => { state.actionMode = 'multiset'; renderAll(); });
|
|
|
- $('tabPhase').addEventListener('click', () => { state.actionMode = 'phase'; renderAll(); });
|
|
|
$('modClear').addEventListener('click', () => {
|
|
|
state.modSig = null; state.modInFilter.clear(); state.modOutFilter.clear();
|
|
|
renderAll();
|
|
|
});
|
|
|
|
|
|
- renderStats();
|
|
|
+ setupSplitter();
|
|
|
renderAll();
|
|
|
}
|
|
|
|