@easysociable/cli 0.1.4 → 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 +190 -6
- package/content-engine/workbench/serve.py +0 -0
- package/content-engine/workbench/template.html +538 -117
- 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,6 +776,13 @@
|
|
|
648
776
|
gap_needs_n: "需求原话未选题 {n}",
|
|
649
777
|
gap_lib_n: "Capture 未入库 {n}",
|
|
650
778
|
gap_review_n: "已发但无法复盘 {n}",
|
|
779
|
+
kv_lane: "Lane",
|
|
780
|
+
kv_mode: "生成路径",
|
|
781
|
+
kv_needs: "需求",
|
|
782
|
+
kv_recs: "推荐对象",
|
|
783
|
+
kv_signal: "信号",
|
|
784
|
+
kv_hit_kind: "命中类型",
|
|
785
|
+
kv_hit_source: "来源配方",
|
|
651
786
|
kv_profile: "Profile",
|
|
652
787
|
kind_profile: "Profile", kind_account: "账号",
|
|
653
788
|
profile_pillars: "支柱",
|
|
@@ -733,7 +868,7 @@
|
|
|
733
868
|
thin_warn: "数据不足:result 不是 win / flat / loss,不画趋势。",
|
|
734
869
|
topic_bind: "选题绑定:{t} · swipe {s} · claim {c}",
|
|
735
870
|
supersedes: "取代 {id}", superseded_by: "被取代 {id}",
|
|
736
|
-
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",
|
|
737
872
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "发布",
|
|
738
873
|
kv_kind: "类型", kv_date: "日期", kv_status: "状态", kv_plat: "平台", kv_score: "分数", kv_relevance: "关联度", kv_ownership: "归属度", kv_product_relevance: "产品关联",
|
|
739
874
|
kv_use: "使用", kv_src: "来源", kv_result: "结果", kv_pillar: "支柱",
|
|
@@ -745,12 +880,38 @@
|
|
|
745
880
|
title: "Content Engine · Observatory",
|
|
746
881
|
brand: "Observatory",
|
|
747
882
|
search: "Search ID, one-liner, source…",
|
|
748
|
-
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",
|
|
749
884
|
notes_in: "Captured", notes_needs: "Needs", notes_lib: "Catalog",
|
|
750
885
|
lead: "Pick an IP, then inspect the stock.",
|
|
751
|
-
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",
|
|
752
887
|
kpi_cap_sub: "Capture", kpi_needs_sub: "Audience quotes", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
753
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",
|
|
754
915
|
ip_section: "IP · Profiles",
|
|
755
916
|
ip_all: "All profiles",
|
|
756
917
|
ip_accounts: "{n} account(s)",
|
|
@@ -764,6 +925,13 @@
|
|
|
764
925
|
gap_needs_n: "{n} needs not yet in a topic",
|
|
765
926
|
gap_lib_n: "{n} captures not ingested",
|
|
766
927
|
gap_review_n: "{n} shipped posts not reviewable",
|
|
928
|
+
kv_lane: "Lane",
|
|
929
|
+
kv_mode: "Generation mode",
|
|
930
|
+
kv_needs: "Needs",
|
|
931
|
+
kv_recs: "Recommendations",
|
|
932
|
+
kv_signal: "Signal",
|
|
933
|
+
kv_hit_kind: "Hit kind",
|
|
934
|
+
kv_hit_source: "Source recipe",
|
|
767
935
|
kv_profile: "Profile",
|
|
768
936
|
kind_profile: "Profile", kind_account: "Account",
|
|
769
937
|
profile_pillars: "Pillars",
|
|
@@ -849,7 +1017,7 @@
|
|
|
849
1017
|
thin_warn: "Too thin to review: result is not win / flat / loss. No chart.",
|
|
850
1018
|
topic_bind: "Topic: {t} · swipe {s} · claim {c}",
|
|
851
1019
|
supersedes: "supersedes {id}", superseded_by: "superseded by {id}",
|
|
852
|
-
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",
|
|
853
1021
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "Shipped",
|
|
854
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",
|
|
855
1023
|
kv_use: "Use", kv_src: "Source", kv_result: "Result", kv_pillar: "Pillar",
|
|
@@ -946,9 +1114,17 @@
|
|
|
946
1114
|
let topicPillar = "";
|
|
947
1115
|
let topicSrc = "";
|
|
948
1116
|
let topicUsage = "";
|
|
949
|
-
let discStatus = "";
|
|
950
|
-
let discSort = "recommend";
|
|
951
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"];
|
|
1127
|
+
const TOPIC_SOURCES = ["need", "inbox", "user", "product", "recommendation"];
|
|
952
1128
|
let selected = null;
|
|
953
1129
|
const $list = document.getElementById("list");
|
|
954
1130
|
const $insp = document.getElementById("insp");
|
|
@@ -1293,7 +1469,7 @@
|
|
|
1293
1469
|
sel.innerHTML = `<option value="">${t("ip_all")}</option>` +
|
|
1294
1470
|
ids.map(id => `<option value="${id}" ${id === profileScope ? "selected" : ""}>${id}</option>`).join("");
|
|
1295
1471
|
tabs.innerHTML = "";
|
|
1296
|
-
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"]) {
|
|
1297
1473
|
const b = document.createElement("button");
|
|
1298
1474
|
b.textContent = t(id);
|
|
1299
1475
|
b.dataset.view = id;
|
|
@@ -1353,7 +1529,7 @@
|
|
|
1353
1529
|
function qmatch(n) {
|
|
1354
1530
|
const q = $q.value.trim().toLowerCase();
|
|
1355
1531
|
if (!q) return true;
|
|
1356
|
-
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.product_ids, n.product_fit, n.recommendation_ids].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();
|
|
1357
1533
|
return blob.includes(q);
|
|
1358
1534
|
}
|
|
1359
1535
|
function connected(id) {
|
|
@@ -1454,7 +1630,7 @@
|
|
|
1454
1630
|
}).join("");
|
|
1455
1631
|
return `<div class="heat" title="${esc(t("heat_hint"))}">${chips}</div>${hint}`;
|
|
1456
1632
|
}
|
|
1457
|
-
function
|
|
1633
|
+
function listRowEl(n, extra) {
|
|
1458
1634
|
const d = document.createElement("div");
|
|
1459
1635
|
d.className = "row" + (selected === n.id ? " sel" : "");
|
|
1460
1636
|
d.dataset.kind = n.kind;
|
|
@@ -1473,46 +1649,6 @@
|
|
|
1473
1649
|
</div>`;
|
|
1474
1650
|
return d;
|
|
1475
1651
|
}
|
|
1476
|
-
function angleRecommendRank(n) {
|
|
1477
|
-
const r = String(n.angle_recommend || "").toLowerCase();
|
|
1478
|
-
if (r === "promote") return 0;
|
|
1479
|
-
if (r === "library") return 1;
|
|
1480
|
-
if (r === "hold") return 2;
|
|
1481
|
-
if (r === "reject") return 3;
|
|
1482
|
-
if (String(n.next_action || "") === "promote_to_topic") return 0;
|
|
1483
|
-
return 4;
|
|
1484
|
-
}
|
|
1485
|
-
function discoveryUnprocessed(n) {
|
|
1486
|
-
const st = String(n.status || "");
|
|
1487
|
-
if (st === "promoted" || st === "rejected") return false;
|
|
1488
|
-
const ang = String(n.angle_status || "");
|
|
1489
|
-
if (ang === "accepted") return false;
|
|
1490
|
-
if (ang === "held" || ang === "rejected") return false;
|
|
1491
|
-
if (ang === "proposed" && (n.angle_recommend === "promote" || !n.angle_recommend)) return true;
|
|
1492
|
-
if (String(n.next_action || "") === "promote_to_topic") return true;
|
|
1493
|
-
return false;
|
|
1494
|
-
}
|
|
1495
|
-
function sortDiscoveries(rows, mode) {
|
|
1496
|
-
const missing = -1;
|
|
1497
|
-
const num = (n, key) => {
|
|
1498
|
-
const v = Number(String(n[`src_${key}`] ?? "").replace(/,/g, ""));
|
|
1499
|
-
return Number.isFinite(v) ? v : missing;
|
|
1500
|
-
};
|
|
1501
|
-
return rows.slice().sort((a, b) => {
|
|
1502
|
-
if (mode === "recommend" || !mode) {
|
|
1503
|
-
const rec = angleRecommendRank(a) - angleRecommendRank(b);
|
|
1504
|
-
if (rec !== 0) return rec;
|
|
1505
|
-
}
|
|
1506
|
-
let cmp = 0;
|
|
1507
|
-
if (mode === "likes") cmp = num(b, "likes") - num(a, "likes");
|
|
1508
|
-
else if (mode === "replies") cmp = num(b, "replies") - num(a, "replies");
|
|
1509
|
-
else if (mode === "views") cmp = num(b, "views") - num(a, "views");
|
|
1510
|
-
else if (mode === "date") cmp = String(b.date || "").localeCompare(String(a.date || ""));
|
|
1511
|
-
else cmp = num(b, "bookmarks") - num(a, "bookmarks");
|
|
1512
|
-
if (cmp !== 0) return cmp;
|
|
1513
|
-
return String(b.date || "").localeCompare(String(a.date || ""));
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
1652
|
function pills(n) {
|
|
1517
1653
|
const bits = [];
|
|
1518
1654
|
if (n.kind === "need") {
|
|
@@ -1522,18 +1658,18 @@
|
|
|
1522
1658
|
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1523
1659
|
if (n.frequency) bits.push(`<span class="pill">×${esc(n.frequency)}</span>`);
|
|
1524
1660
|
}
|
|
1525
|
-
if (n.kind === "
|
|
1661
|
+
if (n.kind === "hit") {
|
|
1526
1662
|
const st = n.status || "";
|
|
1527
|
-
const cls = st === "
|
|
1528
|
-
: (st === "saved" || st === "rejected" || st === "unread" ? "gap" : "");
|
|
1663
|
+
const cls = st === "triage" ? "gap" : (st === "discarded" ? "" : "ok");
|
|
1529
1664
|
if (st) bits.push(`<span class="pill ${cls}">${esc(st)}</span>`);
|
|
1530
|
-
if (n.
|
|
1531
|
-
|
|
1532
|
-
if (n.
|
|
1533
|
-
if (n.next_action && n.next_action !== "none") bits.push(`<span class="pill">${esc(n.next_action)}</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>`);
|
|
1534
1668
|
}
|
|
1535
1669
|
if (n.kind === "topic") {
|
|
1536
1670
|
bits.push(`<span class="pill">${esc(n.status)}</span>`);
|
|
1671
|
+
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1672
|
+
if (n.generation_mode) bits.push(`<span class="pill">${esc(n.generation_mode)}</span>`);
|
|
1537
1673
|
bits.push(n.usage === "unused" ? `<span class="pill gap">${t("unused")}</span>` : `<span class="pill ok">${esc(n.usage)}</span>`);
|
|
1538
1674
|
}
|
|
1539
1675
|
if (n.kind === "run") {
|
|
@@ -1574,6 +1710,7 @@
|
|
|
1574
1710
|
for (const b of tabs.querySelectorAll("button")) b.classList.toggle("on", b.dataset.view === view);
|
|
1575
1711
|
if (view === "overview") renderOverview();
|
|
1576
1712
|
if (view === "needs") renderNeeds();
|
|
1713
|
+
if (view === "hits") renderHits();
|
|
1577
1714
|
if (view === "products") renderProducts();
|
|
1578
1715
|
if (view === "recommendations") renderRecommendations();
|
|
1579
1716
|
if (view === "topics") renderTopics();
|
|
@@ -1594,7 +1731,7 @@
|
|
|
1594
1731
|
n.frequency ? `×${n.frequency}` : "",
|
|
1595
1732
|
n.topic_ids && n.topic_ids !== "none" ? n.topic_ids : "",
|
|
1596
1733
|
].filter(Boolean).join(" · ");
|
|
1597
|
-
cards.appendChild(
|
|
1734
|
+
cards.appendChild(listRowEl(n, meta));
|
|
1598
1735
|
});
|
|
1599
1736
|
if (!needs.length) {
|
|
1600
1737
|
cards.innerHTML = `<p class='empty'>${t("no_need")}</p><p class="note">${esc(t("need_empty_hint"))}</p>`;
|
|
@@ -1608,6 +1745,299 @@
|
|
|
1608
1745
|
renderNeedsList($list);
|
|
1609
1746
|
}
|
|
1610
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
|
+
|
|
1611
2041
|
function renderProducts() {
|
|
1612
2042
|
$list.innerHTML = "";
|
|
1613
2043
|
const products = nodesOf("product").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
@@ -1709,58 +2139,30 @@
|
|
|
1709
2139
|
onReady(wrap);
|
|
1710
2140
|
}
|
|
1711
2141
|
|
|
1712
|
-
function
|
|
1713
|
-
|
|
1714
|
-
const
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
(w) => {
|
|
1727
|
-
const status = w.querySelector("#d-st");
|
|
1728
|
-
const sortEl = w.querySelector("#d-sort");
|
|
1729
|
-
const paint = () => {
|
|
1730
|
-
discStatus = status.value;
|
|
1731
|
-
discSort = sortEl.value || "recommend";
|
|
1732
|
-
const cards = document.createElement("div");
|
|
1733
|
-
cards.className = "cards";
|
|
1734
|
-
let rows = nodesOf("discovery").filter(qmatch);
|
|
1735
|
-
if (discStatus) rows = rows.filter(x => x.status === discStatus);
|
|
1736
|
-
rows = sortDiscoveries(rows, discSort);
|
|
1737
|
-
rows.forEach(x => {
|
|
1738
|
-
const meta = [
|
|
1739
|
-
x.angle_recommend || "",
|
|
1740
|
-
x.source_role,
|
|
1741
|
-
x.next_action && x.next_action !== "none" ? x.next_action : "",
|
|
1742
|
-
`@${x.source_handle || "—"}`,
|
|
1743
|
-
].filter(Boolean).join(" · ");
|
|
1744
|
-
cards.appendChild(discoveryRowEl(x, meta));
|
|
1745
|
-
});
|
|
1746
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_discovery")}</p>`;
|
|
1747
|
-
[...$list.querySelectorAll(".cards")].forEach(n => n.remove());
|
|
1748
|
-
$list.appendChild(cards);
|
|
1749
|
-
};
|
|
1750
|
-
status.onchange = paint;
|
|
1751
|
-
sortEl.onchange = paint;
|
|
1752
|
-
paint();
|
|
1753
|
-
}
|
|
1754
|
-
);
|
|
2142
|
+
function topicMeta(x) {
|
|
2143
|
+
const score = x.fit ? `${x.craft}/${x.fit}` : (x.craft || x.total || "—");
|
|
2144
|
+
const bits = [
|
|
2145
|
+
score,
|
|
2146
|
+
formatPillar(x.pillar),
|
|
2147
|
+
x.lane,
|
|
2148
|
+
x.generation_mode,
|
|
2149
|
+
x.source_type,
|
|
2150
|
+
x.need_ids && x.need_ids !== "none" ? x.need_ids : "",
|
|
2151
|
+
x.product_ids && x.product_ids !== "none" ? x.product_ids : "",
|
|
2152
|
+
x.recommendation_ids && x.recommendation_ids !== "none" ? x.recommendation_ids : "",
|
|
2153
|
+
x.capture_id || "",
|
|
2154
|
+
].filter(Boolean);
|
|
2155
|
+
return bits.join(" · ");
|
|
1755
2156
|
}
|
|
1756
2157
|
|
|
1757
2158
|
function renderTopics() {
|
|
1758
2159
|
$list.innerHTML = "";
|
|
2160
|
+
if (topicSrc && !TOPIC_SOURCES.includes(topicSrc)) topicSrc = "";
|
|
1759
2161
|
const statuses = [...new Set(nodesOf("topic").map(x => x.status).filter(Boolean))];
|
|
1760
2162
|
filterBar(
|
|
1761
2163
|
`<select id="f-st"><option value="">${t("f_status")}</option>${statuses.map(s => `<option value="${s}" ${s === topicStatus ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
|
|
1762
2164
|
<select id="f-p"><option value="">${t("f_pillar")}</option>${pillarIds().map(id => `<option value="${id}" ${id === topicPillar ? "selected" : ""}>${esc(formatPillar(id))}</option>`).join("")}</select>
|
|
1763
|
-
<select id="f-src"><option value="">${t("f_src")}</option>${
|
|
2165
|
+
<select id="f-src"><option value="">${t("f_src")}</option>${TOPIC_SOURCES.map(s => `<option value="${s}" ${s === topicSrc ? "selected" : ""}>${s}</option>`).join("")}</select>
|
|
1764
2166
|
<select id="f-u"><option value="">${t("f_use")}</option>
|
|
1765
2167
|
<option value="unused" ${topicUsage === "unused" ? "selected" : ""}>${t("f_unused")}</option>
|
|
1766
2168
|
<option value="promoted" ${topicUsage === "promoted" ? "selected" : ""}>${t("f_promoted")}</option></select>`,
|
|
@@ -1783,8 +2185,7 @@
|
|
|
1783
2185
|
if (topicSrc) rows = rows.filter(x => x.source_type === topicSrc);
|
|
1784
2186
|
if (topicUsage) rows = rows.filter(x => x.usage === topicUsage);
|
|
1785
2187
|
rows.forEach(x => {
|
|
1786
|
-
|
|
1787
|
-
cards.appendChild(rowEl(x, `${score} · ${formatPillar(x.pillar)} · ${x.source_type} @${x.src_handle || "—"}`));
|
|
2188
|
+
cards.appendChild(rowEl(x, topicMeta(x)));
|
|
1788
2189
|
});
|
|
1789
2190
|
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_topic")}</p>`;
|
|
1790
2191
|
$list.appendChild(cards);
|
|
@@ -1826,7 +2227,7 @@
|
|
|
1826
2227
|
let caps = nodesOf("capture").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
1827
2228
|
if (capIngest === "ingested") caps = caps.filter(c => c.status === "ingested");
|
|
1828
2229
|
if (capIngest === "not_ingested") caps = caps.filter(c => c.status !== "ingested");
|
|
1829
|
-
caps.forEach(c => cards.appendChild(
|
|
2230
|
+
caps.forEach(c => cards.appendChild(listRowEl(c, `${c.type} · ${c.status} · raw ${c.has_raw ? t("raw_yes") : t("raw_no")}`)));
|
|
1830
2231
|
if (!caps.length) cards.innerHTML = `<p class='empty'>${t("no_note")}</p>`;
|
|
1831
2232
|
$list.appendChild(cards);
|
|
1832
2233
|
return;
|
|
@@ -2026,6 +2427,7 @@
|
|
|
2026
2427
|
return {
|
|
2027
2428
|
capture: nodesOfAll("capture").filter(n => n.profile === profileId).length,
|
|
2028
2429
|
need: nodesOfAll("need").filter(n => n.profile === profileId).length,
|
|
2430
|
+
hit: nodesOfAll("hit").filter(n => n.profile === profileId).length,
|
|
2029
2431
|
topic: nodesOfAll("topic").filter(n => n.profile === profileId).length,
|
|
2030
2432
|
lib: GRAPH.nodes.filter(n => n.lib && (String(n.profiles || "").includes(profileId) || String(n.profiles || "").includes("*"))).length,
|
|
2031
2433
|
run: nodesOfAll("run").filter(n => n.profile === profileId).length,
|
|
@@ -2038,6 +2440,7 @@
|
|
|
2038
2440
|
return {
|
|
2039
2441
|
capture: nodesOfAll("capture").length,
|
|
2040
2442
|
need: nodesOfAll("need").length,
|
|
2443
|
+
hit: nodesOfAll("hit").length,
|
|
2041
2444
|
topic: nodesOfAll("topic").length,
|
|
2042
2445
|
lib: GRAPH.nodes.filter(n => n.lib).length,
|
|
2043
2446
|
run: nodesOfAll("run").length,
|
|
@@ -2047,20 +2450,21 @@
|
|
|
2047
2450
|
|
|
2048
2451
|
function gapsFor(profileId) {
|
|
2049
2452
|
const topics = profileId ? nodesOfAll("topic").filter(n => n.profile === profileId) : nodesOfAll("topic");
|
|
2050
|
-
const discs = profileId ? nodesOfAll("discovery").filter(n => n.profile === profileId) : nodesOfAll("discovery");
|
|
2051
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");
|
|
2052
2455
|
const caps = profileId ? nodesOfAll("capture").filter(n => n.profile === profileId) : nodesOfAll("capture");
|
|
2053
2456
|
const pubs = profileId ? nodesOfAll("published").filter(n => n.profile === profileId) : nodesOfAll("published");
|
|
2054
2457
|
const needCount = needs.length;
|
|
2055
2458
|
const gaps = [
|
|
2459
|
+
[hits.filter(h => h.status === "triage").length, "gap_hits_n"],
|
|
2056
2460
|
[topics.filter(x => x.status === "ready" && x.usage === "unused").length, "gap_ready_n"],
|
|
2057
|
-
[discs.filter(discoveryUnprocessed).length, "gap_angle_n"],
|
|
2058
2461
|
[needs.filter(n => n.status === "captured" && (!n.topic_ids || n.topic_ids === "none")).length, "gap_needs_n"],
|
|
2059
2462
|
[caps.filter(c => c.status !== "ingested").length, "gap_lib_n"],
|
|
2060
2463
|
[pubs.filter(p => !p.reviewable).length, "gap_review_n"],
|
|
2061
2464
|
].filter(([n]) => n > 0);
|
|
2062
2465
|
if (GRAPH.needs_index_present && needCount < 3) {
|
|
2063
|
-
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"]);
|
|
2064
2468
|
}
|
|
2065
2469
|
return gaps;
|
|
2066
2470
|
}
|
|
@@ -2069,6 +2473,7 @@
|
|
|
2069
2473
|
return `<div class="prof-stats">
|
|
2070
2474
|
<div><b>${counts.capture}</b><span>${t("kpi_cap")}</span></div>
|
|
2071
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>
|
|
2072
2477
|
<div><b>${counts.topic}</b><span>${t("kpi_topics")}</span></div>
|
|
2073
2478
|
<div><b>${counts.lib}</b><span>${t("kpi_lib")}</span></div>
|
|
2074
2479
|
<div><b>${counts.run}</b><span>Run</span></div>
|
|
@@ -2103,6 +2508,8 @@
|
|
|
2103
2508
|
["audience", t("doc_audience")],
|
|
2104
2509
|
["pillars", t("doc_pillars")],
|
|
2105
2510
|
["lanes", t("doc_lanes")],
|
|
2511
|
+
["keywords", t("doc_keywords")],
|
|
2512
|
+
["handles", t("doc_handles")],
|
|
2106
2513
|
];
|
|
2107
2514
|
const pillarRows = Object.entries(pillars).map(([id, p]) => {
|
|
2108
2515
|
const title = lang === "zh" ? p.zh : p.en;
|
|
@@ -2166,20 +2573,26 @@
|
|
|
2166
2573
|
? `${n.win || 0}w / ${n.loss || 0}l / n=${n.n}`
|
|
2167
2574
|
: "",
|
|
2168
2575
|
[t("kv_pillar")]: formatPillar(n.pillar),
|
|
2169
|
-
|
|
2170
|
-
"
|
|
2576
|
+
[t("kv_lane")]: n.kind === "topic" ? (n.lane || "") : "",
|
|
2577
|
+
[t("kv_mode")]: n.kind === "topic" ? (n.generation_mode || "") : "",
|
|
2171
2578
|
[t("kv_plat")]: n.platform || n.platforms,
|
|
2172
2579
|
[t("kv_profile")]: n.profile || n.profile_id || (n.profiles || ""),
|
|
2173
2580
|
[t("kv_score")]: n.fit ? `${n.craft} opp / ${n.fit} aud` : n.total,
|
|
2174
|
-
[t("kv_use")]: n.
|
|
2175
|
-
[t("kv_src")]: n.kind === "
|
|
2176
|
-
|
|
2581
|
+
[t("kv_use")]: n.usage,
|
|
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 || "") : "",
|
|
2587
|
+
[t("kv_needs")]: n.kind === "topic" ? (n.need_ids || "") : "",
|
|
2177
2588
|
Quote: n.kind === "need" ? (n.quote || "") : "",
|
|
2589
|
+
Excerpt: n.kind === "hit" ? (n.excerpt || "") : "",
|
|
2178
2590
|
Speaker: n.kind === "need" ? (n.speaker || "") : "",
|
|
2179
2591
|
Frequency: n.kind === "need" ? (n.frequency || "") : "",
|
|
2180
2592
|
Topics: n.kind === "need" ? (n.topic_ids || "") : "",
|
|
2181
2593
|
Products: n.kind === "topic" ? (n.product_ids || "") : "",
|
|
2182
2594
|
"Product fit": n.kind === "topic" ? (n.product_fit || "") : "",
|
|
2595
|
+
[t("kv_recs")]: n.kind === "topic" ? (n.recommendation_ids || "") : "",
|
|
2183
2596
|
Audience: n.kind === "product" ? (n.audience || "") : "",
|
|
2184
2597
|
Price: n.kind === "product" ? (n.price || "") : "",
|
|
2185
2598
|
"Effective from": n.kind === "product" ? (n.effective_from || "") : "",
|
|
@@ -2187,16 +2600,23 @@
|
|
|
2187
2600
|
Relationship: n.kind === "recommendation" ? (n.relationship || "") : "",
|
|
2188
2601
|
"Last researched": n.kind === "recommendation" ? (n.last_researched || "") : "",
|
|
2189
2602
|
Handle: n.source_handle || n.src_handle || "",
|
|
2190
|
-
[t("kv_heat_at")]: (n.kind === "
|
|
2603
|
+
[t("kv_heat_at")]: (n.kind === "capture" || n.kind === "hit") ? (n.engagement_at || "") : "",
|
|
2191
2604
|
[t("kv_result")]: n.result,
|
|
2192
2605
|
}).filter(([, v]) => v);
|
|
2193
|
-
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")
|
|
2194
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>` : ""}`
|
|
2195
2614
|
: "";
|
|
2196
2615
|
$insp.innerHTML = `<h2>${t("obj")}</h2>
|
|
2197
2616
|
<p class="kicker">${esc(kindName(n.kind))} · ${esc(n.ident)}</p>
|
|
2198
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>
|
|
2199
2618
|
<dl class="kv">${kv.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join("")}</dl>
|
|
2619
|
+
${kwBlock}
|
|
2200
2620
|
${heatBlock}
|
|
2201
2621
|
${n.kind === "swipe" || n.kind === "atom" ? `<p class="note">${esc(t("lib_use_hist"))}</p>` : ""}
|
|
2202
2622
|
${n.kind === "claim" ? `<p class="note">${esc(t("lib_claim_ev"))}</p>` : ""}
|
|
@@ -2211,6 +2631,7 @@
|
|
|
2211
2631
|
$insp.querySelectorAll("[data-goto]").forEach(btn => {
|
|
2212
2632
|
btn.onclick = () => select(btn.getAttribute("data-goto"));
|
|
2213
2633
|
});
|
|
2634
|
+
bindKwFilters($insp);
|
|
2214
2635
|
const trace = $insp.querySelector("#trace");
|
|
2215
2636
|
for (const k of TRACE_ORDER) {
|
|
2216
2637
|
const items = k === "published" ? publishedFor(n, ctx) : (byKind[k] || []);
|