|
|
@@ -0,0 +1,1094 @@
|
|
|
+<!doctype html>
|
|
|
+<html lang="zh">
|
|
|
+<head>
|
|
|
+<meta charset="utf-8">
|
|
|
+<title>能力 · 五层筛选</title>
|
|
|
+<style>
|
|
|
+ :root{
|
|
|
+ --bg:#f8fafc; --panel:#ffffff; --panel2:#f1f5f9; --border:#e2e8f0;
|
|
|
+ --fg:#0f172a; --muted:#64748b; --accent:#3b82f6; --accent2:#8b5cf6;
|
|
|
+ --tag-bg:#e2e8f0; --tag-bg-active:#3b82f6; --tag-fg-active:#fff;
|
|
|
+ --code:#f1f5f9; --warn:#eab308; --suggest:#8b5cf6; --inferred:#94a3b8;
|
|
|
+ --shi-bg:#dbeafe; --shi-fg:#1e40af;
|
|
|
+ --xing-bg:#ede9fe; --xing-fg:#6d28d9;
|
|
|
+ --both-bg:#fef3c7; --both-fg:#92400e;
|
|
|
+ }
|
|
|
+ *{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}
|
|
|
+ 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:160px 360px 220px 360px 1fr;
|
|
|
+ height:calc(100vh - 41px);min-width:1520px}
|
|
|
+ section{overflow:auto;border-right:1px solid var(--border);padding:8px 10px}
|
|
|
+ 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}
|
|
|
+ .col-count{font-size:10px;color:var(--muted)}
|
|
|
+ .col-clear{font-size:10px;color:var(--accent);cursor:pointer;display:none}
|
|
|
+ .col-clear.show{display:inline}
|
|
|
+
|
|
|
+ .item{padding:6px 8px;border-radius:5px;cursor:pointer;margin-bottom:3px;
|
|
|
+ border:1px solid transparent;display:flex;justify-content:space-between;align-items:center;gap:6px}
|
|
|
+ .item:hover:not(.disabled):not(.active){background:var(--panel2)}
|
|
|
+ .item.active{background:var(--tag-bg-active);color:var(--tag-fg-active);border-color:var(--tag-bg-active)}
|
|
|
+ .item.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+ .item-name{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
|
+ .item-count{font-size:10px;background:var(--bg);padding:1px 5px;border-radius:8px;
|
|
|
+ color:var(--muted);min-width:20px;text-align:center;flex-shrink:0}
|
|
|
+ .item.active .item-count{background:rgba(255,255,255,.3);color:#fff}
|
|
|
+ .item-def{display:block;font-size:10px;color:var(--muted);margin-top:2px;line-height:1.4}
|
|
|
+ .item.active .item-def{color:rgba(255,255,255,.85)}
|
|
|
+ .item-stack{flex:1;min-width:0}
|
|
|
+
|
|
|
+ /* col-2 tabs */
|
|
|
+ .scope-tabs{display:flex;gap:4px;margin:0 0 8px;border-bottom:1px solid var(--border);padding-bottom:5px}
|
|
|
+ .scope-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}
|
|
|
+ .scope-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
|
|
|
+ .scope-tab:hover:not(.active){color:var(--fg)}
|
|
|
+ .scope-pane{display:none}
|
|
|
+ .scope-pane.active{display:block}
|
|
|
+ .scope-meta{font-size:10px;color:var(--muted);margin:0 4px 8px;line-height:1.4}
|
|
|
+
|
|
|
+ /* facets */
|
|
|
+ .facet-title{margin-top:10px;padding:4px 6px;font-size:11px;color:var(--muted);
|
|
|
+ font-weight:600;border-bottom:1px solid var(--border);text-transform:uppercase;letter-spacing:.5px}
|
|
|
+ .facet-title:first-child{margin-top:0}
|
|
|
+
|
|
|
+ /* itemsets */
|
|
|
+ .iset{padding:8px 10px;border-radius:5px;cursor:pointer;margin-bottom:5px;
|
|
|
+ border:1px solid var(--border);background:var(--panel)}
|
|
|
+ .iset:hover:not(.disabled):not(.active){border-color:var(--accent)}
|
|
|
+ .iset.active{border-color:var(--accent);background:#eff6ff;
|
|
|
+ box-shadow:0 0 0 1px var(--accent) inset}
|
|
|
+ .iset.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+ .iset-head{display:flex;align-items:center;gap:6px;margin-bottom:5px}
|
|
|
+ .iset-sup{background:var(--accent);color:#fff;padding:1px 7px;border-radius:3px;
|
|
|
+ font-weight:700;font-size:11px}
|
|
|
+ .iset-k{background:var(--tag-bg);color:var(--fg);padding:1px 6px;border-radius:3px;
|
|
|
+ font-size:10px;letter-spacing:.3px}
|
|
|
+ .iset-meta{font-size:10px;color:var(--muted);margin-left:auto}
|
|
|
+ .iset-paths{display:flex;flex-direction:column;gap:3px}
|
|
|
+ .iset-path{display:flex;gap:5px;font-size:11px;line-height:1.4;align-items:flex-start}
|
|
|
+ .iset-fdot{width:18px;flex-shrink:0;font-size:9px;font-weight:700;text-align:center;
|
|
|
+ border-radius:3px;line-height:16px;height:16px;letter-spacing:0;margin-top:1px}
|
|
|
+ .iset-fdot.shi{background:var(--shi-bg);color:var(--shi-fg)}
|
|
|
+ .iset-fdot.xing{background:var(--xing-bg);color:var(--xing-fg)}
|
|
|
+ .iset-fdot.both{background:var(--both-bg);color:var(--both-fg)}
|
|
|
+ .iset-leaf{font-weight:600;color:var(--fg);word-break:break-all}
|
|
|
+ .iset-parent{color:var(--muted);font-size:10px;margin-top:1px;
|
|
|
+ word-break:break-all;line-height:1.3;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
|
+ .iset-caret{width:14px;display:inline-block;text-align:center;color:var(--muted);
|
|
|
+ cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
|
|
|
+ .iset-caret:hover{color:var(--fg)}
|
|
|
+ .iset-caret.invis{visibility:hidden}
|
|
|
+ .iset-children{margin-left:14px;border-left:1px dashed var(--border);padding-left:8px;
|
|
|
+ margin-top:-2px;margin-bottom:5px}
|
|
|
+ .iset-childcnt{font-size:10px;color:var(--muted);margin-left:4px}
|
|
|
+
|
|
|
+ /* tree */
|
|
|
+ .tnode{margin-left:0}
|
|
|
+ .tnode .tnode{margin-left:12px;border-left:1px dashed var(--border);padding-left:6px}
|
|
|
+ .tline{display:flex;align-items:center;gap:4px;padding:3px 4px;border-radius:4px;
|
|
|
+ cursor:pointer;font-size:12px;line-height:1.35}
|
|
|
+ .tline:hover:not(.disabled):not(.active){background:var(--panel2)}
|
|
|
+ .tline.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
|
|
|
+ .tline.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+ .tline.suggested .tname{color:var(--suggest);font-style:italic}
|
|
|
+ .tline.suggested.active .tname{color:#fff}
|
|
|
+ .tline.inferred .tname{color:var(--inferred);font-style:italic}
|
|
|
+ .tline.inferred.active .tname{color:#fff}
|
|
|
+ .tcaret{width:12px;display:inline-block;text-align:center;color:var(--muted);
|
|
|
+ cursor:pointer;user-select:none;flex-shrink:0;font-size:10px}
|
|
|
+ .tcaret.invis{visibility:hidden}
|
|
|
+ .tname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
|
+ .tcount{font-size:10px;color:var(--muted);background:var(--bg);padding:1px 5px;border-radius:8px;flex-shrink:0}
|
|
|
+ .tline.active .tcount{background:rgba(255,255,255,.3);color:#fff}
|
|
|
+ .tcollapsed > .tnode{display:none}
|
|
|
+
|
|
|
+ /* col-4 frag view tabs */
|
|
|
+ .fv-tabs{display:flex;gap:4px;margin:0 0 6px;border-bottom:1px solid var(--border);padding-bottom:3px}
|
|
|
+ .fv-tab{padding:3px 10px;border-radius:4px 4px 0 0;cursor:pointer;
|
|
|
+ font-size:11px;color:var(--muted);background:transparent;
|
|
|
+ border:1px solid transparent;border-bottom:none;user-select:none}
|
|
|
+ .fv-tab.active{color:var(--fg);background:var(--panel2);border-color:var(--border)}
|
|
|
+ .fv-tab:hover:not(.active){color:var(--fg)}
|
|
|
+
|
|
|
+ /* fragments */
|
|
|
+ .frag{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
|
|
|
+ border:1px solid var(--border);background:var(--panel)}
|
|
|
+ .frag:hover:not(.active){background:var(--panel2)}
|
|
|
+ .frag.active{border-color:var(--accent);background:#eff6ff}
|
|
|
+ .frag-head{font-size:10px;color:var(--muted);margin-bottom:3px;display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
|
|
+ .case-badge{background:var(--accent);color:#fff;padding:1px 5px;border-radius:3px;
|
|
|
+ font-weight:600;font-size:10px}
|
|
|
+ .frag-badge{background:var(--accent2);color:#fff;padding:1px 5px;border-radius:3px;
|
|
|
+ font-weight:600;font-size:10px}
|
|
|
+ .act-badge{background:var(--warn);color:#fff;padding:1px 5px;border-radius:3px;
|
|
|
+ font-weight:600;font-size:10px}
|
|
|
+ .side-badge{padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600}
|
|
|
+ .side-badge.shi{background:var(--shi-bg);color:var(--shi-fg)}
|
|
|
+ .side-badge.xing{background:var(--xing-bg);color:var(--xing-fg)}
|
|
|
+ .src-badge{background:var(--suggest);color:#fff;padding:1px 5px;border-radius:3px;
|
|
|
+ font-size:10px;font-weight:600}
|
|
|
+ .frag-sig{font-size:11px;color:var(--muted);margin-top:2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
|
|
+ .frag-body{font-size:12px;color:#334155;margin-top:4px;line-height:1.45;
|
|
|
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
|
+
|
|
|
+ /* excerpt rows */
|
|
|
+ .excerpt-row{padding:7px 9px;border-radius:5px;cursor:pointer;margin-bottom:4px;
|
|
|
+ border:1px solid var(--border);background:var(--panel)}
|
|
|
+ .excerpt-row:hover:not(.active){background:var(--panel2)}
|
|
|
+ .excerpt-row.active{border-color:var(--accent);background:#eff6ff}
|
|
|
+ .excerpt-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;
|
|
|
+ color:var(--accent);margin-top:4px;word-break:break-all;line-height:1.35}
|
|
|
+ .excerpt-main{font-size:12.5px;color:var(--fg);margin-top:4px;line-height:1.5}
|
|
|
+ .excerpt-note{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.45}
|
|
|
+ .excerpt-body{font-size:11px;color:var(--muted);margin-top:3px;line-height:1.45;
|
|
|
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
|
+
|
|
|
+ /* structured tab */
|
|
|
+ .frag-excerpts{display:flex;flex-direction:column;gap:4px;margin-top:4px}
|
|
|
+ .frag-excerpts .ex{font-size:12px;color:#334155;line-height:1.55;
|
|
|
+ background:var(--code);padding:5px 8px;border-radius:4px;
|
|
|
+ border-left:2px solid var(--accent2)}
|
|
|
+ .frag-excerpts.empty{font-size:12px;color:var(--muted);font-style:italic;
|
|
|
+ background:var(--code);padding:5px 8px;border-radius:4px}
|
|
|
+
|
|
|
+ /* detail */
|
|
|
+ .detail-empty{color:var(--muted);text-align:center;padding:60px 20px;font-size:13px}
|
|
|
+ .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}
|
|
|
+ .effect-card{background:var(--code);border-radius:5px;padding:7px 9px;margin-bottom:5px;
|
|
|
+ border-left:3px solid var(--accent2);font-size:12px}
|
|
|
+ .effect-stmt{font-weight:600;margin-bottom:4px}
|
|
|
+ .effect-meta{color:var(--muted);font-size:11px;margin-top:3px}
|
|
|
+ .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 in detail panel */
|
|
|
+ .case-detail{margin:8px 0;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;
|
|
|
+ margin-top:4px;border:1px solid var(--border)}
|
|
|
+ .case-images{display:flex;gap:8px;overflow-x:auto;margin-bottom:8px;padding-bottom:4px}
|
|
|
+ .case-images img{height:160px;border-radius:5px;object-fit:contain;
|
|
|
+ background:var(--panel2);border:1px solid var(--border)}
|
|
|
+ .case-body{font-size:12px;line-height:1.6;color:var(--fg);white-space:pre-wrap;
|
|
|
+ margin-bottom:10px;max-height:250px;overflow-y:auto;padding-right:4px}
|
|
|
+ .case-link{font-size:11px;color:var(--accent);display:inline-block;
|
|
|
+ background:var(--panel2);padding:4px 8px;border-radius:4px;text-decoration:none}
|
|
|
+ .case-feedback{font-size:11px;color:var(--muted);margin-bottom:6px;display:flex;gap:12px;flex-wrap:wrap}
|
|
|
+ .case-feedback .fb{display:inline-flex;gap:3px;align-items:center}
|
|
|
+ .case-author{font-size:11px;color:var(--muted);margin-bottom:6px}
|
|
|
+
|
|
|
+ .empty-msg{color:var(--muted);text-align:center;padding:30px 10px;font-size:11px;font-style:italic}
|
|
|
+
|
|
|
+ /* modality side filter */
|
|
|
+ .mod-filter{margin:0 4px 8px;padding:6px 6px 4px;background:var(--code);
|
|
|
+ border:1px solid var(--border);border-radius:5px}
|
|
|
+ .mod-filter-row{display:flex;align-items:center;gap:6px;margin-bottom:4px}
|
|
|
+ .mod-filter-row:last-child{margin-bottom:0}
|
|
|
+ .mod-filter-lbl{font-size:10px;color:var(--muted);min-width:42px;flex-shrink:0}
|
|
|
+ .mod-chips{display:flex;gap:4px;flex-wrap:wrap}
|
|
|
+ .mod-chip{font-size:11px;padding:1px 7px;background:var(--tag-bg);border-radius:8px;
|
|
|
+ cursor:pointer;color:var(--fg);user-select:none;border:1px solid transparent}
|
|
|
+ .mod-chip:hover:not(.disabled):not(.active){background:var(--panel2);border-color:var(--border)}
|
|
|
+ .mod-chip.active{background:var(--tag-bg-active);color:var(--tag-fg-active)}
|
|
|
+ .mod-chip.disabled{opacity:.42;cursor:not-allowed}
|
|
|
+
|
|
|
+ /* selection summary chip */
|
|
|
+ .filter-chip{font-size:11px;padding:3px 8px;background:var(--tag-bg);border-radius:10px;
|
|
|
+ color:var(--fg);display:inline-flex;align-items:center;gap:6px;margin-right:4px}
|
|
|
+ .filter-chip .x{color:var(--muted);cursor:pointer;font-weight:bold}
|
|
|
+ .filter-chip .x:hover{color:var(--warn)}
|
|
|
+ .filter-chip .lbl{color:var(--muted);font-size:9px;text-transform:uppercase}
|
|
|
+
|
|
|
+ .loading{display:flex;align-items:center;justify-content:center;
|
|
|
+ height:100vh;font-size:14px;color:var(--muted)}
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<div id="loading" class="loading">加载能力数据中…</div>
|
|
|
+
|
|
|
+<div id="appRoot" style="display:none">
|
|
|
+<header>
|
|
|
+ <h1>能力</h1>
|
|
|
+ <span id="chips"></span>
|
|
|
+ <button class="clear-all" id="clearAll">清除全部筛选</button>
|
|
|
+</header>
|
|
|
+
|
|
|
+<main>
|
|
|
+ <section class="fac1" id="actCol">
|
|
|
+ <div class="col-title"><span>① 动作</span><span class="col-count" id="actCnt"></span></div>
|
|
|
+ <div id="actList"></div>
|
|
|
+ </section>
|
|
|
+ <section class="fac2" id="scopeCol">
|
|
|
+ <div class="col-title">
|
|
|
+ <span>② 作用域</span>
|
|
|
+ <span class="col-count" id="scopeStat"></span>
|
|
|
+ </div>
|
|
|
+ <div class="scope-tabs">
|
|
|
+ <div class="scope-tab" data-mode="node" id="tabNode">node · 内容树</div>
|
|
|
+ <div class="scope-tab" data-mode="pattern" id="tabPattern">pattern · 频繁项集</div>
|
|
|
+ <span class="col-clear" id="scopeClear" style="margin-left:auto;align-self:center">清除</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scope-pane" id="paneNode">
|
|
|
+ <div class="facet-title">实质</div>
|
|
|
+ <div id="shizhiTree"></div>
|
|
|
+ <div class="facet-title">形式</div>
|
|
|
+ <div id="xingshiTree"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="scope-pane" id="panePattern">
|
|
|
+ <div class="scope-meta" id="isParams"></div>
|
|
|
+ <div id="isList"></div>
|
|
|
+ </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="fragCol">
|
|
|
+ <div class="col-title"><span id="fragColTitle">④ 原子能力</span><span class="col-count" id="fragCnt"></span></div>
|
|
|
+ <div class="fv-tabs" id="fvTabs" style="display:none">
|
|
|
+ <div class="fv-tab" data-view="raw" id="fvRaw">raw</div>
|
|
|
+ <div class="fv-tab" data-view="structured" id="fvStructured">structured</div>
|
|
|
+ </div>
|
|
|
+ <div id="fragList"></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>
|
|
|
+
|
|
|
+<script>
|
|
|
+let data = null;
|
|
|
+let casesByIndex = {};
|
|
|
+let SIDE_MODS = null;
|
|
|
+let shiDesc = null;
|
|
|
+let xingDesc = null;
|
|
|
+const fragPaths = new Map();
|
|
|
+
|
|
|
+const state = {
|
|
|
+ action: null,
|
|
|
+ scopeMode: 'node',
|
|
|
+ shizhiPath: null,
|
|
|
+ xingshiPath: null,
|
|
|
+ itemsetIdx: null,
|
|
|
+ patternView: 'raw',
|
|
|
+ modSig: null,
|
|
|
+ modInFilter: new Set(),
|
|
|
+ modOutFilter: new Set(),
|
|
|
+ fragKey: null,
|
|
|
+ collapsed: new Set(),
|
|
|
+ isetExpanded: new Set(),
|
|
|
+};
|
|
|
+
|
|
|
+function $(id){return document.getElementById(id)}
|
|
|
+function el(tag, attrs, ...kids){
|
|
|
+ const e = document.createElement(tag);
|
|
|
+ if (attrs) for (const k in attrs){
|
|
|
+ if (k === 'class') e.className = attrs[k];
|
|
|
+ else if (k === 'onClick') e.addEventListener('click', attrs[k]);
|
|
|
+ else if (k.startsWith('data-')) e.setAttribute(k, attrs[k]);
|
|
|
+ else if (k === 'title') e.title = attrs[k];
|
|
|
+ else if (k === 'style') e.setAttribute('style', attrs[k]);
|
|
|
+ else e[k] = attrs[k];
|
|
|
+ }
|
|
|
+ for (const kid of kids){
|
|
|
+ if (kid == null || kid === false) continue;
|
|
|
+ if (typeof kid === 'string') e.appendChild(document.createTextNode(kid));
|
|
|
+ else e.appendChild(kid);
|
|
|
+ }
|
|
|
+ return e;
|
|
|
+}
|
|
|
+
|
|
|
+const fragKey = f => f.case_index + ':' + f.fragment_id;
|
|
|
+
|
|
|
+function sigParts(sig){
|
|
|
+ const [inS, outS] = sig.split(' → ');
|
|
|
+ const parse = s => new Set(s.split('+').filter(x => x && x !== '∅'));
|
|
|
+ return { in: parse(inS), out: parse(outS) };
|
|
|
+}
|
|
|
+
|
|
|
+function buildDescMap(roots){
|
|
|
+ const map = new Map();
|
|
|
+ function walk(node){
|
|
|
+ const all = [node.path];
|
|
|
+ for (const c of node.children || []){
|
|
|
+ const sub = walk(c);
|
|
|
+ for (const p of sub) all.push(p);
|
|
|
+ }
|
|
|
+ map.set(node.path, all);
|
|
|
+ return all;
|
|
|
+ }
|
|
|
+ for (const r of roots) walk(r);
|
|
|
+ return map;
|
|
|
+}
|
|
|
+
|
|
|
+function fragPathSet(f){
|
|
|
+ const s = new Set();
|
|
|
+ for (const e of (f.apply_shizhi || [])) if (e.category_path) s.add(e.category_path);
|
|
|
+ for (const e of (f.apply_xingshi || [])) if (e.category_path) s.add(e.category_path);
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+function fragMatchesPath(f, key, selPath, descMap){
|
|
|
+ if (!selPath) return true;
|
|
|
+ const allowed = new Set(descMap.get(selPath) || [selPath]);
|
|
|
+ const list = (key === 'shizhi') ? f.apply_shizhi : f.apply_xingshi;
|
|
|
+ for (const e of (list || [])){
|
|
|
+ if (allowed.has(e.category_path)) return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+function fragMatchesItemset(f, itemset){
|
|
|
+ if (!itemset) return true;
|
|
|
+ const ps = fragPaths.get(fragKey(f));
|
|
|
+ for (const p of itemset.items) if (!ps.has(p)) return false;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function scopeFilterActive(){
|
|
|
+ if (state.scopeMode === 'node') return !!(state.shizhiPath || state.xingshiPath);
|
|
|
+ if (state.scopeMode === 'pattern') return state.itemsetIdx != null;
|
|
|
+ return false;
|
|
|
+}
|
|
|
+
|
|
|
+function applyFilters(except){
|
|
|
+ return data.fragments.filter(f => {
|
|
|
+ if (except !== 'action' && state.action && f.action !== state.action) return false;
|
|
|
+ if (except !== 'scope' && scopeFilterActive()){
|
|
|
+ if (state.scopeMode === 'node'){
|
|
|
+ if (state.shizhiPath && !fragMatchesPath(f, 'shizhi', state.shizhiPath, shiDesc)) return false;
|
|
|
+ if (state.xingshiPath && !fragMatchesPath(f, 'xingshi', state.xingshiPath, xingDesc)) return false;
|
|
|
+ } else if (state.scopeMode === 'pattern'){
|
|
|
+ const sel = data.itemsets[state.itemsetIdx];
|
|
|
+ if (sel && !fragMatchesItemset(f, sel)) return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (except !== 'mod' && state.modSig && f.modality_signature !== state.modSig) return false;
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function renderActions(){
|
|
|
+ const baseFiltered = applyFilters('action');
|
|
|
+ const counts = {};
|
|
|
+ for (const f of baseFiltered) counts[f.action] = (counts[f.action]||0)+1;
|
|
|
+ const wrap = $('actList'); wrap.innerHTML = '';
|
|
|
+ for (const a of data.actions){
|
|
|
+ const c = counts[a.verb] || 0;
|
|
|
+ const node = el('div', {
|
|
|
+ class: 'item' + (state.action === a.verb ? ' active' : '') + (c === 0 && state.action !== a.verb ? ' disabled' : ''),
|
|
|
+ onClick: () => {
|
|
|
+ if (c === 0 && state.action !== a.verb) return;
|
|
|
+ state.action = (state.action === a.verb) ? null : 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);
|
|
|
+ }
|
|
|
+ $('actCnt').textContent = '共 ' + data.actions.length + ' 个';
|
|
|
+}
|
|
|
+
|
|
|
+function renderTreeFacet(roots, key, mountId, selPath, descMap){
|
|
|
+ const wrap = $(mountId); wrap.innerHTML = '';
|
|
|
+ const baseFiltered = applyFilters('scope');
|
|
|
+ const hitCounts = new Map();
|
|
|
+ for (const f of baseFiltered){
|
|
|
+ const list = (key === 'shizhi') ? f.apply_shizhi : f.apply_xingshi;
|
|
|
+ const seen = new Set();
|
|
|
+ for (const e of (list || [])){
|
|
|
+ const parts = (e.category_path || '').split('/').filter(Boolean);
|
|
|
+ for (let i = 1; i <= parts.length; i++){
|
|
|
+ const p = '/' + parts.slice(0,i).join('/');
|
|
|
+ if (seen.has(p)) continue;
|
|
|
+ seen.add(p);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (const p of seen) hitCounts.set(p, (hitCounts.get(p)||0)+1);
|
|
|
+ }
|
|
|
+ function renderNode(node, parent){
|
|
|
+ const c = hitCounts.get(node.path) || 0;
|
|
|
+ const collapsed = state.collapsed.has(node.path);
|
|
|
+ const hasChildren = (node.children || []).length > 0;
|
|
|
+ const tnode = el('div', {class: 'tnode' + (collapsed ? ' tcollapsed' : '')});
|
|
|
+ const cls = ['tline'];
|
|
|
+ if (selPath === node.path) cls.push('active');
|
|
|
+ if (c === 0 && selPath !== node.path) cls.push('disabled');
|
|
|
+ if (node.is_suggested && !node.is_hit) cls.push('suggested');
|
|
|
+ if (node.is_inferred) cls.push('inferred');
|
|
|
+ const line = el('div', {class: cls.join(' '), title: node.path + (node.description ? '\n\n' + node.description : '')});
|
|
|
+ const caret = el('span', {
|
|
|
+ class: 'tcaret' + (hasChildren ? '' : ' invis'),
|
|
|
+ onClick: (ev) => {
|
|
|
+ ev.stopPropagation();
|
|
|
+ if (state.collapsed.has(node.path)) state.collapsed.delete(node.path);
|
|
|
+ else state.collapsed.add(node.path);
|
|
|
+ renderTrees();
|
|
|
+ }
|
|
|
+ }, hasChildren ? (collapsed ? '▶' : '▼') : '·');
|
|
|
+ line.appendChild(caret);
|
|
|
+ let label = node.name;
|
|
|
+ if (node.is_suggested && !node.is_hit) label += ' ✦';
|
|
|
+ if (node.is_inferred) label += ' (推断)';
|
|
|
+ line.appendChild(el('span', {class:'tname'}, label));
|
|
|
+ line.appendChild(el('span', {class:'tcount'}, String(c)));
|
|
|
+ line.addEventListener('click', () => {
|
|
|
+ if (c === 0 && selPath !== node.path) return;
|
|
|
+ const k = (key === 'shizhi') ? 'shizhiPath' : 'xingshiPath';
|
|
|
+ state[k] = (state[k] === node.path) ? null : node.path;
|
|
|
+ renderAll();
|
|
|
+ });
|
|
|
+ tnode.appendChild(line);
|
|
|
+ for (const ch of node.children || []) renderNode(ch, tnode);
|
|
|
+ parent.appendChild(tnode);
|
|
|
+ }
|
|
|
+ for (const r of roots) renderNode(r, wrap);
|
|
|
+}
|
|
|
+
|
|
|
+function renderTrees(){
|
|
|
+ renderTreeFacet(data.subtree.shizhi || [], 'shizhi', 'shizhiTree', state.shizhiPath, shiDesc);
|
|
|
+ renderTreeFacet(data.subtree.xingshi || [], 'xingshi', 'xingshiTree', state.xingshiPath, xingDesc);
|
|
|
+}
|
|
|
+
|
|
|
+function pathLeafAndParent(p){
|
|
|
+ const parts = p.split('/').filter(Boolean);
|
|
|
+ if (!parts.length) return {leaf:p, parent:''};
|
|
|
+ return {leaf: parts[parts.length-1], parent: '/' + parts.slice(0,-1).join('/')};
|
|
|
+}
|
|
|
+
|
|
|
+function renderItemsets(){
|
|
|
+ const wrap = $('isList'); wrap.innerHTML = '';
|
|
|
+ const visibleIdxSet = new Set();
|
|
|
+ data.itemsets.forEach((it, i) => { if (it.size >= 2) visibleIdxSet.add(i); });
|
|
|
+ $('isParams').textContent = `${visibleIdxSet.size} closed · k≥2 · min_support=${data.itemsetsParams.min_support}, k≤${data.itemsetsParams.max_k}, leaf-only`;
|
|
|
+
|
|
|
+ const baseFiltered = applyFilters('scope');
|
|
|
+ const roots = [];
|
|
|
+ for (const i of visibleIdxSet){
|
|
|
+ const ps = (data.itemsetParents[i] || []).filter(p => visibleIdxSet.has(p));
|
|
|
+ if (!ps.length) roots.push(i);
|
|
|
+ }
|
|
|
+ roots.sort((a, b) => {
|
|
|
+ const A = data.itemsets[a], B = data.itemsets[b];
|
|
|
+ return (B.support - A.support) || (B.size - A.size) || a - b;
|
|
|
+ });
|
|
|
+
|
|
|
+ for (const idx of roots){
|
|
|
+ renderItemsetNode(idx, -1, wrap, baseFiltered, visibleIdxSet, new Set());
|
|
|
+ }
|
|
|
+ if (visibleIdxSet.size === 0){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '没有符合 k≥2 的项集'));
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderItemsetNode(idx, parentIdx, mountEl, baseFiltered, visibleIdxSet, ancestorSet){
|
|
|
+ const iset = data.itemsets[idx];
|
|
|
+ const isActive = (state.itemsetIdx === idx);
|
|
|
+ const occKey = parentIdx + ':' + idx;
|
|
|
+ const childIdxs = (data.itemsetChildren[idx] || []).filter(c => visibleIdxSet.has(c) && !ancestorSet.has(c));
|
|
|
+ const isExpanded = state.isetExpanded.has(occKey);
|
|
|
+ const supportNow = baseFiltered.filter(f => fragMatchesItemset(f, iset)).length;
|
|
|
+
|
|
|
+ const cls = ['iset'];
|
|
|
+ if (isActive) cls.push('active');
|
|
|
+ if (supportNow === 0 && !isActive) cls.push('disabled');
|
|
|
+ const node = el('div', {
|
|
|
+ class: cls.join(' '),
|
|
|
+ onClick: () => {
|
|
|
+ if (supportNow === 0 && !isActive) return;
|
|
|
+ state.itemsetIdx = isActive ? null : idx;
|
|
|
+ renderAll();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const caret = el('span', {
|
|
|
+ class: 'iset-caret' + (childIdxs.length === 0 ? ' invis' : ''),
|
|
|
+ title: childIdxs.length ? (isExpanded ? '折叠' : '展开 ' + childIdxs.length + ' 个超集') : '',
|
|
|
+ onClick: (ev) => {
|
|
|
+ ev.stopPropagation();
|
|
|
+ if (!childIdxs.length) return;
|
|
|
+ if (state.isetExpanded.has(occKey)) state.isetExpanded.delete(occKey);
|
|
|
+ else state.isetExpanded.add(occKey);
|
|
|
+ renderItemsets();
|
|
|
+ }
|
|
|
+ }, childIdxs.length ? (isExpanded ? '▼' : '▶') : '·');
|
|
|
+ const head = el('div', {class:'iset-head'},
|
|
|
+ caret,
|
|
|
+ el('span', {class:'iset-sup', title:'当前筛选下的支持度'}, '×'+supportNow),
|
|
|
+ el('span', {class:'iset-k'}, 'k='+iset.size),
|
|
|
+ childIdxs.length ? el('span', {class:'iset-childcnt'}, '⊃'+childIdxs.length) : null,
|
|
|
+ el('span', {class:'iset-meta'}, '原始 sup ' + iset.support)
|
|
|
+ );
|
|
|
+ node.appendChild(head);
|
|
|
+ const paths = el('div', {class:'iset-paths'});
|
|
|
+ for (const p of iset.items){
|
|
|
+ const facet = data.pathToFacet[p] || '?';
|
|
|
+ const dot =
|
|
|
+ facet === '实质' ? el('span', {class:'iset-fdot shi', title:'实质'}, '实') :
|
|
|
+ facet === '形式' ? el('span', {class:'iset-fdot xing', title:'形式'}, '形') :
|
|
|
+ el('span', {class:'iset-fdot both', title:'两侧都有'}, '双');
|
|
|
+ const lp = pathLeafAndParent(p);
|
|
|
+ const row = el('div', {class:'iset-path', title: p},
|
|
|
+ dot,
|
|
|
+ el('div', null,
|
|
|
+ el('span', {class:'iset-leaf'}, lp.leaf),
|
|
|
+ el('div', {class:'iset-parent'}, lp.parent)
|
|
|
+ )
|
|
|
+ );
|
|
|
+ paths.appendChild(row);
|
|
|
+ }
|
|
|
+ node.appendChild(paths);
|
|
|
+ mountEl.appendChild(node);
|
|
|
+
|
|
|
+ if (isExpanded && childIdxs.length){
|
|
|
+ const childWrap = el('div', {class:'iset-children'});
|
|
|
+ const newAnc = new Set(ancestorSet); newAnc.add(idx);
|
|
|
+ const sortedChildren = [...childIdxs].sort((a, b) => {
|
|
|
+ const A = data.itemsets[a], B = data.itemsets[b];
|
|
|
+ return (B.support - A.support) || (B.size - A.size) || a - b;
|
|
|
+ });
|
|
|
+ for (const c of sortedChildren){
|
|
|
+ renderItemsetNode(c, idx, childWrap, baseFiltered, visibleIdxSet, newAnc);
|
|
|
+ }
|
|
|
+ mountEl.appendChild(childWrap);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderScope(){
|
|
|
+ $('tabNode').classList.toggle('active', state.scopeMode === 'node');
|
|
|
+ $('tabPattern').classList.toggle('active', state.scopeMode === 'pattern');
|
|
|
+ $('paneNode').classList.toggle('active', state.scopeMode === 'node');
|
|
|
+ $('panePattern').classList.toggle('active', state.scopeMode === 'pattern');
|
|
|
+ if (state.scopeMode === 'node') {
|
|
|
+ $('scopeStat').textContent = '路径树(双 facet)';
|
|
|
+ } else {
|
|
|
+ const k2 = data.itemsets.filter(i => i.size >= 2).length;
|
|
|
+ $('scopeStat').textContent = `${k2} 项集 · k≥2`;
|
|
|
+ }
|
|
|
+ $('scopeClear').classList.toggle('show', scopeFilterActive());
|
|
|
+ if (state.scopeMode === 'node') renderTrees();
|
|
|
+ else renderItemsets();
|
|
|
+}
|
|
|
+
|
|
|
+function sigPassesChipFilter(sig){
|
|
|
+ const p = sigParts(sig);
|
|
|
+ for (const m of state.modInFilter) if (!p.in.has(m)) return false;
|
|
|
+ for (const m of state.modOutFilter) if (!p.out.has(m)) return false;
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function renderModalityChips(){
|
|
|
+ const renderSide = (mountId, palette, selectedSet, side) => {
|
|
|
+ const wrap = $(mountId); wrap.innerHTML = '';
|
|
|
+ for (const m of palette){
|
|
|
+ const trial = new Set(selectedSet);
|
|
|
+ if (!trial.has(m)) trial.add(m);
|
|
|
+ let c = 0;
|
|
|
+ for (const sig of data.modalities){
|
|
|
+ const p = sigParts(sig.sig);
|
|
|
+ const inOK = side === 'in'
|
|
|
+ ? [...trial].every(x => p.in.has(x)) && [...state.modOutFilter].every(x => p.out.has(x))
|
|
|
+ : [...state.modInFilter].every(x => p.in.has(x)) && [...trial].every(x => p.out.has(x));
|
|
|
+ if (inOK) c++;
|
|
|
+ }
|
|
|
+ const isActive = selectedSet.has(m);
|
|
|
+ const chip = el('span', {
|
|
|
+ class: 'mod-chip' + (isActive ? ' active' : '') + (c === 0 && !isActive ? ' disabled' : ''),
|
|
|
+ title: m + (c === 0 && !isActive ? ' (无匹配)' : ''),
|
|
|
+ onClick: () => {
|
|
|
+ if (c === 0 && !isActive) return;
|
|
|
+ if (selectedSet.has(m)) selectedSet.delete(m);
|
|
|
+ else selectedSet.add(m);
|
|
|
+ if (state.modSig && !sigPassesChipFilter(state.modSig)) state.modSig = null;
|
|
|
+ renderAll();
|
|
|
+ }
|
|
|
+ }, m);
|
|
|
+ wrap.appendChild(chip);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ renderSide('modInChips', SIDE_MODS.in, state.modInFilter, 'in');
|
|
|
+ renderSide('modOutChips', SIDE_MODS.out, state.modOutFilter, 'out');
|
|
|
+}
|
|
|
+
|
|
|
+function renderModalities(){
|
|
|
+ const baseFiltered = applyFilters('mod');
|
|
|
+ const counts = {};
|
|
|
+ for (const f of baseFiltered) counts[f.modality_signature] = (counts[f.modality_signature]||0)+1;
|
|
|
+ renderModalityChips();
|
|
|
+ const wrap = $('modList'); wrap.innerHTML = '';
|
|
|
+ const visible = data.modalities.filter(m => sigPassesChipFilter(m.sig));
|
|
|
+ const sorted = [...visible].sort((a,b) => (counts[b.sig]||0) - (counts[a.sig]||0));
|
|
|
+ if (sorted.length === 0){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '当前 chip 筛选下没有匹配签名'));
|
|
|
+ }
|
|
|
+ for (const m of sorted){
|
|
|
+ const c = counts[m.sig] || 0;
|
|
|
+ const node = el('div', {
|
|
|
+ class: 'item' + (state.modSig === m.sig ? ' active' : '') + (c === 0 && state.modSig !== m.sig ? ' disabled' : ''),
|
|
|
+ onClick: () => {
|
|
|
+ if (c === 0 && state.modSig !== m.sig) return;
|
|
|
+ state.modSig = (state.modSig === m.sig) ? null : m.sig;
|
|
|
+ renderAll();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ el('span', {class:'item-name'}, m.sig),
|
|
|
+ el('span', {class:'item-count'}, String(c))
|
|
|
+ );
|
|
|
+ wrap.appendChild(node);
|
|
|
+ }
|
|
|
+ $('modClear').classList.toggle('show', !!state.modSig || state.modInFilter.size > 0 || state.modOutFilter.size > 0);
|
|
|
+}
|
|
|
+
|
|
|
+function makeFragBodyRow(f){
|
|
|
+ const k = fragKey(f);
|
|
|
+ const head = el('div', {class:'frag-head'},
|
|
|
+ el('span', {class:'case-badge'}, '案例'+f.case_index),
|
|
|
+ el('span', {class:'frag-badge'}, f.fragment_id),
|
|
|
+ el('span', {class:'act-badge'}, f.action)
|
|
|
+ );
|
|
|
+ const sig = el('div', {class:'frag-sig'}, f.modality_signature);
|
|
|
+ const body = el('div', {class:'frag-body'}, f.body || '');
|
|
|
+ return el('div', {
|
|
|
+ class: 'frag' + (state.fragKey === k ? ' active' : ''),
|
|
|
+ onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
|
|
|
+ }, head, sig, body);
|
|
|
+}
|
|
|
+
|
|
|
+function makeExcerptRow(frag, side, entry){
|
|
|
+ const k = fragKey(frag);
|
|
|
+ const head = el('div', {class:'frag-head'},
|
|
|
+ el('span', {class:'case-badge'}, '案例'+frag.case_index),
|
|
|
+ el('span', {class:'frag-badge'}, frag.fragment_id),
|
|
|
+ el('span', {class:'act-badge'}, frag.action),
|
|
|
+ el('span', {class:'side-badge ' + (side==='实质'?'shi':'xing')}, side),
|
|
|
+ entry.source === 'suggest' ? el('span', {class:'src-badge'}, '建议') : null
|
|
|
+ );
|
|
|
+ const node = el('div', {
|
|
|
+ class: 'excerpt-row' + (state.fragKey === k ? ' active' : ''),
|
|
|
+ onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
|
|
|
+ });
|
|
|
+ node.appendChild(head);
|
|
|
+ node.appendChild(el('div', {class:'excerpt-path'}, entry.category_path));
|
|
|
+ if (entry.body_excerpt){
|
|
|
+ node.appendChild(el('div', {class:'excerpt-main'}, entry.body_excerpt));
|
|
|
+ }
|
|
|
+ if (entry.body_excerpt_note){
|
|
|
+ node.appendChild(el('div', {class:'excerpt-note'}, entry.body_excerpt_note));
|
|
|
+ }
|
|
|
+ if (frag.body){
|
|
|
+ node.appendChild(el('div', {class:'excerpt-body'}, frag.body));
|
|
|
+ }
|
|
|
+ return node;
|
|
|
+}
|
|
|
+
|
|
|
+function renderFragmentsNode(filtered, wrap){
|
|
|
+ const hasNodeSel = !!(state.shizhiPath || state.xingshiPath);
|
|
|
+ if (!hasNodeSel){
|
|
|
+ $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
|
|
|
+ if (!filtered.length){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (const f of filtered) wrap.appendChild(makeFragBodyRow(f));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const rows = [];
|
|
|
+ for (const f of filtered){
|
|
|
+ if (state.shizhiPath){
|
|
|
+ const allowed = new Set(shiDesc.get(state.shizhiPath) || [state.shizhiPath]);
|
|
|
+ for (const e of (f.apply_shizhi || [])){
|
|
|
+ if (allowed.has(e.category_path)) rows.push({frag:f, side:'实质', entry:e});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (state.xingshiPath){
|
|
|
+ const allowed = new Set(xingDesc.get(state.xingshiPath) || [state.xingshiPath]);
|
|
|
+ for (const e of (f.apply_xingshi || [])){
|
|
|
+ if (allowed.has(e.category_path)) rows.push({frag:f, side:'形式', entry:e});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $('fragCnt').textContent = rows.length + ' 条 · ' + filtered.length + ' 能力';
|
|
|
+ if (!rows.length){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的 excerpt'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (const r of rows) wrap.appendChild(makeExcerptRow(r.frag, r.side, r.entry));
|
|
|
+}
|
|
|
+
|
|
|
+function renderFragmentsPatternRaw(filtered, wrap){
|
|
|
+ $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
|
|
|
+ if (!filtered.length){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (const f of filtered) wrap.appendChild(makeFragBodyRow(f));
|
|
|
+}
|
|
|
+
|
|
|
+function renderFragmentsPatternStructured(filtered, wrap){
|
|
|
+ $('fragCnt').textContent = filtered.length + ' / ' + data.fragments.length;
|
|
|
+ const sel = (state.itemsetIdx != null) ? data.itemsets[state.itemsetIdx] : null;
|
|
|
+ if (!sel){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '请先在第二列选择一个项集'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!filtered.length){
|
|
|
+ wrap.appendChild(el('div', {class:'empty-msg'}, '当前筛选下没有命中的能力'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const itemSet = new Set(sel.items);
|
|
|
+ for (const f of filtered){
|
|
|
+ const k = fragKey(f);
|
|
|
+ const excerpts = [];
|
|
|
+ const seen = new Set();
|
|
|
+ const addFrom = (list) => {
|
|
|
+ for (const e of (list || [])){
|
|
|
+ if (!itemSet.has(e.category_path)) continue;
|
|
|
+ if (!e.body_excerpt) continue;
|
|
|
+ if (seen.has(e.body_excerpt)) continue;
|
|
|
+ seen.add(e.body_excerpt);
|
|
|
+ excerpts.push(e.body_excerpt);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ addFrom(f.apply_shizhi);
|
|
|
+ addFrom(f.apply_xingshi);
|
|
|
+
|
|
|
+ const head = el('div', {class:'frag-head'},
|
|
|
+ el('span', {class:'case-badge'}, '案例'+f.case_index),
|
|
|
+ el('span', {class:'frag-badge'}, f.fragment_id),
|
|
|
+ el('span', {class:'act-badge'}, f.action)
|
|
|
+ );
|
|
|
+ const node = el('div', {
|
|
|
+ class: 'frag' + (state.fragKey === k ? ' active' : ''),
|
|
|
+ onClick: () => { state.fragKey = (state.fragKey === k) ? null : k; renderFragments(); renderDetail(); }
|
|
|
+ });
|
|
|
+ node.appendChild(head);
|
|
|
+ if (excerpts.length){
|
|
|
+ const wrapEx = el('div', {class:'frag-excerpts'});
|
|
|
+ for (const ex of excerpts) wrapEx.appendChild(el('div', {class:'ex'}, ex));
|
|
|
+ node.appendChild(wrapEx);
|
|
|
+ } else {
|
|
|
+ node.appendChild(el('div', {class:'frag-excerpts empty'}, '(本能力无 excerpt 命中该项集)'));
|
|
|
+ }
|
|
|
+ wrap.appendChild(node);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderFragments(){
|
|
|
+ const filtered = applyFilters(null);
|
|
|
+ const wrap = $('fragList'); wrap.innerHTML = '';
|
|
|
+ $('fragColTitle').textContent = '④ ' + (state.scopeMode === 'pattern' ? '分子能力' : '原子能力');
|
|
|
+ $('fvTabs').style.display = (state.scopeMode === 'pattern') ? 'flex' : 'none';
|
|
|
+ if (state.scopeMode === 'pattern'){
|
|
|
+ $('fvRaw').classList.toggle('active', state.patternView === 'raw');
|
|
|
+ $('fvStructured').classList.toggle('active', state.patternView === 'structured');
|
|
|
+ }
|
|
|
+ if (state.scopeMode === 'node'){
|
|
|
+ renderFragmentsNode(filtered, wrap);
|
|
|
+ } else {
|
|
|
+ if (state.patternView === 'structured') renderFragmentsPatternStructured(filtered, wrap);
|
|
|
+ else renderFragmentsPatternRaw(filtered, wrap);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderCaseDetail(wrap, caseIdx){
|
|
|
+ const c = casesByIndex[caseIdx];
|
|
|
+ if (!c) return;
|
|
|
+ const det = el('details', {class:'case-detail'});
|
|
|
+ const sumLabel = '📄 源: ' + (c.title || '案例 ' + caseIdx);
|
|
|
+ det.appendChild(el('summary', null, sumLabel));
|
|
|
+ const content = el('div', {class:'case-content'});
|
|
|
+
|
|
|
+ if (c.author) {
|
|
|
+ content.appendChild(el('div', {class:'case-author'}, '作者:' + c.author));
|
|
|
+ }
|
|
|
+ if (c.feedback && (c.feedback.like_count || c.feedback.comment_count || c.feedback.collect_count || c.feedback.share_count)){
|
|
|
+ const fb = c.feedback;
|
|
|
+ const row = el('div', {class:'case-feedback'});
|
|
|
+ if (fb.like_count != null) row.appendChild(el('span', {class:'fb'}, '👍 ' + fb.like_count));
|
|
|
+ if (fb.comment_count != null) row.appendChild(el('span', {class:'fb'}, '💬 ' + fb.comment_count));
|
|
|
+ if (fb.collect_count != null) row.appendChild(el('span', {class:'fb'}, '⭐ ' + fb.collect_count));
|
|
|
+ if (fb.share_count != null) row.appendChild(el('span', {class:'fb'}, '↗ ' + fb.share_count));
|
|
|
+ content.appendChild(row);
|
|
|
+ }
|
|
|
+
|
|
|
+ const imgs = (c.images && c.images.length) ? c.images : (c.cover ? [c.cover] : []);
|
|
|
+ if (imgs.length){
|
|
|
+ const wrapImg = el('div', {class:'case-images'});
|
|
|
+ for (const src of imgs) wrapImg.appendChild(el('img', {src, loading:'lazy'}));
|
|
|
+ content.appendChild(wrapImg);
|
|
|
+ }
|
|
|
+ if (c.body){
|
|
|
+ content.appendChild(el('div', {class:'case-body'}, c.body));
|
|
|
+ }
|
|
|
+ if (c.url){
|
|
|
+ content.appendChild(el('a', {href:c.url, target:'_blank', rel:'noopener', class:'case-link'}, '🔗 访问原始链接'));
|
|
|
+ }
|
|
|
+ det.appendChild(content);
|
|
|
+ wrap.appendChild(det);
|
|
|
+}
|
|
|
+
|
|
|
+function renderDetail(){
|
|
|
+ const wrap = $('detailBody'); wrap.innerHTML = '';
|
|
|
+ const f = data.fragments.find(x => fragKey(x) === state.fragKey);
|
|
|
+ if (!f){ wrap.appendChild(el('div', {class:'detail-empty'}, '点击左侧能力查看详情')); return; }
|
|
|
+
|
|
|
+ wrap.appendChild(el('h2', null,
|
|
|
+ el('span', {class:'case-badge'}, '案例 '+f.case_index),
|
|
|
+ el('span', {class:'frag-badge'}, f.fragment_id),
|
|
|
+ el('span', {class:'act-badge'}, f.action),
|
|
|
+ f.workflow_step_ref && f.workflow_step_ref.step_id ? el('span', {class:'pill'}, 'step '+f.workflow_step_ref.step_id) : null
|
|
|
+ ));
|
|
|
+
|
|
|
+ renderCaseDetail(wrap, f.case_index);
|
|
|
+
|
|
|
+ const ioSec = el('div', {class:'detail-section'}, el('h3', null, 'I/O 模态'));
|
|
|
+ ioSec.appendChild(el('div', {class:'frag-sig', style:'font-size:13px;margin-bottom:6px'}, f.modality_signature));
|
|
|
+ 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;
|
|
|
+ };
|
|
|
+ ioSec.appendChild(ioRow('IN', f.inputs, 'in'));
|
|
|
+ ioSec.appendChild(ioRow('OUT', f.outputs, 'out'));
|
|
|
+ wrap.appendChild(ioSec);
|
|
|
+
|
|
|
+ if (f.body){
|
|
|
+ wrap.appendChild(el('div', {class:'detail-section'},
|
|
|
+ el('h3', null, 'Body'),
|
|
|
+ el('div', {class:'body-text'}, f.body)
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ let hotPaths = new Set();
|
|
|
+ if (state.scopeMode === 'pattern' && state.itemsetIdx != null){
|
|
|
+ const sel = data.itemsets[state.itemsetIdx];
|
|
|
+ if (sel) for (const p of sel.items) hotPaths.add(p);
|
|
|
+ }
|
|
|
+ const renderApply = (label, arr) => {
|
|
|
+ const sec = el('div', {class:'detail-section'}, el('h3', null, 'Apply to · ' + label));
|
|
|
+ if (!arr || !arr.length){ sec.appendChild(el('div', {class:'empty-msg', style:'padding:8px'}, '无')); return sec; }
|
|
|
+ for (const e of arr){
|
|
|
+ let isHighlighted = false;
|
|
|
+ if (state.scopeMode === 'node'){
|
|
|
+ isHighlighted =
|
|
|
+ (label === '实质' && state.shizhiPath && (shiDesc.get(state.shizhiPath)||[]).includes(e.category_path)) ||
|
|
|
+ (label === '形式' && state.xingshiPath && (xingDesc.get(state.xingshiPath)||[]).includes(e.category_path));
|
|
|
+ } else {
|
|
|
+ isHighlighted = hotPaths.has(e.category_path);
|
|
|
+ }
|
|
|
+ const isSuggest = e.source === 'suggest';
|
|
|
+ const pl = el('div', {class:'pathline' + (isSuggest ? ' suggest' : ''), style: isHighlighted ? 'color:#ca8a04;font-weight:600' : ''},
|
|
|
+ e.category_path + (isSuggest ? ' ✦' : ''));
|
|
|
+ sec.appendChild(pl);
|
|
|
+ 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;
|
|
|
+ };
|
|
|
+ wrap.appendChild(renderApply('实质', f.apply_shizhi));
|
|
|
+ wrap.appendChild(renderApply('形式', f.apply_xingshi));
|
|
|
+
|
|
|
+ if (f.effects && f.effects.length){
|
|
|
+ const sec = el('div', {class:'detail-section'}, el('h3', null, 'Effects'));
|
|
|
+ f.effects.forEach((e,i)=>{
|
|
|
+ const card = el('div', {class:'effect-card'});
|
|
|
+ card.appendChild(el('div', {class:'effect-stmt'}, '#'+i+' ' + (e.statement || '-')));
|
|
|
+ if (e.criteria) card.appendChild(el('div', {class:'effect-meta'}, '判定标准:'+e.criteria));
|
|
|
+ if (e.judge_method) card.appendChild(el('div', {class:'effect-meta'}, '判定方式:'+e.judge_method));
|
|
|
+ if (e.negative_examples && e.negative_examples.length){
|
|
|
+ const ne = el('div', {class:'effect-meta'}, '反例:');
|
|
|
+ for (const n of e.negative_examples) ne.appendChild(el('div', {style:'margin-left:8px'}, '· '+n));
|
|
|
+ card.appendChild(ne);
|
|
|
+ }
|
|
|
+ sec.appendChild(card);
|
|
|
+ });
|
|
|
+ wrap.appendChild(sec);
|
|
|
+ }
|
|
|
+
|
|
|
+ const miscRows = [];
|
|
|
+ if (f.tools && f.tools.length) miscRows.push(['tools', f.tools.join(', ')]);
|
|
|
+ if (f.artifact_type) miscRows.push(['artifact_type', f.artifact_type]);
|
|
|
+ if (f.control_target && f.control_target.length) miscRows.push(['control_target', f.control_target.join(', ')]);
|
|
|
+ if (f.is_alternative_to && f.is_alternative_to.length) miscRows.push(['alt_to', f.is_alternative_to.join(', ')]);
|
|
|
+ if (miscRows.length){
|
|
|
+ const sec = el('div', {class:'detail-section'}, el('h3', null, '其他'));
|
|
|
+ for (const [k2,v2] of miscRows){
|
|
|
+ const row = el('div', {style:'font-size:11px;margin-bottom:3px'},
|
|
|
+ el('span', {style:'color:var(--muted);margin-right:8px'}, k2),
|
|
|
+ el('span', null, v2)
|
|
|
+ );
|
|
|
+ sec.appendChild(row);
|
|
|
+ }
|
|
|
+ wrap.appendChild(sec);
|
|
|
+ }
|
|
|
+
|
|
|
+ wrap.appendChild(el('details', null,
|
|
|
+ el('summary', null, '查看原始能力 JSON'),
|
|
|
+ el('pre', {class:'json-raw'}, JSON.stringify(f, null, 2))
|
|
|
+ ));
|
|
|
+}
|
|
|
+
|
|
|
+function renderChips(){
|
|
|
+ const c = $('chips'); c.innerHTML = '';
|
|
|
+ let any = false;
|
|
|
+ const mk = (lbl, val, onX) => {
|
|
|
+ any = true;
|
|
|
+ c.appendChild(el('span', {class:'filter-chip'},
|
|
|
+ el('span', {class:'lbl'}, lbl),
|
|
|
+ el('span', null, val),
|
|
|
+ el('span', {class:'x', onClick: () => { onX(); renderAll(); }}, '×')
|
|
|
+ ));
|
|
|
+ };
|
|
|
+ if (state.action) mk('动作', state.action, () => { state.action = null; });
|
|
|
+ if (state.scopeMode === 'node'){
|
|
|
+ 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; });
|
|
|
+ } else if (state.scopeMode === 'pattern' && state.itemsetIdx != null){
|
|
|
+ const sel = data.itemsets[state.itemsetIdx];
|
|
|
+ const desc = sel.items.map(p => p.split('/').pop()).join(' + ');
|
|
|
+ mk('项集 k='+sel.size, desc, () => { state.itemsetIdx = null; });
|
|
|
+ }
|
|
|
+ if (state.modSig) mk('模态', state.modSig, () => { state.modSig = null; });
|
|
|
+ $('clearAll').classList.toggle('active', any);
|
|
|
+}
|
|
|
+
|
|
|
+function renderStats(){}
|
|
|
+
|
|
|
+function renderAll(){
|
|
|
+ renderActions();
|
|
|
+ renderScope();
|
|
|
+ renderModalities();
|
|
|
+ renderFragments();
|
|
|
+ renderDetail();
|
|
|
+ renderChips();
|
|
|
+}
|
|
|
+
|
|
|
+function init(){
|
|
|
+ for (const c of (data.cases || [])) casesByIndex[c.index] = c;
|
|
|
+
|
|
|
+ SIDE_MODS = (() => {
|
|
|
+ const inSet = new Set(), outSet = new Set();
|
|
|
+ for (const m of data.modalities){
|
|
|
+ const p = sigParts(m.sig);
|
|
|
+ for (const x of p.in) inSet.add(x);
|
|
|
+ for (const x of p.out) outSet.add(x);
|
|
|
+ }
|
|
|
+ return { in: [...inSet].sort(), out: [...outSet].sort() };
|
|
|
+ })();
|
|
|
+
|
|
|
+ shiDesc = buildDescMap(data.subtree.shizhi || []);
|
|
|
+ xingDesc = buildDescMap(data.subtree.xingshi || []);
|
|
|
+ for (const f of data.fragments) fragPaths.set(fragKey(f), fragPathSet(f));
|
|
|
+
|
|
|
+ $('loading').style.display = 'none';
|
|
|
+ $('appRoot').style.display = '';
|
|
|
+
|
|
|
+ $('clearAll').addEventListener('click', () => {
|
|
|
+ state.action = null; state.shizhiPath = null; state.xingshiPath = null;
|
|
|
+ state.itemsetIdx = null; state.modSig = null;
|
|
|
+ state.modInFilter.clear(); state.modOutFilter.clear();
|
|
|
+ renderAll();
|
|
|
+ });
|
|
|
+ $('scopeClear').addEventListener('click', () => {
|
|
|
+ if (state.scopeMode === 'node'){ state.shizhiPath = null; state.xingshiPath = null; }
|
|
|
+ else { state.itemsetIdx = null; }
|
|
|
+ renderAll();
|
|
|
+ });
|
|
|
+ $('tabNode').addEventListener('click', () => { state.scopeMode = 'node'; renderAll(); });
|
|
|
+ $('tabPattern').addEventListener('click', () => { state.scopeMode = 'pattern'; renderAll(); });
|
|
|
+ $('fvRaw').addEventListener('click', () => { state.patternView = 'raw'; renderFragments(); });
|
|
|
+ $('fvStructured').addEventListener('click', () => { state.patternView = 'structured'; renderFragments(); });
|
|
|
+ $('modClear').addEventListener('click', () => {
|
|
|
+ state.modSig = null; state.modInFilter.clear(); state.modOutFilter.clear();
|
|
|
+ renderAll();
|
|
|
+ });
|
|
|
+
|
|
|
+ renderStats();
|
|
|
+ renderAll();
|
|
|
+}
|
|
|
+
|
|
|
+fetch('/api/viz/data/capability')
|
|
|
+ .then(r => {
|
|
|
+ if (!r.ok) throw new Error('fetch failed: ' + r.status);
|
|
|
+ return r.json();
|
|
|
+ })
|
|
|
+ .then(d => { data = d; init(); })
|
|
|
+ .catch(err => {
|
|
|
+ $('loading').textContent = '加载失败:' + err.message + '。请先上传数据:POST /api/viz/data/capability';
|
|
|
+ });
|
|
|
+</script>
|
|
|
+</body>
|
|
|
+</html>
|