@easysociable/cli 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content-engine/scripts/starter.mjs +1 -0
- package/content-engine/starter/CLAUDE.md +100 -38
- package/content-engine/starter/engine.json +1 -0
- package/content-engine/starter/hits/README.md +5 -0
- package/content-engine/starter/hits/_index.md +30 -0
- package/content-engine/starter/hits/_template.md +46 -0
- package/content-engine/starter/hits/entries/README.md +3 -0
- package/content-engine/starter/products/_index.md +1 -0
- package/content-engine/starter/products/entries/P-002-content-engine.md +136 -0
- package/content-engine/starter/profiles/default/README.md +3 -0
- package/content-engine/starter/profiles/default/handles.md +8 -0
- package/content-engine/starter/profiles/default/keywords.md +31 -0
- package/content-engine/starter/runs/_template/brief.md +41 -31
- package/content-engine/starter/topics/_template-item.md +30 -27
- package/content-engine/starter/topics/scoring.md +5 -2
- package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
- package/content-engine/workbench/build.py +177 -2
- package/content-engine/workbench/serve.py +0 -0
- package/content-engine/workbench/template.html +505 -16
- package/dist/easysociable.js +2 -1
- package/dist/easysociable.js.map +1 -1
- package/package.json +14 -13
- package/skills/easysociable-content/SKILL.md +18 -9
- package/skills/easysociable-content/workflows/NEEDS.md +20 -3
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
|
@@ -164,6 +164,7 @@
|
|
|
164
164
|
.row[data-kind="swipe"] .stripe { background: #c4a06a; }
|
|
165
165
|
.row[data-kind="atom"] .stripe { background: #a78bb0; }
|
|
166
166
|
.row[data-kind="claim"] .stripe { background: #d08a78; }
|
|
167
|
+
.row[data-kind="hit"] .stripe { background: #6b8f9e; }
|
|
167
168
|
.row[data-kind="run"] .stripe { background: #8aa87a; }
|
|
168
169
|
.row[data-kind="profile"] .stripe { background: #8b6b4a; }
|
|
169
170
|
.row[data-kind="account"] .stripe { background: #5c6b7a; }
|
|
@@ -189,6 +190,107 @@
|
|
|
189
190
|
.heat-chip.empty { opacity: .55; font-weight: 400; }
|
|
190
191
|
.heat-note { font-size: 11px; color: var(--faint); margin: 6px 0 0; }
|
|
191
192
|
.insp .heat { margin: 0 0 14px; }
|
|
193
|
+
.kw-filter { margin: 0 0 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
194
|
+
.kw-filter-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
195
|
+
.kw-search {
|
|
196
|
+
position: relative;
|
|
197
|
+
flex: 1 1 180px;
|
|
198
|
+
min-width: 160px;
|
|
199
|
+
max-width: 320px;
|
|
200
|
+
}
|
|
201
|
+
.kw-search::before {
|
|
202
|
+
content: ""; position: absolute; left: 12px; top: 50%; width: 12px; height: 12px;
|
|
203
|
+
margin-top: -7px; border: 1.5px solid var(--faint); border-radius: 50%;
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
}
|
|
206
|
+
.kw-search::after {
|
|
207
|
+
content: ""; position: absolute; left: 22px; top: 50%; width: 6px; height: 1.5px;
|
|
208
|
+
margin-top: 3px; background: var(--faint); transform: rotate(45deg);
|
|
209
|
+
pointer-events: none;
|
|
210
|
+
}
|
|
211
|
+
.kw-search input[type="search"] {
|
|
212
|
+
width: 100%;
|
|
213
|
+
background: var(--paper);
|
|
214
|
+
border: 1px solid var(--line);
|
|
215
|
+
color: var(--text);
|
|
216
|
+
border-radius: 999px;
|
|
217
|
+
padding: 8px 14px 8px 32px;
|
|
218
|
+
font: inherit;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
outline: none;
|
|
221
|
+
box-shadow: inset 0 1px 0 rgba(255, 250, 240, .55);
|
|
222
|
+
transition: border-color .15s ease, box-shadow .15s ease;
|
|
223
|
+
-webkit-appearance: none;
|
|
224
|
+
appearance: none;
|
|
225
|
+
}
|
|
226
|
+
.kw-search input[type="search"]::placeholder { color: var(--faint); }
|
|
227
|
+
.kw-search input[type="search"]::-webkit-search-decoration,
|
|
228
|
+
.kw-search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; }
|
|
229
|
+
.kw-search input[type="search"]:hover { border-color: color-mix(in srgb, var(--line) 40%, var(--accent-dim)); }
|
|
230
|
+
.kw-search input[type="search"]:focus {
|
|
231
|
+
border-color: var(--accent-dim);
|
|
232
|
+
box-shadow: inset 0 1px 0 rgba(255, 250, 240, .55), 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
|
|
233
|
+
}
|
|
234
|
+
.kw-quick { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; width: 100%; }
|
|
235
|
+
.kw-quick .kw-chip.on { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
236
|
+
.kw-active {
|
|
237
|
+
display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
|
|
238
|
+
border: 1px solid var(--line); background: var(--bg-2);
|
|
239
|
+
border-radius: 10px; padding: 8px 10px; font-size: 12px;
|
|
240
|
+
}
|
|
241
|
+
.kw-active .kid {
|
|
242
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
243
|
+
color: var(--accent); font-weight: 600;
|
|
244
|
+
}
|
|
245
|
+
.kw-active .why { color: var(--text); flex: 1 1 180px; }
|
|
246
|
+
.kw-active .meta { color: var(--muted); }
|
|
247
|
+
.kw-active .clear {
|
|
248
|
+
border: 0; background: transparent; color: var(--muted); cursor: pointer; font: inherit; font-size: 12px;
|
|
249
|
+
}
|
|
250
|
+
.kw-active .clear:hover { color: var(--text); }
|
|
251
|
+
.kw-chip {
|
|
252
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
253
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
254
|
+
font-size: 11px; padding: 3px 8px; border-radius: 999px;
|
|
255
|
+
border: 1px solid var(--line); background: var(--bg-3); color: var(--accent);
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
}
|
|
258
|
+
.kw-chip .n { font-family: inherit; color: var(--muted); font-weight: 600; }
|
|
259
|
+
.kw-chip.on .n { color: rgba(255,255,255,.85); }
|
|
260
|
+
.kw-chip:hover { border-color: var(--accent); }
|
|
261
|
+
.kw-catalog-grid {
|
|
262
|
+
display: grid;
|
|
263
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
264
|
+
gap: 10px;
|
|
265
|
+
margin-top: 12px;
|
|
266
|
+
}
|
|
267
|
+
.kw-catalog-card {
|
|
268
|
+
border: 1px solid var(--line); background: var(--bg-2);
|
|
269
|
+
border-radius: 12px; padding: 12px; text-align: left; color: inherit;
|
|
270
|
+
cursor: pointer; display: flex; flex-direction: column; gap: 6px;
|
|
271
|
+
}
|
|
272
|
+
.kw-catalog-card:hover { border-color: var(--accent); }
|
|
273
|
+
.kw-catalog-card .kid {
|
|
274
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
275
|
+
font-size: 12px; color: var(--accent); font-weight: 600;
|
|
276
|
+
}
|
|
277
|
+
.kw-catalog-card .meta { font-size: 11px; color: var(--muted); }
|
|
278
|
+
.kw-catalog-card .query {
|
|
279
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
280
|
+
font-size: 12px; color: var(--text); line-height: 1.4;
|
|
281
|
+
word-break: break-word;
|
|
282
|
+
}
|
|
283
|
+
.kw-catalog-card .why { font-size: 12px; color: var(--muted); line-height: 1.35; }
|
|
284
|
+
.kw-catalog-card .count { font-size: 11px; color: var(--faint); margin-top: auto; }
|
|
285
|
+
.kw-active .query {
|
|
286
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
287
|
+
font-size: 12px; color: var(--text); flex: 1 1 220px; word-break: break-word;
|
|
288
|
+
}
|
|
289
|
+
.kw-catalog-card .count b { color: var(--text); }
|
|
290
|
+
.kw-catalog-card .go {
|
|
291
|
+
font-size: 11px; color: var(--accent); border: 0; background: transparent;
|
|
292
|
+
padding: 0; cursor: pointer; text-align: left; font: inherit;
|
|
293
|
+
}
|
|
192
294
|
.row .id {
|
|
193
295
|
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
194
296
|
font-size: 11px; color: var(--accent); letter-spacing: .02em;
|
|
@@ -623,18 +725,44 @@
|
|
|
623
725
|
</div>
|
|
624
726
|
<script>
|
|
625
727
|
const GRAPH = /*__GRAPH__*/null;
|
|
626
|
-
const TRACE_ORDER = ["raw", "capture", "need", "product", "recommendation", "topic", "swipe", "atom", "claim", "run", "published"];
|
|
728
|
+
const TRACE_ORDER = ["raw", "capture", "hit", "need", "product", "recommendation", "topic", "swipe", "atom", "claim", "run", "published"];
|
|
627
729
|
const STR = {
|
|
628
730
|
zh: {
|
|
629
731
|
title: "Content Engine · 观察台",
|
|
630
732
|
brand: "观察台",
|
|
631
733
|
search: "搜索 ID、一句话、来源…",
|
|
632
|
-
overview: "总览", needs: "需求库", products: "产品库", recommendations: "推荐库", topics: "选题库", notes: "记录与知识", runs: "Run 档案", pub: "发布与复盘",
|
|
734
|
+
overview: "总览", needs: "需求库", hits: "Hits", products: "产品库", recommendations: "推荐库", topics: "选题库", notes: "记录与知识", runs: "Run 档案", pub: "发布与复盘",
|
|
633
735
|
notes_in: "记下的", notes_needs: "需求原话", notes_lib: "进库的",
|
|
634
736
|
lead: "先认 IP,再看库存。",
|
|
635
|
-
kpi_cap: "记下的", kpi_needs: "需求", kpi_lib: "进库的", kpi_topics: "选题", kpi_out: "下场",
|
|
737
|
+
kpi_cap: "记下的", kpi_needs: "需求", kpi_hits: "Hits", kpi_lib: "进库的", kpi_topics: "选题", kpi_out: "下场",
|
|
636
738
|
kpi_cap_sub: "Capture", kpi_needs_sub: "Needs 原话", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
637
739
|
kpi_topics_sub: "{n} 条 ready 未开单", kpi_out_sub: "Run / 已发帖",
|
|
740
|
+
kpi_hits_sub: "待分流 triage",
|
|
741
|
+
no_hit: "尚无 Hits",
|
|
742
|
+
no_keyword: "尚无 active 关键词",
|
|
743
|
+
hit_empty_hint: "对 Agent 说「扫关键词」写入 triage。路由后才会进 Needs 或 Library。",
|
|
744
|
+
gap_hits_n: "Hits 待分流 {n}",
|
|
745
|
+
f_signal: "信号",
|
|
746
|
+
f_hit_kind: "类型",
|
|
747
|
+
f_keyword: "关键词",
|
|
748
|
+
kw_search: "搜 K- / why…",
|
|
749
|
+
kw_triage: "待分流",
|
|
750
|
+
kw_hits: "命中",
|
|
751
|
+
kw_clear: "清除",
|
|
752
|
+
hits_sub_list: "命中",
|
|
753
|
+
hits_sub_catalog: "关键词",
|
|
754
|
+
kw_catalog_search: "搜全部关键词…",
|
|
755
|
+
kw_catalog_empty: "无匹配关键词",
|
|
756
|
+
kw_open_hits: "查看命中",
|
|
757
|
+
kw_track_all: "全部轨",
|
|
758
|
+
kw_track_listen: "listen 社交",
|
|
759
|
+
kw_track_search: "search SEO",
|
|
760
|
+
kw_track_ask: "ask GEO",
|
|
761
|
+
kw_vol: "量",
|
|
762
|
+
|
|
763
|
+
kw_catalog_count: "共 {n} 条",
|
|
764
|
+
doc_keywords: "Keywords",
|
|
765
|
+
doc_handles: "Handles",
|
|
638
766
|
ip_section: "IP · Profiles",
|
|
639
767
|
ip_all: "全部 Profile",
|
|
640
768
|
ip_accounts: "账号 {n}",
|
|
@@ -648,11 +776,13 @@
|
|
|
648
776
|
gap_needs_n: "需求原话未选题 {n}",
|
|
649
777
|
gap_lib_n: "Capture 未入库 {n}",
|
|
650
778
|
gap_review_n: "已发但无法复盘 {n}",
|
|
651
|
-
topics_lead: "选题必须有明确输入:need / inbox / user / product / recommendation。没有 Discovery 中间层;来源热度不是选题。",
|
|
652
779
|
kv_lane: "Lane",
|
|
653
780
|
kv_mode: "生成路径",
|
|
654
781
|
kv_needs: "需求",
|
|
655
782
|
kv_recs: "推荐对象",
|
|
783
|
+
kv_signal: "信号",
|
|
784
|
+
kv_hit_kind: "命中类型",
|
|
785
|
+
kv_hit_source: "来源配方",
|
|
656
786
|
kv_profile: "Profile",
|
|
657
787
|
kind_profile: "Profile", kind_account: "账号",
|
|
658
788
|
profile_pillars: "支柱",
|
|
@@ -738,7 +868,7 @@
|
|
|
738
868
|
thin_warn: "数据不足:result 不是 win / flat / loss,不画趋势。",
|
|
739
869
|
topic_bind: "选题绑定:{t} · swipe {s} · claim {c}",
|
|
740
870
|
supersedes: "取代 {id}", superseded_by: "被取代 {id}",
|
|
741
|
-
kind_raw: "Raw", kind_capture: "Capture", kind_need: "需求", kind_product: "产品", kind_recommendation: "推荐对象", kind_topic: "选题", kind_swipe: "Swipe",
|
|
871
|
+
kind_raw: "Raw", kind_capture: "Capture", kind_hit: "Hit", kind_need: "需求", kind_product: "产品", kind_recommendation: "推荐对象", kind_topic: "选题", kind_swipe: "Swipe",
|
|
742
872
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "发布",
|
|
743
873
|
kv_kind: "类型", kv_date: "日期", kv_status: "状态", kv_plat: "平台", kv_score: "分数", kv_relevance: "关联度", kv_ownership: "归属度", kv_product_relevance: "产品关联",
|
|
744
874
|
kv_use: "使用", kv_src: "来源", kv_result: "结果", kv_pillar: "支柱",
|
|
@@ -750,12 +880,38 @@
|
|
|
750
880
|
title: "Content Engine · Observatory",
|
|
751
881
|
brand: "Observatory",
|
|
752
882
|
search: "Search ID, one-liner, source…",
|
|
753
|
-
overview: "Overview", needs: "Needs", products: "Products", recommendations: "Recommendations", topics: "Topics", notes: "Notes & library", runs: "Run files", pub: "Shipped & review",
|
|
883
|
+
overview: "Overview", needs: "Needs", hits: "Hits", products: "Products", recommendations: "Recommendations", topics: "Topics", notes: "Notes & library", runs: "Run files", pub: "Shipped & review",
|
|
754
884
|
notes_in: "Captured", notes_needs: "Needs", notes_lib: "Catalog",
|
|
755
885
|
lead: "Pick an IP, then inspect the stock.",
|
|
756
|
-
kpi_cap: "Captured", kpi_needs: "Needs", kpi_lib: "Catalog", kpi_topics: "Topics", kpi_out: "Outcomes",
|
|
886
|
+
kpi_cap: "Captured", kpi_needs: "Needs", kpi_hits: "Hits", kpi_lib: "Catalog", kpi_topics: "Topics", kpi_out: "Outcomes",
|
|
757
887
|
kpi_cap_sub: "Capture", kpi_needs_sub: "Audience quotes", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
758
888
|
kpi_topics_sub: "{n} ready, never opened as a run", kpi_out_sub: "Runs / shipped URLs",
|
|
889
|
+
kpi_hits_sub: "awaiting triage",
|
|
890
|
+
no_hit: "No hits yet",
|
|
891
|
+
no_keyword: "No active keywords",
|
|
892
|
+
hit_empty_hint: "Ask the Agent to scan keywords. Hits stay in triage until you route them.",
|
|
893
|
+
gap_hits_n: "{n} hits awaiting triage",
|
|
894
|
+
f_signal: "Signal",
|
|
895
|
+
f_hit_kind: "Kind",
|
|
896
|
+
f_keyword: "Keyword",
|
|
897
|
+
kw_search: "Search K- / why…",
|
|
898
|
+
kw_triage: "triage",
|
|
899
|
+
kw_hits: "hits",
|
|
900
|
+
kw_clear: "Clear",
|
|
901
|
+
hits_sub_list: "Hits",
|
|
902
|
+
hits_sub_catalog: "Keywords",
|
|
903
|
+
kw_catalog_search: "Search all keywords…",
|
|
904
|
+
kw_catalog_empty: "No matching keywords",
|
|
905
|
+
kw_open_hits: "View hits",
|
|
906
|
+
kw_track_all: "All tracks",
|
|
907
|
+
kw_track_listen: "listen",
|
|
908
|
+
kw_track_search: "search SEO",
|
|
909
|
+
kw_track_ask: "ask GEO",
|
|
910
|
+
kw_vol: "vol",
|
|
911
|
+
|
|
912
|
+
kw_catalog_count: "{n} total",
|
|
913
|
+
doc_keywords: "Keywords",
|
|
914
|
+
doc_handles: "Handles",
|
|
759
915
|
ip_section: "IP · Profiles",
|
|
760
916
|
ip_all: "All profiles",
|
|
761
917
|
ip_accounts: "{n} account(s)",
|
|
@@ -769,11 +925,13 @@
|
|
|
769
925
|
gap_needs_n: "{n} needs not yet in a topic",
|
|
770
926
|
gap_lib_n: "{n} captures not ingested",
|
|
771
927
|
gap_review_n: "{n} shipped posts not reviewable",
|
|
772
|
-
topics_lead: "Topics need an explicit input: need, inbox, user, product, or recommendation. No Discovery layer; source heat is not a topic.",
|
|
773
928
|
kv_lane: "Lane",
|
|
774
929
|
kv_mode: "Generation mode",
|
|
775
930
|
kv_needs: "Needs",
|
|
776
931
|
kv_recs: "Recommendations",
|
|
932
|
+
kv_signal: "Signal",
|
|
933
|
+
kv_hit_kind: "Hit kind",
|
|
934
|
+
kv_hit_source: "Source recipe",
|
|
777
935
|
kv_profile: "Profile",
|
|
778
936
|
kind_profile: "Profile", kind_account: "Account",
|
|
779
937
|
profile_pillars: "Pillars",
|
|
@@ -859,7 +1017,7 @@
|
|
|
859
1017
|
thin_warn: "Too thin to review: result is not win / flat / loss. No chart.",
|
|
860
1018
|
topic_bind: "Topic: {t} · swipe {s} · claim {c}",
|
|
861
1019
|
supersedes: "supersedes {id}", superseded_by: "superseded by {id}",
|
|
862
|
-
kind_raw: "Raw", kind_capture: "Capture", kind_need: "Need", kind_product: "Product", kind_recommendation: "Recommendation", kind_topic: "Topic", kind_swipe: "Swipe",
|
|
1020
|
+
kind_raw: "Raw", kind_capture: "Capture", kind_hit: "Hit", kind_need: "Need", kind_product: "Product", kind_recommendation: "Recommendation", kind_topic: "Topic", kind_swipe: "Swipe",
|
|
863
1021
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "Shipped",
|
|
864
1022
|
kv_kind: "Kind", kv_date: "Date", kv_status: "Status", kv_plat: "Platform", kv_score: "Score", kv_relevance: "Relevance", kv_ownership: "Ownership", kv_product_relevance: "Product relevance",
|
|
865
1023
|
kv_use: "Use", kv_src: "Source", kv_result: "Result", kv_pillar: "Pillar",
|
|
@@ -957,6 +1115,15 @@
|
|
|
957
1115
|
let topicSrc = "";
|
|
958
1116
|
let topicUsage = "";
|
|
959
1117
|
let capIngest = "";
|
|
1118
|
+
let hitKw = "";
|
|
1119
|
+
let hitKwQ = "";
|
|
1120
|
+
let hitSt = "";
|
|
1121
|
+
let hitSig = "";
|
|
1122
|
+
let hitsSub = "list"; // list = 命中 · catalog = 关键词
|
|
1123
|
+
let kwCatalogQ = "";
|
|
1124
|
+
let kwCatalogTrack = ""; // "" | listen | search | ask
|
|
1125
|
+
|
|
1126
|
+
const KW_TRACKS = ["listen", "search", "ask"];
|
|
960
1127
|
const TOPIC_SOURCES = ["need", "inbox", "user", "product", "recommendation"];
|
|
961
1128
|
let selected = null;
|
|
962
1129
|
const $list = document.getElementById("list");
|
|
@@ -1302,7 +1469,7 @@
|
|
|
1302
1469
|
sel.innerHTML = `<option value="">${t("ip_all")}</option>` +
|
|
1303
1470
|
ids.map(id => `<option value="${id}" ${id === profileScope ? "selected" : ""}>${id}</option>`).join("");
|
|
1304
1471
|
tabs.innerHTML = "";
|
|
1305
|
-
for (const id of ["overview", "needs", "products", "recommendations", "topics", "notes", "runs", "pub"]) {
|
|
1472
|
+
for (const id of ["overview", "hits", "needs", "products", "recommendations", "topics", "notes", "runs", "pub"]) {
|
|
1306
1473
|
const b = document.createElement("button");
|
|
1307
1474
|
b.textContent = t(id);
|
|
1308
1475
|
b.dataset.view = id;
|
|
@@ -1362,7 +1529,7 @@
|
|
|
1362
1529
|
function qmatch(n) {
|
|
1363
1530
|
const q = $q.value.trim().toLowerCase();
|
|
1364
1531
|
if (!q) return true;
|
|
1365
|
-
const blob = [n.ident, n.label, n.kind, n.status, n.pillar, n.lane, n.generation_mode, n.profile, n.profiles, n.src_handle, n.url, n.notes, n.type, n.atom_type, n.topic_id, n.audience, n.need_ids, n.product_ids, n.product_fit, n.recommendation_ids, n.source_type, n.capture_id].filter(Boolean).join(" ").toLowerCase();
|
|
1532
|
+
const blob = [n.ident, n.label, n.kind, n.status, n.pillar, n.lane, n.generation_mode, n.profile, n.profiles, n.src_handle, n.url, n.notes, n.type, n.atom_type, n.topic_id, n.audience, n.need_ids, n.product_ids, n.product_fit, n.recommendation_ids, n.source_type, n.capture_id, n.source, n.keyword_id, n.hit_kind, n.signal, n.excerpt].filter(Boolean).join(" ").toLowerCase();
|
|
1366
1533
|
return blob.includes(q);
|
|
1367
1534
|
}
|
|
1368
1535
|
function connected(id) {
|
|
@@ -1491,6 +1658,14 @@
|
|
|
1491
1658
|
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1492
1659
|
if (n.frequency) bits.push(`<span class="pill">×${esc(n.frequency)}</span>`);
|
|
1493
1660
|
}
|
|
1661
|
+
if (n.kind === "hit") {
|
|
1662
|
+
const st = n.status || "";
|
|
1663
|
+
const cls = st === "triage" ? "gap" : (st === "discarded" ? "" : "ok");
|
|
1664
|
+
if (st) bits.push(`<span class="pill ${cls}">${esc(st)}</span>`);
|
|
1665
|
+
if (n.keyword_id) bits.push(`<button type="button" class="kw-chip" data-kw-filter="${esc(n.keyword_id)}">${esc(n.keyword_id)}</button>`);
|
|
1666
|
+
if (n.signal) bits.push(`<span class="pill">${esc(n.signal)}</span>`);
|
|
1667
|
+
if (n.hit_kind) bits.push(`<span class="pill">${esc(n.hit_kind)}</span>`);
|
|
1668
|
+
}
|
|
1494
1669
|
if (n.kind === "topic") {
|
|
1495
1670
|
bits.push(`<span class="pill">${esc(n.status)}</span>`);
|
|
1496
1671
|
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
@@ -1535,6 +1710,7 @@
|
|
|
1535
1710
|
for (const b of tabs.querySelectorAll("button")) b.classList.toggle("on", b.dataset.view === view);
|
|
1536
1711
|
if (view === "overview") renderOverview();
|
|
1537
1712
|
if (view === "needs") renderNeeds();
|
|
1713
|
+
if (view === "hits") renderHits();
|
|
1538
1714
|
if (view === "products") renderProducts();
|
|
1539
1715
|
if (view === "recommendations") renderRecommendations();
|
|
1540
1716
|
if (view === "topics") renderTopics();
|
|
@@ -1569,6 +1745,299 @@
|
|
|
1569
1745
|
renderNeedsList($list);
|
|
1570
1746
|
}
|
|
1571
1747
|
|
|
1748
|
+
function keywordsInScope(opts = {}) {
|
|
1749
|
+
const all = GRAPH.keywords || [];
|
|
1750
|
+
let rows = profileScope
|
|
1751
|
+
? all.filter(k => k.profile === profileScope)
|
|
1752
|
+
: all.slice();
|
|
1753
|
+
if (opts.activeOnly !== false) rows = rows.filter(k => k.status === "active" || !k.status);
|
|
1754
|
+
if (opts.track) rows = rows.filter(k => (k.track || "listen") === opts.track);
|
|
1755
|
+
return rows;
|
|
1756
|
+
}
|
|
1757
|
+
function listenKeywordsInScope() {
|
|
1758
|
+
return keywordsInScope({ track: "listen" });
|
|
1759
|
+
}
|
|
1760
|
+
function hitCountForKeyword(kid) {
|
|
1761
|
+
return nodesOf("hit").filter(h => h.keyword_id === kid).length;
|
|
1762
|
+
}
|
|
1763
|
+
function triageCountForKeyword(kid) {
|
|
1764
|
+
return nodesOf("hit").filter(h => h.keyword_id === kid && h.status === "triage").length;
|
|
1765
|
+
}
|
|
1766
|
+
function keywordMatchesQuery(k, q) {
|
|
1767
|
+
if (!q) return true;
|
|
1768
|
+
const blob = [k.id, k.why, k.query, k.intent, k.layer, k.pillar].filter(Boolean).join(" ").toLowerCase();
|
|
1769
|
+
return blob.includes(q);
|
|
1770
|
+
}
|
|
1771
|
+
function rankedKeywords(opts = {}) {
|
|
1772
|
+
const base = opts.forHits ? listenKeywordsInScope() : keywordsInScope({ activeOnly: false, track: opts.track || "" });
|
|
1773
|
+
return base
|
|
1774
|
+
.map(k => ({
|
|
1775
|
+
...k,
|
|
1776
|
+
track: k.track || "listen",
|
|
1777
|
+
triage: triageCountForKeyword(k.id),
|
|
1778
|
+
total: hitCountForKeyword(k.id),
|
|
1779
|
+
}))
|
|
1780
|
+
.sort((a, b) => (b.triage - a.triage) || (b.total - a.total) || (Number(b.weight || 0) - Number(a.weight || 0)));
|
|
1781
|
+
}
|
|
1782
|
+
function keywordById(kid) {
|
|
1783
|
+
return (GRAPH.keywords || []).find(k => k.id === kid && (!profileScope || k.profile === profileScope))
|
|
1784
|
+
|| (GRAPH.keywords || []).find(k => k.id === kid)
|
|
1785
|
+
|| null;
|
|
1786
|
+
}
|
|
1787
|
+
function keywordIdNum(id) {
|
|
1788
|
+
const m = String(id || "").match(/K-(\d+)/i);
|
|
1789
|
+
return m ? Number(m[1]) : 0;
|
|
1790
|
+
}
|
|
1791
|
+
function keywordOptionLabel(k) {
|
|
1792
|
+
const triage = typeof k.triage === "number" ? k.triage : triageCountForKeyword(k.id);
|
|
1793
|
+
const total = typeof k.total === "number" ? k.total : hitCountForKeyword(k.id);
|
|
1794
|
+
const bits = [k.id];
|
|
1795
|
+
if (k.track && k.track !== "listen") bits.push(k.track);
|
|
1796
|
+
if (k.layer) bits.push(k.layer);
|
|
1797
|
+
bits.push(`${triage}/${total}`);
|
|
1798
|
+
return bits.join(" · ");
|
|
1799
|
+
}
|
|
1800
|
+
function bindKwFilters(root) {
|
|
1801
|
+
root.querySelectorAll("[data-kw-filter]").forEach(btn => {
|
|
1802
|
+
btn.onclick = (e) => {
|
|
1803
|
+
e.preventDefault();
|
|
1804
|
+
e.stopPropagation();
|
|
1805
|
+
const kid = btn.getAttribute("data-kw-filter") || "";
|
|
1806
|
+
hitKw = hitKw === kid ? "" : kid;
|
|
1807
|
+
hitsSub = "list";
|
|
1808
|
+
renderHits();
|
|
1809
|
+
};
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
function renderKeywordFilter(into) {
|
|
1813
|
+
const wrap = document.createElement("div");
|
|
1814
|
+
wrap.className = "kw-filter";
|
|
1815
|
+
const ranked = rankedKeywords({ forHits: true });
|
|
1816
|
+
const q = hitKwQ.trim().toLowerCase();
|
|
1817
|
+
const filtered = ranked.filter(k => keywordMatchesQuery(k, q));
|
|
1818
|
+
const withHits = filtered.filter(k => k.total > 0);
|
|
1819
|
+
const listenN = listenKeywordsInScope().length;
|
|
1820
|
+
|
|
1821
|
+
const row = document.createElement("div");
|
|
1822
|
+
row.className = "kw-filter-row";
|
|
1823
|
+
const searchWrap = document.createElement("div");
|
|
1824
|
+
searchWrap.className = "kw-search";
|
|
1825
|
+
const search = document.createElement("input");
|
|
1826
|
+
search.type = "search";
|
|
1827
|
+
search.placeholder = t("kw_search");
|
|
1828
|
+
search.setAttribute("aria-label", t("kw_search"));
|
|
1829
|
+
search.value = hitKwQ;
|
|
1830
|
+
search.oninput = () => {
|
|
1831
|
+
const pos = search.selectionStart;
|
|
1832
|
+
hitKwQ = search.value;
|
|
1833
|
+
renderHits();
|
|
1834
|
+
const s2 = $list.querySelector(".kw-search input[type='search']");
|
|
1835
|
+
if (s2) {
|
|
1836
|
+
s2.focus();
|
|
1837
|
+
try { s2.setSelectionRange(pos, pos); } catch (_) {}
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1840
|
+
searchWrap.appendChild(search);
|
|
1841
|
+
row.appendChild(searchWrap);
|
|
1842
|
+
wrap.appendChild(row);
|
|
1843
|
+
|
|
1844
|
+
const quickWrap = document.createElement("div");
|
|
1845
|
+
quickWrap.className = "kw-quick";
|
|
1846
|
+
if (!listenN) {
|
|
1847
|
+
quickWrap.innerHTML = `<span class="vacant">${esc(t("no_keyword"))}</span>`;
|
|
1848
|
+
} else if (!withHits.length) {
|
|
1849
|
+
quickWrap.innerHTML = `<span class="vacant">${esc(t("no_hit"))}</span>`;
|
|
1850
|
+
} else {
|
|
1851
|
+
withHits.forEach(k => {
|
|
1852
|
+
const b = document.createElement("button");
|
|
1853
|
+
b.type = "button";
|
|
1854
|
+
b.className = "kw-chip" + (hitKw === k.id ? " on" : "");
|
|
1855
|
+
b.innerHTML = `${esc(k.id)}<span class="n">${k.triage}</span>`;
|
|
1856
|
+
b.title = [k.query, k.why, `${k.triage} ${t("kw_triage")} · ${k.total} ${t("kw_hits")}`].filter(Boolean).join("\n");
|
|
1857
|
+
b.onclick = () => {
|
|
1858
|
+
hitKw = hitKw === k.id ? "" : k.id;
|
|
1859
|
+
renderHits();
|
|
1860
|
+
};
|
|
1861
|
+
quickWrap.appendChild(b);
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
wrap.appendChild(quickWrap);
|
|
1865
|
+
|
|
1866
|
+
if (hitKw) {
|
|
1867
|
+
const k = keywordById(hitKw);
|
|
1868
|
+
const active = document.createElement("div");
|
|
1869
|
+
active.className = "kw-active";
|
|
1870
|
+
const triage = triageCountForKeyword(hitKw);
|
|
1871
|
+
const total = hitCountForKeyword(hitKw);
|
|
1872
|
+
active.innerHTML = `<span class="kid">${esc(hitKw)}</span>
|
|
1873
|
+
<span class="meta">${esc([(k && k.layer) || "", (k && k.intent) || "", (k && k.pillar) || ""].filter(Boolean).join(" · "))}</span>
|
|
1874
|
+
<span class="query" title="${esc((k && k.why) || "")}">${esc((k && k.query) || "")}</span>
|
|
1875
|
+
<span class="meta"><b>${triage}</b> ${esc(t("kw_triage"))} · ${total} ${esc(t("kw_hits"))}</span>
|
|
1876
|
+
<button type="button" class="clear">${esc(t("kw_clear"))}</button>`;
|
|
1877
|
+
active.querySelector(".clear").onclick = () => { hitKw = ""; renderHits(); };
|
|
1878
|
+
wrap.appendChild(active);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
into.appendChild(wrap);
|
|
1882
|
+
return { filtered };
|
|
1883
|
+
}
|
|
1884
|
+
function renderHitsSubtabs() {
|
|
1885
|
+
const sub = document.createElement("div");
|
|
1886
|
+
sub.className = "subtabs";
|
|
1887
|
+
sub.innerHTML = `<button type="button" data-hits-sub="list">${t("hits_sub_list")}</button><button type="button" data-hits-sub="catalog">${t("hits_sub_catalog")}</button>`;
|
|
1888
|
+
sub.querySelectorAll("button").forEach(b => {
|
|
1889
|
+
b.classList.toggle("on", b.dataset.hitsSub === hitsSub);
|
|
1890
|
+
b.onclick = () => {
|
|
1891
|
+
hitsSub = b.dataset.hitsSub;
|
|
1892
|
+
renderHits();
|
|
1893
|
+
};
|
|
1894
|
+
});
|
|
1895
|
+
$list.appendChild(sub);
|
|
1896
|
+
}
|
|
1897
|
+
function openKeywordInHits(kid) {
|
|
1898
|
+
hitKw = kid || "";
|
|
1899
|
+
hitsSub = "list";
|
|
1900
|
+
renderHits();
|
|
1901
|
+
}
|
|
1902
|
+
function renderKeywordCatalog() {
|
|
1903
|
+
const row = document.createElement("div");
|
|
1904
|
+
row.className = "kw-filter-row";
|
|
1905
|
+
const searchWrap = document.createElement("div");
|
|
1906
|
+
searchWrap.className = "kw-search";
|
|
1907
|
+
const search = document.createElement("input");
|
|
1908
|
+
search.type = "search";
|
|
1909
|
+
search.placeholder = t("kw_catalog_search");
|
|
1910
|
+
search.setAttribute("aria-label", t("kw_catalog_search"));
|
|
1911
|
+
search.value = kwCatalogQ;
|
|
1912
|
+
search.oninput = () => {
|
|
1913
|
+
const pos = search.selectionStart;
|
|
1914
|
+
kwCatalogQ = search.value;
|
|
1915
|
+
renderHits();
|
|
1916
|
+
const s2 = $list.querySelector(".kw-search input[type='search']");
|
|
1917
|
+
if (s2) {
|
|
1918
|
+
s2.focus();
|
|
1919
|
+
try { s2.setSelectionRange(pos, pos); } catch (_) {}
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
searchWrap.appendChild(search);
|
|
1923
|
+
row.appendChild(searchWrap);
|
|
1924
|
+
|
|
1925
|
+
const tracks = document.createElement("div");
|
|
1926
|
+
tracks.className = "kw-quick";
|
|
1927
|
+
[
|
|
1928
|
+
["", "kw_track_all"],
|
|
1929
|
+
["listen", "kw_track_listen"],
|
|
1930
|
+
["search", "kw_track_search"],
|
|
1931
|
+
["ask", "kw_track_ask"],
|
|
1932
|
+
].forEach(([id, labelKey]) => {
|
|
1933
|
+
const b = document.createElement("button");
|
|
1934
|
+
b.type = "button";
|
|
1935
|
+
b.className = "kw-chip" + (kwCatalogTrack === id ? " on" : "");
|
|
1936
|
+
b.textContent = t(labelKey);
|
|
1937
|
+
b.onclick = () => { kwCatalogTrack = id; renderHits(); };
|
|
1938
|
+
tracks.appendChild(b);
|
|
1939
|
+
});
|
|
1940
|
+
row.appendChild(tracks);
|
|
1941
|
+
$list.appendChild(row);
|
|
1942
|
+
|
|
1943
|
+
const q = kwCatalogQ.trim().toLowerCase();
|
|
1944
|
+
const keys = rankedKeywords({ track: kwCatalogTrack || "" })
|
|
1945
|
+
.filter(k => keywordMatchesQuery(k, q))
|
|
1946
|
+
.sort((a, b) => keywordIdNum(a.id) - keywordIdNum(b.id));
|
|
1947
|
+
const count = document.createElement("p");
|
|
1948
|
+
count.className = "note";
|
|
1949
|
+
count.style.margin = "8px 0 0";
|
|
1950
|
+
count.textContent = t("kw_catalog_count", { n: keys.length });
|
|
1951
|
+
$list.appendChild(count);
|
|
1952
|
+
const grid = document.createElement("div");
|
|
1953
|
+
grid.className = "kw-catalog-grid";
|
|
1954
|
+
if (!keys.length) {
|
|
1955
|
+
grid.innerHTML = `<p class='empty'>${esc(t("kw_catalog_empty"))}</p>`;
|
|
1956
|
+
} else {
|
|
1957
|
+
keys.forEach(k => {
|
|
1958
|
+
const card = document.createElement("button");
|
|
1959
|
+
card.type = "button";
|
|
1960
|
+
card.className = "kw-catalog-card";
|
|
1961
|
+
const vol = k.track === "search" && k.volume
|
|
1962
|
+
? ` · ${t("kw_vol")} ${esc(k.volume)}`
|
|
1963
|
+
: "";
|
|
1964
|
+
const go = k.track === "listen"
|
|
1965
|
+
? `<span class="go">${esc(t("kw_open_hits"))} →</span>`
|
|
1966
|
+
: `<span class="meta">${esc(k.track)}</span>`;
|
|
1967
|
+
card.innerHTML = `<div class="kid">${esc(k.id)} <span class="pill">${esc(k.track || "listen")}</span></div>
|
|
1968
|
+
<div class="meta">${esc([k.layer, k.intent, k.pillar, k.status].filter(Boolean).join(" · "))}${vol}</div>
|
|
1969
|
+
<div class="query">${esc(k.query || "")}</div>
|
|
1970
|
+
${k.why ? `<div class="why">${esc(k.why)}</div>` : ""}
|
|
1971
|
+
<div class="count"><b>${k.triage}</b> ${esc(t("kw_triage"))} · ${k.total} ${esc(t("kw_hits"))}${k.last_scanned ? ` · ${esc(k.last_scanned)}` : ""}</div>
|
|
1972
|
+
${go}`;
|
|
1973
|
+
card.onclick = () => {
|
|
1974
|
+
if ((k.track || "listen") === "listen") openKeywordInHits(k.id);
|
|
1975
|
+
};
|
|
1976
|
+
grid.appendChild(card);
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
$list.appendChild(grid);
|
|
1980
|
+
}
|
|
1981
|
+
function renderHitsList() {
|
|
1982
|
+
const statuses = [...new Set(nodesOf("hit").map(x => x.status).filter(Boolean))];
|
|
1983
|
+
const signals = [...new Set(nodesOf("hit").map(x => x.signal).filter(Boolean))];
|
|
1984
|
+
const { filtered } = renderKeywordFilter($list);
|
|
1985
|
+
const kwOptions = filtered.length ? filtered : rankedKeywords({ forHits: true });
|
|
1986
|
+
filterBar(
|
|
1987
|
+
`<select id="h-st"><option value="">${t("f_status")}</option>${statuses.map(s => `<option value="${s}" ${s === hitSt ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
|
|
1988
|
+
<select id="h-sig"><option value="">${t("f_signal")}</option>${signals.map(s => `<option value="${s}" ${s === hitSig ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
|
|
1989
|
+
<select id="h-kw"><option value="">${t("f_keyword")} (${listenKeywordsInScope().length})</option>${kwOptions.map(k => `<option value="${esc(k.id)}" ${k.id === hitKw ? "selected" : ""}>${esc(keywordOptionLabel(k))}</option>`).join("")}</select>`,
|
|
1990
|
+
(w) => {
|
|
1991
|
+
const st = w.querySelector("#h-st");
|
|
1992
|
+
const sig = w.querySelector("#h-sig");
|
|
1993
|
+
const kw = w.querySelector("#h-kw");
|
|
1994
|
+
if (hitKw && ![...kw.options].some(o => o.value === hitKw)) {
|
|
1995
|
+
const opt = document.createElement("option");
|
|
1996
|
+
opt.value = hitKw;
|
|
1997
|
+
opt.textContent = keywordOptionLabel(keywordById(hitKw) || { id: hitKw });
|
|
1998
|
+
opt.selected = true;
|
|
1999
|
+
kw.appendChild(opt);
|
|
2000
|
+
}
|
|
2001
|
+
const paintCards = () => {
|
|
2002
|
+
[...$list.querySelectorAll(".cards")].forEach(n => n.remove());
|
|
2003
|
+
const cards = document.createElement("div");
|
|
2004
|
+
cards.className = "cards";
|
|
2005
|
+
let rows = nodesOf("hit").filter(qmatch).sort((a, b) => {
|
|
2006
|
+
const ta = a.status === "triage" ? 0 : 1;
|
|
2007
|
+
const tb = b.status === "triage" ? 0 : 1;
|
|
2008
|
+
if (ta !== tb) return ta - tb;
|
|
2009
|
+
const ha = Number(String(a.src_likes || "0").replace(/,/g, "")) || 0;
|
|
2010
|
+
const hb = Number(String(b.src_likes || "0").replace(/,/g, "")) || 0;
|
|
2011
|
+
if (hb !== ha) return hb - ha;
|
|
2012
|
+
return String(b.date || "").localeCompare(String(a.date || ""));
|
|
2013
|
+
});
|
|
2014
|
+
if (hitSt) rows = rows.filter(x => x.status === hitSt);
|
|
2015
|
+
if (hitSig) rows = rows.filter(x => x.signal === hitSig);
|
|
2016
|
+
if (hitKw) rows = rows.filter(x => x.keyword_id === hitKw);
|
|
2017
|
+
rows.forEach(x => {
|
|
2018
|
+
const meta = [x.platform, x.hit_kind, x.signal].filter(Boolean).join(" · ");
|
|
2019
|
+
cards.appendChild(listRowEl(x, meta));
|
|
2020
|
+
});
|
|
2021
|
+
if (!rows.length) {
|
|
2022
|
+
cards.innerHTML = `<p class='empty'>${t("no_hit")}</p><p class="note">${esc(t("hit_empty_hint"))}</p>`;
|
|
2023
|
+
}
|
|
2024
|
+
$list.appendChild(cards);
|
|
2025
|
+
bindKwFilters(cards);
|
|
2026
|
+
};
|
|
2027
|
+
st.onchange = () => { hitSt = st.value || ""; paintCards(); };
|
|
2028
|
+
sig.onchange = () => { hitSig = sig.value || ""; paintCards(); };
|
|
2029
|
+
kw.onchange = () => { hitKw = kw.value || ""; renderHits(); };
|
|
2030
|
+
paintCards();
|
|
2031
|
+
}
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
function renderHits() {
|
|
2035
|
+
$list.innerHTML = "";
|
|
2036
|
+
renderHitsSubtabs();
|
|
2037
|
+
if (hitsSub === "catalog") renderKeywordCatalog();
|
|
2038
|
+
else renderHitsList();
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1572
2041
|
function renderProducts() {
|
|
1573
2042
|
$list.innerHTML = "";
|
|
1574
2043
|
const products = nodesOf("product").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
@@ -1688,7 +2157,6 @@
|
|
|
1688
2157
|
|
|
1689
2158
|
function renderTopics() {
|
|
1690
2159
|
$list.innerHTML = "";
|
|
1691
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">${esc(t("topics_lead"))}</p>`);
|
|
1692
2160
|
if (topicSrc && !TOPIC_SOURCES.includes(topicSrc)) topicSrc = "";
|
|
1693
2161
|
const statuses = [...new Set(nodesOf("topic").map(x => x.status).filter(Boolean))];
|
|
1694
2162
|
filterBar(
|
|
@@ -1959,6 +2427,7 @@
|
|
|
1959
2427
|
return {
|
|
1960
2428
|
capture: nodesOfAll("capture").filter(n => n.profile === profileId).length,
|
|
1961
2429
|
need: nodesOfAll("need").filter(n => n.profile === profileId).length,
|
|
2430
|
+
hit: nodesOfAll("hit").filter(n => n.profile === profileId).length,
|
|
1962
2431
|
topic: nodesOfAll("topic").filter(n => n.profile === profileId).length,
|
|
1963
2432
|
lib: GRAPH.nodes.filter(n => n.lib && (String(n.profiles || "").includes(profileId) || String(n.profiles || "").includes("*"))).length,
|
|
1964
2433
|
run: nodesOfAll("run").filter(n => n.profile === profileId).length,
|
|
@@ -1971,6 +2440,7 @@
|
|
|
1971
2440
|
return {
|
|
1972
2441
|
capture: nodesOfAll("capture").length,
|
|
1973
2442
|
need: nodesOfAll("need").length,
|
|
2443
|
+
hit: nodesOfAll("hit").length,
|
|
1974
2444
|
topic: nodesOfAll("topic").length,
|
|
1975
2445
|
lib: GRAPH.nodes.filter(n => n.lib).length,
|
|
1976
2446
|
run: nodesOfAll("run").length,
|
|
@@ -1981,17 +2451,20 @@
|
|
|
1981
2451
|
function gapsFor(profileId) {
|
|
1982
2452
|
const topics = profileId ? nodesOfAll("topic").filter(n => n.profile === profileId) : nodesOfAll("topic");
|
|
1983
2453
|
const needs = profileId ? nodesOfAll("need").filter(n => n.profile === profileId) : nodesOfAll("need");
|
|
2454
|
+
const hits = profileId ? nodesOfAll("hit").filter(n => n.profile === profileId) : nodesOfAll("hit");
|
|
1984
2455
|
const caps = profileId ? nodesOfAll("capture").filter(n => n.profile === profileId) : nodesOfAll("capture");
|
|
1985
2456
|
const pubs = profileId ? nodesOfAll("published").filter(n => n.profile === profileId) : nodesOfAll("published");
|
|
1986
2457
|
const needCount = needs.length;
|
|
1987
2458
|
const gaps = [
|
|
2459
|
+
[hits.filter(h => h.status === "triage").length, "gap_hits_n"],
|
|
1988
2460
|
[topics.filter(x => x.status === "ready" && x.usage === "unused").length, "gap_ready_n"],
|
|
1989
2461
|
[needs.filter(n => n.status === "captured" && (!n.topic_ids || n.topic_ids === "none")).length, "gap_needs_n"],
|
|
1990
2462
|
[caps.filter(c => c.status !== "ingested").length, "gap_lib_n"],
|
|
1991
2463
|
[pubs.filter(p => !p.reviewable).length, "gap_review_n"],
|
|
1992
2464
|
].filter(([n]) => n > 0);
|
|
1993
2465
|
if (GRAPH.needs_index_present && needCount < 3) {
|
|
1994
|
-
gaps.
|
|
2466
|
+
const idx = gaps.findIndex(([, k]) => k === "gap_needs_n");
|
|
2467
|
+
gaps.splice(idx < 0 ? gaps.length : idx, 0, [needCount, "gap_needs_thin_n"]);
|
|
1995
2468
|
}
|
|
1996
2469
|
return gaps;
|
|
1997
2470
|
}
|
|
@@ -2000,6 +2473,7 @@
|
|
|
2000
2473
|
return `<div class="prof-stats">
|
|
2001
2474
|
<div><b>${counts.capture}</b><span>${t("kpi_cap")}</span></div>
|
|
2002
2475
|
<div><b>${counts.need || 0}</b><span>${t("kpi_needs")}</span></div>
|
|
2476
|
+
<div><b>${counts.hit || 0}</b><span>${t("kpi_hits")}</span></div>
|
|
2003
2477
|
<div><b>${counts.topic}</b><span>${t("kpi_topics")}</span></div>
|
|
2004
2478
|
<div><b>${counts.lib}</b><span>${t("kpi_lib")}</span></div>
|
|
2005
2479
|
<div><b>${counts.run}</b><span>Run</span></div>
|
|
@@ -2034,6 +2508,8 @@
|
|
|
2034
2508
|
["audience", t("doc_audience")],
|
|
2035
2509
|
["pillars", t("doc_pillars")],
|
|
2036
2510
|
["lanes", t("doc_lanes")],
|
|
2511
|
+
["keywords", t("doc_keywords")],
|
|
2512
|
+
["handles", t("doc_handles")],
|
|
2037
2513
|
];
|
|
2038
2514
|
const pillarRows = Object.entries(pillars).map(([id, p]) => {
|
|
2039
2515
|
const title = lang === "zh" ? p.zh : p.en;
|
|
@@ -2103,9 +2579,14 @@
|
|
|
2103
2579
|
[t("kv_profile")]: n.profile || n.profile_id || (n.profiles || ""),
|
|
2104
2580
|
[t("kv_score")]: n.fit ? `${n.craft} opp / ${n.fit} aud` : n.total,
|
|
2105
2581
|
[t("kv_use")]: n.usage,
|
|
2106
|
-
[t("kv_src")]: n.source_type,
|
|
2582
|
+
[t("kv_src")]: n.kind === "hit" ? (n.source || "") : n.source_type,
|
|
2583
|
+
[t("kv_hit_kind")]: n.kind === "hit" ? (n.hit_kind || "") : "",
|
|
2584
|
+
[t("kv_signal")]: n.kind === "hit" ? (n.signal || "") : "",
|
|
2585
|
+
Keyword: n.kind === "hit" ? (n.keyword_id || "") : "",
|
|
2586
|
+
[t("kv_hit_source")]: n.kind === "hit" ? (n.source || "") : "",
|
|
2107
2587
|
[t("kv_needs")]: n.kind === "topic" ? (n.need_ids || "") : "",
|
|
2108
2588
|
Quote: n.kind === "need" ? (n.quote || "") : "",
|
|
2589
|
+
Excerpt: n.kind === "hit" ? (n.excerpt || "") : "",
|
|
2109
2590
|
Speaker: n.kind === "need" ? (n.speaker || "") : "",
|
|
2110
2591
|
Frequency: n.kind === "need" ? (n.frequency || "") : "",
|
|
2111
2592
|
Topics: n.kind === "need" ? (n.topic_ids || "") : "",
|
|
@@ -2119,16 +2600,23 @@
|
|
|
2119
2600
|
Relationship: n.kind === "recommendation" ? (n.relationship || "") : "",
|
|
2120
2601
|
"Last researched": n.kind === "recommendation" ? (n.last_researched || "") : "",
|
|
2121
2602
|
Handle: n.source_handle || n.src_handle || "",
|
|
2122
|
-
[t("kv_heat_at")]: n.kind === "capture" ? (n.engagement_at || "") : "",
|
|
2603
|
+
[t("kv_heat_at")]: (n.kind === "capture" || n.kind === "hit") ? (n.engagement_at || "") : "",
|
|
2123
2604
|
[t("kv_result")]: n.result,
|
|
2124
2605
|
}).filter(([, v]) => v);
|
|
2125
|
-
const
|
|
2606
|
+
const kwMeta = n.kind === "hit" && n.keyword_id
|
|
2607
|
+
? (GRAPH.keywords || []).find(k => k.id === n.keyword_id && (!n.profile || k.profile === n.profile))
|
|
2608
|
+
: null;
|
|
2609
|
+
const kwBlock = n.kind === "hit" && n.keyword_id
|
|
2610
|
+
? `<p class="note"><button type="button" class="kw-chip" data-kw-filter="${esc(n.keyword_id)}">${esc(n.keyword_id)}</button>${kwMeta ? ` · ${esc([kwMeta.layer, kwMeta.intent, kwMeta.why].filter(Boolean).join(" · "))}` : ""}</p>`
|
|
2611
|
+
: "";
|
|
2612
|
+
const heatBlock = (n.kind === "capture" || n.kind === "hit")
|
|
2126
2613
|
? `${heatStripHtml(n, { hint: true })}${n.source_url ? `<p class="note">${t("src_url")}<a href="${esc(n.source_url)}" target="_blank" rel="noreferrer">${esc(n.source_url)}</a></p>` : ""}`
|
|
2127
2614
|
: "";
|
|
2128
2615
|
$insp.innerHTML = `<h2>${t("obj")}</h2>
|
|
2129
2616
|
<p class="kicker">${esc(kindName(n.kind))} · ${esc(n.ident)}</p>
|
|
2130
2617
|
<p class="title">${n.kind === "run" ? `<button type="button" class="ghost-link title-btn" data-run-open="${esc(n.id)}">${esc(n.label)}</button>` : esc(n.label)}</p>
|
|
2131
2618
|
<dl class="kv">${kv.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join("")}</dl>
|
|
2619
|
+
${kwBlock}
|
|
2132
2620
|
${heatBlock}
|
|
2133
2621
|
${n.kind === "swipe" || n.kind === "atom" ? `<p class="note">${esc(t("lib_use_hist"))}</p>` : ""}
|
|
2134
2622
|
${n.kind === "claim" ? `<p class="note">${esc(t("lib_claim_ev"))}</p>` : ""}
|
|
@@ -2143,6 +2631,7 @@
|
|
|
2143
2631
|
$insp.querySelectorAll("[data-goto]").forEach(btn => {
|
|
2144
2632
|
btn.onclick = () => select(btn.getAttribute("data-goto"));
|
|
2145
2633
|
});
|
|
2634
|
+
bindKwFilters($insp);
|
|
2146
2635
|
const trace = $insp.querySelector("#trace");
|
|
2147
2636
|
for (const k of TRACE_ORDER) {
|
|
2148
2637
|
const items = k === "published" ? publishedFor(n, ctx) : (byKind[k] || []);
|