@easysociable/cli 0.1.3 → 0.1.5
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/README.md +13 -10
- package/content-engine/scripts/starter.mjs +3 -1
- package/content-engine/scripts/workbench.mjs +13 -8
- package/content-engine/starter/CLAUDE.md +217 -128
- package/content-engine/starter/engine.json +3 -0
- package/content-engine/starter/needs/README.md +38 -0
- package/content-engine/starter/needs/_index.md +31 -0
- package/content-engine/starter/needs/_template.md +24 -0
- package/content-engine/starter/needs/entries/README.md +5 -0
- package/content-engine/starter/products/README.md +38 -0
- package/content-engine/starter/products/_index.md +24 -0
- package/content-engine/starter/products/_template.md +56 -0
- package/content-engine/starter/products/entries/P-001-easysociable.md +150 -0
- package/content-engine/starter/profiles/_index.md +6 -3
- package/content-engine/starter/profiles/default/README.md +4 -1
- package/content-engine/starter/profiles/default/boundaries.md +14 -0
- package/content-engine/starter/profiles/default/expression.md +18 -0
- package/content-engine/starter/profiles/default/lanes.md +12 -0
- package/content-engine/starter/profiles/default/stories.md +15 -0
- package/content-engine/starter/profiles/default/voice.md +14 -0
- package/content-engine/starter/recommendations/README.md +21 -0
- package/content-engine/starter/recommendations/_index.md +21 -0
- package/content-engine/starter/recommendations/_template.md +44 -0
- package/content-engine/starter/runs/_template/brief.md +1 -1
- package/content-engine/starter/runs/_template/pack.md +15 -7
- package/content-engine/starter/runs/_template/packet.md +6 -0
- package/content-engine/starter/swipe/_template.md +3 -1
- package/content-engine/starter/topics/README.md +4 -8
- package/content-engine/starter/topics/_index.md +12 -5
- package/content-engine/starter/topics/_template-item.md +64 -51
- package/content-engine/starter/topics/scoring.md +55 -4
- package/content-engine/workbench/build.py +315 -97
- package/content-engine/workbench/serve.py +0 -0
- package/content-engine/workbench/template.html +172 -94
- package/dist/easysociable.js +191 -133
- package/dist/easysociable.js.map +1 -1
- package/package.json +13 -14
- package/skills/ONBOARDING.md +14 -15
- package/skills/easysociable-content/SKILL.md +100 -19
- package/skills/easysociable-content/workflows/INTERVIEW.md +70 -0
- package/skills/easysociable-content/workflows/NEEDS.md +82 -0
- package/skills/easysociable-slideshow/SKILL.md +79 -45
- package/skills/easysociable-slideshow/references/ARC.md +44 -0
- package/skills/easysociable-slideshow/references/CONTENT_PACK.md +34 -6
- package/skills/easysociable-slideshow/workflows/CREATE_RUN.md +24 -5
- package/skills/easysociable-slideshow/workflows/OUT_OF_SCOPE.md +3 -0
- package/skills/easysociable-template/SKILL.md +13 -16
- package/skills/easysociable-template/references/CATALOG_METADATA.md +21 -18
- package/skills/easysociable-template/scripts/generate-theme-from-hex.mjs +0 -0
- package/skills/easysociable-template/workflows/FILL_AND_RENDER.md +4 -83
- package/skills/easysociable-template/workflows/OUT_OF_SCOPE.md +3 -2
- package/content-engine/starter/discovery/README.md +0 -18
- package/content-engine/starter/discovery/_index.md +0 -27
- package/content-engine/starter/discovery/_template.md +0 -41
- package/content-engine/starter/topics/WATCHLIST.md +0 -65
- package/content-engine/starter/topics/watchlists/README.md +0 -10
- package/content-engine/starter/topics/watchlists/_shared.md +0 -14
- package/content-engine/starter/topics/watchlists/default.md +0 -6
|
@@ -158,8 +158,8 @@
|
|
|
158
158
|
.row.sel { border-color: var(--accent); box-shadow: var(--shadow); }
|
|
159
159
|
.stripe { border-radius: 14px 0 0 14px; background: var(--accent-dim); }
|
|
160
160
|
.row[data-kind="capture"] .stripe { background: #7d9aa8; }
|
|
161
|
+
.row[data-kind="need"] .stripe { background: #5a9e8f; }
|
|
161
162
|
.row[data-kind="raw"] .stripe { background: #6e7f6a; }
|
|
162
|
-
.row[data-kind="discovery"] .stripe { background: #6b7c8a; }
|
|
163
163
|
.row[data-kind="topic"] .stripe { background: var(--accent); }
|
|
164
164
|
.row[data-kind="swipe"] .stripe { background: #c4a06a; }
|
|
165
165
|
.row[data-kind="atom"] .stripe { background: #a78bb0; }
|
|
@@ -623,17 +623,17 @@
|
|
|
623
623
|
</div>
|
|
624
624
|
<script>
|
|
625
625
|
const GRAPH = /*__GRAPH__*/null;
|
|
626
|
-
const TRACE_ORDER = ["raw", "capture", "
|
|
626
|
+
const TRACE_ORDER = ["raw", "capture", "need", "product", "recommendation", "topic", "swipe", "atom", "claim", "run", "published"];
|
|
627
627
|
const STR = {
|
|
628
628
|
zh: {
|
|
629
629
|
title: "Content Engine · 观察台",
|
|
630
630
|
brand: "观察台",
|
|
631
631
|
search: "搜索 ID、一句话、来源…",
|
|
632
|
-
overview: "总览",
|
|
633
|
-
notes_in: "记下的", notes_lib: "进库的",
|
|
632
|
+
overview: "总览", needs: "需求库", products: "产品库", recommendations: "推荐库", topics: "选题库", notes: "记录与知识", runs: "Run 档案", pub: "发布与复盘",
|
|
633
|
+
notes_in: "记下的", notes_needs: "需求原话", notes_lib: "进库的",
|
|
634
634
|
lead: "先认 IP,再看库存。",
|
|
635
|
-
kpi_cap: "记下的", kpi_lib: "进库的", kpi_topics: "选题", kpi_out: "下场",
|
|
636
|
-
kpi_cap_sub: "Capture", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
635
|
+
kpi_cap: "记下的", kpi_needs: "需求", kpi_lib: "进库的", kpi_topics: "选题", kpi_out: "下场",
|
|
636
|
+
kpi_cap_sub: "Capture", kpi_needs_sub: "Needs 原话", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
637
637
|
kpi_topics_sub: "{n} 条 ready 未开单", kpi_out_sub: "Run / 已发帖",
|
|
638
638
|
ip_section: "IP · Profiles",
|
|
639
639
|
ip_all: "全部 Profile",
|
|
@@ -644,8 +644,15 @@
|
|
|
644
644
|
gap_section: "断链",
|
|
645
645
|
gap_none: "没有明显断链。",
|
|
646
646
|
gap_ready_n: "ready 选题未开单 {n}",
|
|
647
|
+
gap_needs_thin_n: "需求原话不足 3 条(出图硬门闩)· 现有 {n}",
|
|
648
|
+
gap_needs_n: "需求原话未选题 {n}",
|
|
647
649
|
gap_lib_n: "Capture 未入库 {n}",
|
|
648
650
|
gap_review_n: "已发但无法复盘 {n}",
|
|
651
|
+
topics_lead: "选题必须有明确输入:need / inbox / user / product / recommendation。没有 Discovery 中间层;来源热度不是选题。",
|
|
652
|
+
kv_lane: "Lane",
|
|
653
|
+
kv_mode: "生成路径",
|
|
654
|
+
kv_needs: "需求",
|
|
655
|
+
kv_recs: "推荐对象",
|
|
649
656
|
kv_profile: "Profile",
|
|
650
657
|
kind_profile: "Profile", kind_account: "账号",
|
|
651
658
|
profile_pillars: "支柱",
|
|
@@ -657,6 +664,7 @@
|
|
|
657
664
|
doc_voice: "Voice",
|
|
658
665
|
doc_audience: "Audience",
|
|
659
666
|
doc_pillars: "Pillars",
|
|
667
|
+
doc_lanes: "Lanes",
|
|
660
668
|
empty_profile_t: "点选一个 Profile",
|
|
661
669
|
empty_profile_b: "右侧会显示该 IP 的支柱、账号、档案与断链。",
|
|
662
670
|
acct_handle: "Handle",
|
|
@@ -680,6 +688,7 @@
|
|
|
680
688
|
lib_claim_ev: "命题证据在本 claim 的 Evidence 表;命中率看 n/win/loss。",
|
|
681
689
|
f_status: "状态(登记)", f_pillar: "Pillar", f_src: "来源", f_use: "使用", f_unused: "未开单", f_promoted: "已开单",
|
|
682
690
|
f_sort: "排序",
|
|
691
|
+
f_sort_recommend: "报题优先",
|
|
683
692
|
f_sort_bookmarks: "Bookmarks ↓",
|
|
684
693
|
f_sort_replies: "Replies ↓",
|
|
685
694
|
f_sort_likes: "Likes ↓",
|
|
@@ -702,7 +711,8 @@
|
|
|
702
711
|
f_cap_ingest: "入库",
|
|
703
712
|
f_cap_ingested: "已入库",
|
|
704
713
|
f_cap_not_ingested: "未入库",
|
|
705
|
-
no_topic: "无匹配选题",
|
|
714
|
+
no_topic: "无匹配选题", no_note: "无匹配记录", no_need: "尚无需求原话", no_pub: "尚无已发帖",
|
|
715
|
+
need_empty_hint: "对 Agent 说「记需求:……」写入第一条读者原话。不要编造。",
|
|
706
716
|
no_stage: "无阶段文件",
|
|
707
717
|
empty_t: "从左边拾起一件东西",
|
|
708
718
|
empty_b: "它从哪来、有没有被写过、发过没有、值不值得再拿出来——会在这里连成一条。",
|
|
@@ -728,7 +738,7 @@
|
|
|
728
738
|
thin_warn: "数据不足:result 不是 win / flat / loss,不画趋势。",
|
|
729
739
|
topic_bind: "选题绑定:{t} · swipe {s} · claim {c}",
|
|
730
740
|
supersedes: "取代 {id}", superseded_by: "被取代 {id}",
|
|
731
|
-
kind_raw: "Raw", kind_capture: "Capture",
|
|
741
|
+
kind_raw: "Raw", kind_capture: "Capture", kind_need: "需求", kind_product: "产品", kind_recommendation: "推荐对象", kind_topic: "选题", kind_swipe: "Swipe",
|
|
732
742
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "发布",
|
|
733
743
|
kv_kind: "类型", kv_date: "日期", kv_status: "状态", kv_plat: "平台", kv_score: "分数", kv_relevance: "关联度", kv_ownership: "归属度", kv_product_relevance: "产品关联",
|
|
734
744
|
kv_use: "使用", kv_src: "来源", kv_result: "结果", kv_pillar: "支柱",
|
|
@@ -740,11 +750,11 @@
|
|
|
740
750
|
title: "Content Engine · Observatory",
|
|
741
751
|
brand: "Observatory",
|
|
742
752
|
search: "Search ID, one-liner, source…",
|
|
743
|
-
overview: "Overview",
|
|
744
|
-
notes_in: "Captured", notes_lib: "Catalog",
|
|
753
|
+
overview: "Overview", needs: "Needs", products: "Products", recommendations: "Recommendations", topics: "Topics", notes: "Notes & library", runs: "Run files", pub: "Shipped & review",
|
|
754
|
+
notes_in: "Captured", notes_needs: "Needs", notes_lib: "Catalog",
|
|
745
755
|
lead: "Pick an IP, then inspect the stock.",
|
|
746
|
-
kpi_cap: "Captured", kpi_lib: "Catalog", kpi_topics: "Topics", kpi_out: "Outcomes",
|
|
747
|
-
kpi_cap_sub: "Capture", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
756
|
+
kpi_cap: "Captured", kpi_needs: "Needs", kpi_lib: "Catalog", kpi_topics: "Topics", kpi_out: "Outcomes",
|
|
757
|
+
kpi_cap_sub: "Capture", kpi_needs_sub: "Audience quotes", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
748
758
|
kpi_topics_sub: "{n} ready, never opened as a run", kpi_out_sub: "Runs / shipped URLs",
|
|
749
759
|
ip_section: "IP · Profiles",
|
|
750
760
|
ip_all: "All profiles",
|
|
@@ -755,8 +765,15 @@
|
|
|
755
765
|
gap_section: "Breaks",
|
|
756
766
|
gap_none: "No obvious breaks.",
|
|
757
767
|
gap_ready_n: "{n} ready topics with no run",
|
|
768
|
+
gap_needs_thin_n: "Needs under 3 quotes (graphics hard gate) · have {n}",
|
|
769
|
+
gap_needs_n: "{n} needs not yet in a topic",
|
|
758
770
|
gap_lib_n: "{n} captures not ingested",
|
|
759
771
|
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
|
+
kv_lane: "Lane",
|
|
774
|
+
kv_mode: "Generation mode",
|
|
775
|
+
kv_needs: "Needs",
|
|
776
|
+
kv_recs: "Recommendations",
|
|
760
777
|
kv_profile: "Profile",
|
|
761
778
|
kind_profile: "Profile", kind_account: "Account",
|
|
762
779
|
profile_pillars: "Pillars",
|
|
@@ -768,6 +785,7 @@
|
|
|
768
785
|
doc_voice: "Voice",
|
|
769
786
|
doc_audience: "Audience",
|
|
770
787
|
doc_pillars: "Pillars",
|
|
788
|
+
doc_lanes: "Lanes",
|
|
771
789
|
empty_profile_t: "Pick a Profile",
|
|
772
790
|
empty_profile_b: "The inspector shows that IP’s pillars, accounts, files, and breaks.",
|
|
773
791
|
acct_handle: "Handle",
|
|
@@ -791,6 +809,7 @@
|
|
|
791
809
|
lib_claim_ev: "Proposition evidence is this claim’s Evidence table; hit rate is n/win/loss.",
|
|
792
810
|
f_status: "Registry status", f_pillar: "Pillar", f_src: "Source", f_use: "Use", f_unused: "unused", f_promoted: "promoted",
|
|
793
811
|
f_sort: "Sort",
|
|
812
|
+
f_sort_recommend: "Report first",
|
|
794
813
|
f_sort_bookmarks: "Bookmarks ↓",
|
|
795
814
|
f_sort_replies: "Replies ↓",
|
|
796
815
|
f_sort_likes: "Likes ↓",
|
|
@@ -813,7 +832,8 @@
|
|
|
813
832
|
f_cap_ingest: "Ingest",
|
|
814
833
|
f_cap_ingested: "Ingested",
|
|
815
834
|
f_cap_not_ingested: "Not ingested",
|
|
816
|
-
no_topic: "No matching topics",
|
|
835
|
+
no_topic: "No matching topics", no_note: "No matching notes", no_need: "No audience quotes yet", no_pub: "No shipped posts",
|
|
836
|
+
need_empty_hint: "Ask the Agent: “capture audience quotes: …” with a verbatim quote. Do not invent quotes.",
|
|
817
837
|
no_stage: "no stage files",
|
|
818
838
|
empty_t: "Pick something on the left",
|
|
819
839
|
empty_b: "Where it came from, whether it was written, whether it shipped, whether it is worth reusing — that chain shows up here.",
|
|
@@ -839,7 +859,7 @@
|
|
|
839
859
|
thin_warn: "Too thin to review: result is not win / flat / loss. No chart.",
|
|
840
860
|
topic_bind: "Topic: {t} · swipe {s} · claim {c}",
|
|
841
861
|
supersedes: "supersedes {id}", superseded_by: "superseded by {id}",
|
|
842
|
-
kind_raw: "Raw", kind_capture: "Capture",
|
|
862
|
+
kind_raw: "Raw", kind_capture: "Capture", kind_need: "Need", kind_product: "Product", kind_recommendation: "Recommendation", kind_topic: "Topic", kind_swipe: "Swipe",
|
|
843
863
|
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "Shipped",
|
|
844
864
|
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",
|
|
845
865
|
kv_use: "Use", kv_src: "Source", kv_result: "Result", kv_pillar: "Pillar",
|
|
@@ -936,9 +956,8 @@
|
|
|
936
956
|
let topicPillar = "";
|
|
937
957
|
let topicSrc = "";
|
|
938
958
|
let topicUsage = "";
|
|
939
|
-
let discStatus = "";
|
|
940
|
-
let discSort = "bookmarks";
|
|
941
959
|
let capIngest = "";
|
|
960
|
+
const TOPIC_SOURCES = ["need", "inbox", "user", "product", "recommendation"];
|
|
942
961
|
let selected = null;
|
|
943
962
|
const $list = document.getElementById("list");
|
|
944
963
|
const $insp = document.getElementById("insp");
|
|
@@ -1283,7 +1302,7 @@
|
|
|
1283
1302
|
sel.innerHTML = `<option value="">${t("ip_all")}</option>` +
|
|
1284
1303
|
ids.map(id => `<option value="${id}" ${id === profileScope ? "selected" : ""}>${id}</option>`).join("");
|
|
1285
1304
|
tabs.innerHTML = "";
|
|
1286
|
-
for (const id of ["overview", "
|
|
1305
|
+
for (const id of ["overview", "needs", "products", "recommendations", "topics", "notes", "runs", "pub"]) {
|
|
1287
1306
|
const b = document.createElement("button");
|
|
1288
1307
|
b.textContent = t(id);
|
|
1289
1308
|
b.dataset.view = id;
|
|
@@ -1343,7 +1362,7 @@
|
|
|
1343
1362
|
function qmatch(n) {
|
|
1344
1363
|
const q = $q.value.trim().toLowerCase();
|
|
1345
1364
|
if (!q) return true;
|
|
1346
|
-
const blob = [n.ident, n.label, n.kind, n.status, n.pillar, n.profile, n.profiles, n.src_handle, n.url, n.notes, n.type, n.atom_type, n.topic_id].filter(Boolean).join(" ").toLowerCase();
|
|
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();
|
|
1347
1366
|
return blob.includes(q);
|
|
1348
1367
|
}
|
|
1349
1368
|
function connected(id) {
|
|
@@ -1423,11 +1442,16 @@
|
|
|
1423
1442
|
function heatMetric(n, key) {
|
|
1424
1443
|
return numCompact(n[`src_${key}`]);
|
|
1425
1444
|
}
|
|
1445
|
+
function hasHeat(n) {
|
|
1446
|
+
return ["likes", "replies", "bookmarks", "views"].some(k => heatMetric(n, k) !== null);
|
|
1447
|
+
}
|
|
1426
1448
|
function heatStripHtml(n, opts = {}) {
|
|
1449
|
+
// No strip when every metric is missing — empty "—" chips inflate row height.
|
|
1450
|
+
if (!hasHeat(n)) return "";
|
|
1427
1451
|
const hint = opts.hint ? `<span class="heat-note">${esc(t("heat_hint"))}</span>` : "";
|
|
1428
1452
|
const items = [
|
|
1429
1453
|
{ key: "likes", sym: "♥", title: "likes" },
|
|
1430
|
-
{ key: "replies", sym: "
|
|
1454
|
+
{ key: "replies", sym: "💬", title: "replies" },
|
|
1431
1455
|
{ key: "bookmarks", sym: "🔖", title: "bookmarks" },
|
|
1432
1456
|
{ key: "views", sym: "👁", title: "views" },
|
|
1433
1457
|
];
|
|
@@ -1439,7 +1463,7 @@
|
|
|
1439
1463
|
}).join("");
|
|
1440
1464
|
return `<div class="heat" title="${esc(t("heat_hint"))}">${chips}</div>${hint}`;
|
|
1441
1465
|
}
|
|
1442
|
-
function
|
|
1466
|
+
function listRowEl(n, extra) {
|
|
1443
1467
|
const d = document.createElement("div");
|
|
1444
1468
|
d.className = "row" + (selected === n.id ? " sel" : "");
|
|
1445
1469
|
d.dataset.kind = n.kind;
|
|
@@ -1458,35 +1482,19 @@
|
|
|
1458
1482
|
</div>`;
|
|
1459
1483
|
return d;
|
|
1460
1484
|
}
|
|
1461
|
-
function sortDiscoveries(rows, mode) {
|
|
1462
|
-
const missing = -1;
|
|
1463
|
-
const num = (n, key) => {
|
|
1464
|
-
const v = Number(String(n[`src_${key}`] ?? "").replace(/,/g, ""));
|
|
1465
|
-
return Number.isFinite(v) ? v : missing;
|
|
1466
|
-
};
|
|
1467
|
-
return rows.slice().sort((a, b) => {
|
|
1468
|
-
let cmp = 0;
|
|
1469
|
-
if (mode === "likes") cmp = num(b, "likes") - num(a, "likes");
|
|
1470
|
-
else if (mode === "replies") cmp = num(b, "replies") - num(a, "replies");
|
|
1471
|
-
else if (mode === "views") cmp = num(b, "views") - num(a, "views");
|
|
1472
|
-
else if (mode === "date") cmp = String(b.date || "").localeCompare(String(a.date || ""));
|
|
1473
|
-
else cmp = num(b, "bookmarks") - num(a, "bookmarks");
|
|
1474
|
-
if (cmp !== 0) return cmp;
|
|
1475
|
-
return String(b.date || "").localeCompare(String(a.date || ""));
|
|
1476
|
-
});
|
|
1477
|
-
}
|
|
1478
1485
|
function pills(n) {
|
|
1479
1486
|
const bits = [];
|
|
1480
|
-
if (n.kind === "
|
|
1487
|
+
if (n.kind === "need") {
|
|
1481
1488
|
const st = n.status || "";
|
|
1482
|
-
const cls = st === "
|
|
1483
|
-
: (st === "saved" || st === "rejected" || st === "unread" ? "gap" : "");
|
|
1489
|
+
const cls = st === "promoted" ? "ok" : (st === "captured" ? "gap" : "");
|
|
1484
1490
|
if (st) bits.push(`<span class="pill ${cls}">${esc(st)}</span>`);
|
|
1485
|
-
if (n.
|
|
1486
|
-
if (n.
|
|
1491
|
+
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1492
|
+
if (n.frequency) bits.push(`<span class="pill">×${esc(n.frequency)}</span>`);
|
|
1487
1493
|
}
|
|
1488
1494
|
if (n.kind === "topic") {
|
|
1489
1495
|
bits.push(`<span class="pill">${esc(n.status)}</span>`);
|
|
1496
|
+
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1497
|
+
if (n.generation_mode) bits.push(`<span class="pill">${esc(n.generation_mode)}</span>`);
|
|
1490
1498
|
bits.push(n.usage === "unused" ? `<span class="pill gap">${t("unused")}</span>` : `<span class="pill ok">${esc(n.usage)}</span>`);
|
|
1491
1499
|
}
|
|
1492
1500
|
if (n.kind === "run") {
|
|
@@ -1526,7 +1534,9 @@
|
|
|
1526
1534
|
function render() {
|
|
1527
1535
|
for (const b of tabs.querySelectorAll("button")) b.classList.toggle("on", b.dataset.view === view);
|
|
1528
1536
|
if (view === "overview") renderOverview();
|
|
1529
|
-
if (view === "
|
|
1537
|
+
if (view === "needs") renderNeeds();
|
|
1538
|
+
if (view === "products") renderProducts();
|
|
1539
|
+
if (view === "recommendations") renderRecommendations();
|
|
1530
1540
|
if (view === "topics") renderTopics();
|
|
1531
1541
|
if (view === "notes") renderNotes();
|
|
1532
1542
|
if (view === "runs") renderRuns();
|
|
@@ -1534,6 +1544,69 @@
|
|
|
1534
1544
|
renderInsp();
|
|
1535
1545
|
}
|
|
1536
1546
|
|
|
1547
|
+
function renderNeedsList(into) {
|
|
1548
|
+
const cards = document.createElement("div");
|
|
1549
|
+
cards.className = "cards";
|
|
1550
|
+
const needs = nodesOf("need").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
1551
|
+
needs.forEach(n => {
|
|
1552
|
+
const meta = [
|
|
1553
|
+
n.source_type || "",
|
|
1554
|
+
n.status || "",
|
|
1555
|
+
n.frequency ? `×${n.frequency}` : "",
|
|
1556
|
+
n.topic_ids && n.topic_ids !== "none" ? n.topic_ids : "",
|
|
1557
|
+
].filter(Boolean).join(" · ");
|
|
1558
|
+
cards.appendChild(listRowEl(n, meta));
|
|
1559
|
+
});
|
|
1560
|
+
if (!needs.length) {
|
|
1561
|
+
cards.innerHTML = `<p class='empty'>${t("no_need")}</p><p class="note">${esc(t("need_empty_hint"))}</p>`;
|
|
1562
|
+
}
|
|
1563
|
+
into.appendChild(cards);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function renderNeeds() {
|
|
1567
|
+
$list.innerHTML = "";
|
|
1568
|
+
$list.insertAdjacentHTML("beforeend", `<p class="lead">${esc(t("need_empty_hint"))}</p>`);
|
|
1569
|
+
renderNeedsList($list);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function renderProducts() {
|
|
1573
|
+
$list.innerHTML = "";
|
|
1574
|
+
const products = nodesOf("product").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
1575
|
+
$list.insertAdjacentHTML("beforeend", `<p class="lead">Needs describe demand. Products describe the offer. Topics connect them only when the fit is honest.</p>`);
|
|
1576
|
+
const cards = document.createElement("div");
|
|
1577
|
+
cards.className = "cards";
|
|
1578
|
+
products.forEach(product => {
|
|
1579
|
+
const meta = [
|
|
1580
|
+
product.status,
|
|
1581
|
+
product.audience,
|
|
1582
|
+
product.price,
|
|
1583
|
+
product.effective_from,
|
|
1584
|
+
].filter(Boolean).join(" · ");
|
|
1585
|
+
cards.appendChild(rowEl(product, meta));
|
|
1586
|
+
});
|
|
1587
|
+
if (!products.length) cards.innerHTML = `<p class='empty'>No products in products/_index.md</p>`;
|
|
1588
|
+
$list.appendChild(cards);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function renderRecommendations() {
|
|
1592
|
+
$list.innerHTML = "";
|
|
1593
|
+
const recommendations = nodesOf("recommendation").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
1594
|
+
$list.insertAdjacentHTML("beforeend", `<p class="lead">Recommendations are third-party review subjects, not owned Products.</p>`);
|
|
1595
|
+
const cards = document.createElement("div");
|
|
1596
|
+
cards.className = "cards";
|
|
1597
|
+
recommendations.forEach(item => {
|
|
1598
|
+
const meta = [
|
|
1599
|
+
item.status,
|
|
1600
|
+
item.category,
|
|
1601
|
+
item.relationship,
|
|
1602
|
+
item.last_researched,
|
|
1603
|
+
].filter(Boolean).join(" · ");
|
|
1604
|
+
cards.appendChild(rowEl(item, meta));
|
|
1605
|
+
});
|
|
1606
|
+
if (!recommendations.length) cards.innerHTML = `<p class='empty'>No recommendations in recommendations/_index.md</p>`;
|
|
1607
|
+
$list.appendChild(cards);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1537
1610
|
function renderOverview() {
|
|
1538
1611
|
$list.innerHTML = "";
|
|
1539
1612
|
$list.insertAdjacentHTML("beforeend", `<p class="lead">${t("lead")}</p>`);
|
|
@@ -1597,56 +1670,31 @@
|
|
|
1597
1670
|
onReady(wrap);
|
|
1598
1671
|
}
|
|
1599
1672
|
|
|
1600
|
-
function
|
|
1601
|
-
|
|
1602
|
-
const
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
const status = w.querySelector("#d-st");
|
|
1615
|
-
const sortEl = w.querySelector("#d-sort");
|
|
1616
|
-
const paint = () => {
|
|
1617
|
-
discStatus = status.value;
|
|
1618
|
-
discSort = sortEl.value || "bookmarks";
|
|
1619
|
-
const cards = document.createElement("div");
|
|
1620
|
-
cards.className = "cards";
|
|
1621
|
-
let rows = nodesOf("discovery").filter(qmatch);
|
|
1622
|
-
if (discStatus) rows = rows.filter(x => x.status === discStatus);
|
|
1623
|
-
rows = sortDiscoveries(rows, discSort);
|
|
1624
|
-
rows.forEach(x => {
|
|
1625
|
-
const meta = [
|
|
1626
|
-
x.source_role,
|
|
1627
|
-
x.next_action && x.next_action !== "none" ? x.next_action : "",
|
|
1628
|
-
`@${x.source_handle || "—"}`,
|
|
1629
|
-
].filter(Boolean).join(" · ");
|
|
1630
|
-
cards.appendChild(discoveryRowEl(x, meta));
|
|
1631
|
-
});
|
|
1632
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_discovery")}</p>`;
|
|
1633
|
-
[...$list.querySelectorAll(".cards")].forEach(n => n.remove());
|
|
1634
|
-
$list.appendChild(cards);
|
|
1635
|
-
};
|
|
1636
|
-
status.onchange = paint;
|
|
1637
|
-
sortEl.onchange = paint;
|
|
1638
|
-
paint();
|
|
1639
|
-
}
|
|
1640
|
-
);
|
|
1673
|
+
function topicMeta(x) {
|
|
1674
|
+
const score = x.fit ? `${x.craft}/${x.fit}` : (x.craft || x.total || "—");
|
|
1675
|
+
const bits = [
|
|
1676
|
+
score,
|
|
1677
|
+
formatPillar(x.pillar),
|
|
1678
|
+
x.lane,
|
|
1679
|
+
x.generation_mode,
|
|
1680
|
+
x.source_type,
|
|
1681
|
+
x.need_ids && x.need_ids !== "none" ? x.need_ids : "",
|
|
1682
|
+
x.product_ids && x.product_ids !== "none" ? x.product_ids : "",
|
|
1683
|
+
x.recommendation_ids && x.recommendation_ids !== "none" ? x.recommendation_ids : "",
|
|
1684
|
+
x.capture_id || "",
|
|
1685
|
+
].filter(Boolean);
|
|
1686
|
+
return bits.join(" · ");
|
|
1641
1687
|
}
|
|
1642
1688
|
|
|
1643
1689
|
function renderTopics() {
|
|
1644
1690
|
$list.innerHTML = "";
|
|
1691
|
+
$list.insertAdjacentHTML("beforeend", `<p class="lead">${esc(t("topics_lead"))}</p>`);
|
|
1692
|
+
if (topicSrc && !TOPIC_SOURCES.includes(topicSrc)) topicSrc = "";
|
|
1645
1693
|
const statuses = [...new Set(nodesOf("topic").map(x => x.status).filter(Boolean))];
|
|
1646
1694
|
filterBar(
|
|
1647
1695
|
`<select id="f-st"><option value="">${t("f_status")}</option>${statuses.map(s => `<option value="${s}" ${s === topicStatus ? "selected" : ""}>${esc(s)}</option>`).join("")}</select>
|
|
1648
1696
|
<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>
|
|
1649
|
-
<select id="f-src"><option value="">${t("f_src")}</option>${
|
|
1697
|
+
<select id="f-src"><option value="">${t("f_src")}</option>${TOPIC_SOURCES.map(s => `<option value="${s}" ${s === topicSrc ? "selected" : ""}>${s}</option>`).join("")}</select>
|
|
1650
1698
|
<select id="f-u"><option value="">${t("f_use")}</option>
|
|
1651
1699
|
<option value="unused" ${topicUsage === "unused" ? "selected" : ""}>${t("f_unused")}</option>
|
|
1652
1700
|
<option value="promoted" ${topicUsage === "promoted" ? "selected" : ""}>${t("f_promoted")}</option></select>`,
|
|
@@ -1669,8 +1717,7 @@
|
|
|
1669
1717
|
if (topicSrc) rows = rows.filter(x => x.source_type === topicSrc);
|
|
1670
1718
|
if (topicUsage) rows = rows.filter(x => x.usage === topicUsage);
|
|
1671
1719
|
rows.forEach(x => {
|
|
1672
|
-
|
|
1673
|
-
cards.appendChild(rowEl(x, `${score} · ${formatPillar(x.pillar)} · ${x.source_type} @${x.src_handle || "—"}`));
|
|
1720
|
+
cards.appendChild(rowEl(x, topicMeta(x)));
|
|
1674
1721
|
});
|
|
1675
1722
|
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_topic")}</p>`;
|
|
1676
1723
|
$list.appendChild(cards);
|
|
@@ -1685,7 +1732,7 @@
|
|
|
1685
1732
|
$list.innerHTML = "";
|
|
1686
1733
|
const sub = document.createElement("div");
|
|
1687
1734
|
sub.className = "subtabs";
|
|
1688
|
-
sub.innerHTML = `<button data-t="notes">${t("notes_in")}</button><button data-t="lib">${t("notes_lib")}</button>`;
|
|
1735
|
+
sub.innerHTML = `<button data-t="notes">${t("notes_in")}</button><button data-t="needs">${t("notes_needs")}</button><button data-t="lib">${t("notes_lib")}</button>`;
|
|
1689
1736
|
$list.appendChild(sub);
|
|
1690
1737
|
sub.querySelectorAll("button").forEach(b => b.classList.toggle("on", b.dataset.t === notesTab));
|
|
1691
1738
|
sub.onclick = (e) => {
|
|
@@ -1712,11 +1759,15 @@
|
|
|
1712
1759
|
let caps = nodesOf("capture").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
1713
1760
|
if (capIngest === "ingested") caps = caps.filter(c => c.status === "ingested");
|
|
1714
1761
|
if (capIngest === "not_ingested") caps = caps.filter(c => c.status !== "ingested");
|
|
1715
|
-
caps.forEach(c => cards.appendChild(
|
|
1762
|
+
caps.forEach(c => cards.appendChild(listRowEl(c, `${c.type} · ${c.status} · raw ${c.has_raw ? t("raw_yes") : t("raw_no")}`)));
|
|
1716
1763
|
if (!caps.length) cards.innerHTML = `<p class='empty'>${t("no_note")}</p>`;
|
|
1717
1764
|
$list.appendChild(cards);
|
|
1718
1765
|
return;
|
|
1719
1766
|
}
|
|
1767
|
+
if (notesTab === "needs") {
|
|
1768
|
+
renderNeedsList($list);
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1720
1771
|
const items = GRAPH.nodes.filter(n => n.lib && inScope(n));
|
|
1721
1772
|
const subtypeOf = (n) => n.atom_type || n.form || n.claim_type || "";
|
|
1722
1773
|
const kinds = [...new Set(items.map(n => n.kind))];
|
|
@@ -1907,6 +1958,7 @@
|
|
|
1907
1958
|
function profileCounts(profileId) {
|
|
1908
1959
|
return {
|
|
1909
1960
|
capture: nodesOfAll("capture").filter(n => n.profile === profileId).length,
|
|
1961
|
+
need: nodesOfAll("need").filter(n => n.profile === profileId).length,
|
|
1910
1962
|
topic: nodesOfAll("topic").filter(n => n.profile === profileId).length,
|
|
1911
1963
|
lib: GRAPH.nodes.filter(n => n.lib && (String(n.profiles || "").includes(profileId) || String(n.profiles || "").includes("*"))).length,
|
|
1912
1964
|
run: nodesOfAll("run").filter(n => n.profile === profileId).length,
|
|
@@ -1918,6 +1970,7 @@
|
|
|
1918
1970
|
if (profileId) return profileCounts(profileId);
|
|
1919
1971
|
return {
|
|
1920
1972
|
capture: nodesOfAll("capture").length,
|
|
1973
|
+
need: nodesOfAll("need").length,
|
|
1921
1974
|
topic: nodesOfAll("topic").length,
|
|
1922
1975
|
lib: GRAPH.nodes.filter(n => n.lib).length,
|
|
1923
1976
|
run: nodesOfAll("run").length,
|
|
@@ -1927,18 +1980,26 @@
|
|
|
1927
1980
|
|
|
1928
1981
|
function gapsFor(profileId) {
|
|
1929
1982
|
const topics = profileId ? nodesOfAll("topic").filter(n => n.profile === profileId) : nodesOfAll("topic");
|
|
1983
|
+
const needs = profileId ? nodesOfAll("need").filter(n => n.profile === profileId) : nodesOfAll("need");
|
|
1930
1984
|
const caps = profileId ? nodesOfAll("capture").filter(n => n.profile === profileId) : nodesOfAll("capture");
|
|
1931
1985
|
const pubs = profileId ? nodesOfAll("published").filter(n => n.profile === profileId) : nodesOfAll("published");
|
|
1932
|
-
|
|
1986
|
+
const needCount = needs.length;
|
|
1987
|
+
const gaps = [
|
|
1933
1988
|
[topics.filter(x => x.status === "ready" && x.usage === "unused").length, "gap_ready_n"],
|
|
1989
|
+
[needs.filter(n => n.status === "captured" && (!n.topic_ids || n.topic_ids === "none")).length, "gap_needs_n"],
|
|
1934
1990
|
[caps.filter(c => c.status !== "ingested").length, "gap_lib_n"],
|
|
1935
1991
|
[pubs.filter(p => !p.reviewable).length, "gap_review_n"],
|
|
1936
1992
|
].filter(([n]) => n > 0);
|
|
1993
|
+
if (GRAPH.needs_index_present && needCount < 3) {
|
|
1994
|
+
gaps.splice(1, 0, [needCount, "gap_needs_thin_n"]);
|
|
1995
|
+
}
|
|
1996
|
+
return gaps;
|
|
1937
1997
|
}
|
|
1938
1998
|
|
|
1939
1999
|
function statsHtml(counts) {
|
|
1940
2000
|
return `<div class="prof-stats">
|
|
1941
2001
|
<div><b>${counts.capture}</b><span>${t("kpi_cap")}</span></div>
|
|
2002
|
+
<div><b>${counts.need || 0}</b><span>${t("kpi_needs")}</span></div>
|
|
1942
2003
|
<div><b>${counts.topic}</b><span>${t("kpi_topics")}</span></div>
|
|
1943
2004
|
<div><b>${counts.lib}</b><span>${t("kpi_lib")}</span></div>
|
|
1944
2005
|
<div><b>${counts.run}</b><span>Run</span></div>
|
|
@@ -1972,6 +2033,7 @@
|
|
|
1972
2033
|
["voice", t("doc_voice")],
|
|
1973
2034
|
["audience", t("doc_audience")],
|
|
1974
2035
|
["pillars", t("doc_pillars")],
|
|
2036
|
+
["lanes", t("doc_lanes")],
|
|
1975
2037
|
];
|
|
1976
2038
|
const pillarRows = Object.entries(pillars).map(([id, p]) => {
|
|
1977
2039
|
const title = lang === "zh" ? p.zh : p.en;
|
|
@@ -2035,16 +2097,32 @@
|
|
|
2035
2097
|
? `${n.win || 0}w / ${n.loss || 0}l / n=${n.n}`
|
|
2036
2098
|
: "",
|
|
2037
2099
|
[t("kv_pillar")]: formatPillar(n.pillar),
|
|
2100
|
+
[t("kv_lane")]: n.kind === "topic" ? (n.lane || "") : "",
|
|
2101
|
+
[t("kv_mode")]: n.kind === "topic" ? (n.generation_mode || "") : "",
|
|
2038
2102
|
[t("kv_plat")]: n.platform || n.platforms,
|
|
2039
2103
|
[t("kv_profile")]: n.profile || n.profile_id || (n.profiles || ""),
|
|
2040
2104
|
[t("kv_score")]: n.fit ? `${n.craft} opp / ${n.fit} aud` : n.total,
|
|
2041
|
-
[t("kv_use")]: n.
|
|
2042
|
-
[t("kv_src")]: n.
|
|
2105
|
+
[t("kv_use")]: n.usage,
|
|
2106
|
+
[t("kv_src")]: n.source_type,
|
|
2107
|
+
[t("kv_needs")]: n.kind === "topic" ? (n.need_ids || "") : "",
|
|
2108
|
+
Quote: n.kind === "need" ? (n.quote || "") : "",
|
|
2109
|
+
Speaker: n.kind === "need" ? (n.speaker || "") : "",
|
|
2110
|
+
Frequency: n.kind === "need" ? (n.frequency || "") : "",
|
|
2111
|
+
Topics: n.kind === "need" ? (n.topic_ids || "") : "",
|
|
2112
|
+
Products: n.kind === "topic" ? (n.product_ids || "") : "",
|
|
2113
|
+
"Product fit": n.kind === "topic" ? (n.product_fit || "") : "",
|
|
2114
|
+
[t("kv_recs")]: n.kind === "topic" ? (n.recommendation_ids || "") : "",
|
|
2115
|
+
Audience: n.kind === "product" ? (n.audience || "") : "",
|
|
2116
|
+
Price: n.kind === "product" ? (n.price || "") : "",
|
|
2117
|
+
"Effective from": n.kind === "product" ? (n.effective_from || "") : "",
|
|
2118
|
+
Category: n.kind === "recommendation" ? (n.category || "") : "",
|
|
2119
|
+
Relationship: n.kind === "recommendation" ? (n.relationship || "") : "",
|
|
2120
|
+
"Last researched": n.kind === "recommendation" ? (n.last_researched || "") : "",
|
|
2043
2121
|
Handle: n.source_handle || n.src_handle || "",
|
|
2044
|
-
[t("kv_heat_at")]: n.kind === "
|
|
2122
|
+
[t("kv_heat_at")]: n.kind === "capture" ? (n.engagement_at || "") : "",
|
|
2045
2123
|
[t("kv_result")]: n.result,
|
|
2046
2124
|
}).filter(([, v]) => v);
|
|
2047
|
-
const heatBlock = n.kind === "
|
|
2125
|
+
const heatBlock = n.kind === "capture"
|
|
2048
2126
|
? `${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>` : ""}`
|
|
2049
2127
|
: "";
|
|
2050
2128
|
$insp.innerHTML = `<h2>${t("obj")}</h2>
|