@easysociable/cli 0.1.8 → 0.1.10
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 +30 -9
- package/dist/easysociable.js +234 -98
- package/dist/easysociable.js.map +1 -1
- package/package.json +2 -2
- package/scripts/engine/migrate-split-vault.py +75 -0
- package/skills/ONBOARDING.md +19 -4
- package/skills/easysociable-content/SKILL.md +25 -9
- package/content-engine/scripts/fixture.test.mjs +0 -24
- package/content-engine/scripts/starter.mjs +0 -58
- package/content-engine/scripts/validate.test.mjs +0 -101
- package/content-engine/scripts/workbench.mjs +0 -140
- package/content-engine/starter/CLAUDE.md +0 -908
- package/content-engine/starter/accounts/README.md +0 -9
- package/content-engine/starter/accounts/_index.md +0 -31
- package/content-engine/starter/accounts/overrides/README.md +0 -3
- package/content-engine/starter/engine.json +0 -23
- package/content-engine/starter/exports/README.md +0 -28
- package/content-engine/starter/hits/README.md +0 -5
- package/content-engine/starter/hits/_index.md +0 -30
- package/content-engine/starter/hits/_template.md +0 -46
- package/content-engine/starter/hits/entries/README.md +0 -3
- package/content-engine/starter/inbox/README.md +0 -9
- package/content-engine/starter/inbox/_index.md +0 -24
- package/content-engine/starter/inbox/entries/README.md +0 -3
- package/content-engine/starter/needs/README.md +0 -38
- package/content-engine/starter/needs/_index.md +0 -31
- package/content-engine/starter/needs/_template.md +0 -24
- package/content-engine/starter/needs/entries/README.md +0 -5
- package/content-engine/starter/platforms/README.md +0 -19
- package/content-engine/starter/platforms/ig.md +0 -69
- package/content-engine/starter/platforms/instagram.md +0 -5
- package/content-engine/starter/platforms/linkedin.md +0 -72
- package/content-engine/starter/platforms/threads.md +0 -69
- package/content-engine/starter/platforms/tiktok.md +0 -71
- package/content-engine/starter/platforms/x.md +0 -78
- package/content-engine/starter/platforms/xiaohongshu.md +0 -19
- package/content-engine/starter/products/README.md +0 -38
- package/content-engine/starter/products/_index.md +0 -25
- package/content-engine/starter/products/_template.md +0 -56
- package/content-engine/starter/products/entries/P-001-easysociable.md +0 -150
- package/content-engine/starter/products/entries/P-002-content-engine.md +0 -136
- package/content-engine/starter/profiles/_index.md +0 -16
- package/content-engine/starter/profiles/default/README.md +0 -17
- package/content-engine/starter/profiles/default/audience.md +0 -38
- package/content-engine/starter/profiles/default/boundaries.md +0 -14
- package/content-engine/starter/profiles/default/expression.md +0 -18
- package/content-engine/starter/profiles/default/handles.md +0 -8
- package/content-engine/starter/profiles/default/keywords.md +0 -31
- package/content-engine/starter/profiles/default/lanes.md +0 -12
- package/content-engine/starter/profiles/default/pillars.md +0 -33
- package/content-engine/starter/profiles/default/stories.md +0 -15
- package/content-engine/starter/profiles/default/voice.md +0 -38
- package/content-engine/starter/published/_index.md +0 -20
- package/content-engine/starter/raw/README.md +0 -4
- package/content-engine/starter/raw/_index.md +0 -12
- package/content-engine/starter/recommendations/README.md +0 -21
- package/content-engine/starter/recommendations/_index.md +0 -21
- package/content-engine/starter/recommendations/_template.md +0 -44
- package/content-engine/starter/runs/README.md +0 -22
- package/content-engine/starter/runs/_index.md +0 -41
- package/content-engine/starter/runs/_template/brief.md +0 -76
- package/content-engine/starter/runs/_template/draft.md +0 -11
- package/content-engine/starter/runs/_template/editor.md +0 -18
- package/content-engine/starter/runs/_template/feedback.md +0 -15
- package/content-engine/starter/runs/_template/idea.md +0 -27
- package/content-engine/starter/runs/_template/pack.md +0 -55
- package/content-engine/starter/runs/_template/packet.md +0 -31
- package/content-engine/starter/runs/_template/rubric.md +0 -19
- package/content-engine/starter/swipe/_index.md +0 -27
- package/content-engine/starter/swipe/_template.md +0 -36
- package/content-engine/starter/topics/README.md +0 -21
- package/content-engine/starter/topics/_index.md +0 -51
- package/content-engine/starter/topics/_template-item.md +0 -106
- package/content-engine/starter/topics/items/README.md +0 -4
- package/content-engine/starter/topics/scoring.md +0 -255
- package/content-engine/starter/wiki/README.md +0 -13
- package/content-engine/starter/wiki/atoms/_index.md +0 -23
- package/content-engine/starter/wiki/atoms/_template.md +0 -31
- package/content-engine/starter/wiki/claims/_index.md +0 -26
- package/content-engine/starter/wiki/claims/_template.md +0 -38
- package/content-engine/starter/workbench/README.md +0 -17
- package/content-engine/workbench/__pycache__/build.cpython-310.pyc +0 -0
- package/content-engine/workbench/build.py +0 -1467
- package/content-engine/workbench/serve.py +0 -53
- package/content-engine/workbench/template.html +0 -2683
- /package/{content-engine/scripts → scripts/engine}/contracts.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/export-contracts.mjs +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/draft.md +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/idea.md +0 -0
- /package/{content-engine → scripts/engine}/fixtures/golden-run-en/pack-tiktok.json +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-export.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-pack.mjs +0 -0
- /package/{content-engine/scripts → scripts/engine}/validate-run.mjs +0 -0
|
@@ -1,2683 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<title>Content Engine · 观察台</title>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--ink: #2c2822;
|
|
10
|
-
--bg: #f4ecdc;
|
|
11
|
-
--bg-2: #efe6d2;
|
|
12
|
-
--bg-3: #e5d9c0;
|
|
13
|
-
--paper: #faf6ec;
|
|
14
|
-
--text: #2c2822;
|
|
15
|
-
--muted: #6e675c;
|
|
16
|
-
--faint: #8a8276;
|
|
17
|
-
--line: #d4c6a8;
|
|
18
|
-
--accent: #9c3d32;
|
|
19
|
-
--accent-dim: #7a3028;
|
|
20
|
-
--gap: #a14a38;
|
|
21
|
-
--ok: #4a6750;
|
|
22
|
-
--chip: #e8dcc4;
|
|
23
|
-
--shadow: 0 12px 28px rgba(70, 50, 24, .08);
|
|
24
|
-
--glow: #ead9b8;
|
|
25
|
-
--gap-bg: #f6e6d6;
|
|
26
|
-
--sumi: #2c2822;
|
|
27
|
-
}
|
|
28
|
-
* { box-sizing: border-box; }
|
|
29
|
-
html, body {
|
|
30
|
-
margin: 0; height: 100%;
|
|
31
|
-
background:
|
|
32
|
-
radial-gradient(900px 420px at 12% -8%, var(--glow) 0%, transparent 55%),
|
|
33
|
-
var(--bg);
|
|
34
|
-
color: var(--text);
|
|
35
|
-
font: 14px/1.5 "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", -apple-system, BlinkMacSystemFont, sans-serif;
|
|
36
|
-
}
|
|
37
|
-
body::before {
|
|
38
|
-
content: "";
|
|
39
|
-
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
|
40
|
-
opacity: .14; mix-blend-mode: multiply;
|
|
41
|
-
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
|
42
|
-
}
|
|
43
|
-
body::after {
|
|
44
|
-
content: "";
|
|
45
|
-
position: fixed; inset: 0; pointer-events: none; z-index: 0;
|
|
46
|
-
opacity: .045;
|
|
47
|
-
background: repeating-linear-gradient(
|
|
48
|
-
0deg,
|
|
49
|
-
transparent 0 5px,
|
|
50
|
-
rgba(80, 60, 30, .35) 5px 6px
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
button { font: inherit; }
|
|
54
|
-
a { color: var(--accent); text-decoration: none; }
|
|
55
|
-
a:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
56
|
-
.tools { display: flex; align-items: center; gap: 8px; }
|
|
57
|
-
.tools button, .tools select {
|
|
58
|
-
background: var(--bg-3); border: 1px solid var(--line); color: var(--text);
|
|
59
|
-
border-radius: 999px; padding: 6px 10px; cursor: pointer; font: inherit;
|
|
60
|
-
}
|
|
61
|
-
#profileSel { max-width: 220px; }
|
|
62
|
-
header.app {
|
|
63
|
-
display: grid;
|
|
64
|
-
grid-template-columns: auto 1fr auto auto;
|
|
65
|
-
align-items: center;
|
|
66
|
-
gap: 20px;
|
|
67
|
-
padding: 14px 28px;
|
|
68
|
-
border-bottom: 1px solid var(--line);
|
|
69
|
-
background: color-mix(in srgb, var(--paper) 82%, transparent);
|
|
70
|
-
backdrop-filter: blur(16px);
|
|
71
|
-
position: sticky; top: 0; z-index: 5;
|
|
72
|
-
}
|
|
73
|
-
.brand { display: flex; align-items: center; gap: 12px; min-width: 168px; }
|
|
74
|
-
.mark {
|
|
75
|
-
width: 28px; height: 28px; border-radius: 8px;
|
|
76
|
-
background: linear-gradient(145deg, #3a342c, #1e1b17);
|
|
77
|
-
box-shadow: inset 0 1px 0 rgba(255,255,255,.12);
|
|
78
|
-
position: relative;
|
|
79
|
-
}
|
|
80
|
-
.mark::after {
|
|
81
|
-
content: ""; position: absolute; inset: 8px 7px 8px 13px;
|
|
82
|
-
border-right: 1.5px solid var(--paper); border-bottom: 1.5px solid var(--paper);
|
|
83
|
-
opacity: .55;
|
|
84
|
-
}
|
|
85
|
-
header.app h1 { font-size: 15px; font-weight: 600; margin: 0; letter-spacing: .04em; }
|
|
86
|
-
header.app h1 small { display: block; color: var(--faint); font-weight: 400; font-size: 11px; letter-spacing: .16em; text-transform: uppercase; margin-top: 2px; }
|
|
87
|
-
nav.tabs { display: flex; gap: 2px; flex-wrap: wrap; }
|
|
88
|
-
nav.tabs button, .subtabs button {
|
|
89
|
-
background: transparent; border: 0; color: var(--muted);
|
|
90
|
-
padding: 8px 12px; border-radius: 999px; cursor: pointer;
|
|
91
|
-
}
|
|
92
|
-
nav.tabs button.on, .subtabs button.on {
|
|
93
|
-
background: var(--sumi); color: var(--paper); font-weight: 600;
|
|
94
|
-
}
|
|
95
|
-
nav.tabs button:not(.on):hover { color: var(--text); background: var(--bg-3); }
|
|
96
|
-
.search {
|
|
97
|
-
position: relative;
|
|
98
|
-
}
|
|
99
|
-
.search::before {
|
|
100
|
-
content: ""; position: absolute; left: 12px; top: 11px; width: 12px; height: 12px;
|
|
101
|
-
border: 1.5px solid var(--faint); border-radius: 50%;
|
|
102
|
-
}
|
|
103
|
-
.search::after {
|
|
104
|
-
content: ""; position: absolute; left: 22px; top: 21px; width: 6px; height: 1.5px;
|
|
105
|
-
background: var(--faint); transform: rotate(45deg);
|
|
106
|
-
}
|
|
107
|
-
#q {
|
|
108
|
-
width: 260px; background: var(--paper); border: 1px solid var(--line); color: var(--text);
|
|
109
|
-
padding: 8px 12px 8px 32px; border-radius: 999px; outline: none;
|
|
110
|
-
}
|
|
111
|
-
#q:focus { border-color: var(--accent-dim); }
|
|
112
|
-
.stamp { color: var(--faint); font-size: 11px; letter-spacing: .04em; }
|
|
113
|
-
.shell { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(340px, .8fr); height: calc(100% - 61px); }
|
|
114
|
-
.list { overflow: auto; padding: 28px 32px 56px; }
|
|
115
|
-
.insp {
|
|
116
|
-
overflow: auto; padding: 28px 26px 56px;
|
|
117
|
-
background:
|
|
118
|
-
linear-gradient(180deg, rgba(255,250,240,.5), transparent 140px),
|
|
119
|
-
var(--bg-2);
|
|
120
|
-
border-left: 1px solid var(--line);
|
|
121
|
-
}
|
|
122
|
-
.lead {
|
|
123
|
-
font-family: "Iowan Old Style", "Palatino Linotype", "Songti SC", Georgia, serif;
|
|
124
|
-
font-size: 28px; line-height: 1.25; font-weight: 500; margin: 0 0 8px; max-width: 18em;
|
|
125
|
-
}
|
|
126
|
-
.lede { color: var(--muted); margin: 0 0 22px; max-width: 42em; }
|
|
127
|
-
.filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
|
|
128
|
-
.filters select {
|
|
129
|
-
background: var(--paper); border: 1.5px solid var(--line); color: var(--text);
|
|
130
|
-
padding: 7px 28px 7px 12px; border-radius: 8px; min-width: 140px;
|
|
131
|
-
}
|
|
132
|
-
.filt {
|
|
133
|
-
display: flex; flex-direction: column; gap: 14px;
|
|
134
|
-
margin: 0 0 16px; padding: 14px 16px;
|
|
135
|
-
background: var(--paper); border: 1px solid var(--line); border-radius: 14px;
|
|
136
|
-
}
|
|
137
|
-
.filt-row {
|
|
138
|
-
display: flex; flex-direction: column; align-items: stretch; gap: 6px;
|
|
139
|
-
}
|
|
140
|
-
.filt-lab { font-size: 11px; color: var(--faint); letter-spacing: .08em; }
|
|
141
|
-
.filt .subtabs {
|
|
142
|
-
display: flex; flex-wrap: wrap; margin: 0; width: 100%;
|
|
143
|
-
border-radius: 12px; gap: 2px;
|
|
144
|
-
}
|
|
145
|
-
.filt .subtabs button { white-space: nowrap; }
|
|
146
|
-
.ghost-link {
|
|
147
|
-
background: none; border: 0; padding: 0; color: var(--accent); cursor: pointer;
|
|
148
|
-
font: inherit; text-decoration: underline; text-underline-offset: 3px;
|
|
149
|
-
}
|
|
150
|
-
.cards { display: grid; gap: 10px; }
|
|
151
|
-
.row {
|
|
152
|
-
display: grid; grid-template-columns: 8px 1fr auto; gap: 14px;
|
|
153
|
-
padding: 14px 16px 14px 0; background: var(--paper);
|
|
154
|
-
border: 1px solid var(--line); border-radius: 14px;
|
|
155
|
-
cursor: pointer; text-align: left; box-shadow: 0 1px 0 rgba(255,255,255,.55) inset;
|
|
156
|
-
}
|
|
157
|
-
.row:hover { border-color: #b8a888; transform: translateY(-1px); }
|
|
158
|
-
.row.sel { border-color: var(--accent); box-shadow: var(--shadow); }
|
|
159
|
-
.stripe { border-radius: 14px 0 0 14px; background: var(--accent-dim); }
|
|
160
|
-
.row[data-kind="capture"] .stripe { background: #7d9aa8; }
|
|
161
|
-
.row[data-kind="need"] .stripe { background: #5a9e8f; }
|
|
162
|
-
.row[data-kind="raw"] .stripe { background: #6e7f6a; }
|
|
163
|
-
.row[data-kind="topic"] .stripe { background: var(--accent); }
|
|
164
|
-
.row[data-kind="swipe"] .stripe { background: #c4a06a; }
|
|
165
|
-
.row[data-kind="atom"] .stripe { background: #a78bb0; }
|
|
166
|
-
.row[data-kind="claim"] .stripe { background: #d08a78; }
|
|
167
|
-
.row[data-kind="hit"] .stripe { background: #6b8f9e; }
|
|
168
|
-
.row[data-kind="run"] .stripe { background: #8aa87a; }
|
|
169
|
-
.row[data-kind="profile"] .stripe { background: #8b6b4a; }
|
|
170
|
-
.row[data-kind="account"] .stripe { background: #5c6b7a; }
|
|
171
|
-
.heat {
|
|
172
|
-
display: flex; flex-wrap: wrap; gap: 6px;
|
|
173
|
-
margin-top: 10px; align-items: center;
|
|
174
|
-
}
|
|
175
|
-
.heat-chip {
|
|
176
|
-
display: inline-flex; align-items: center; gap: 5px;
|
|
177
|
-
font-size: 11px; font-variant-numeric: tabular-nums;
|
|
178
|
-
padding: 3px 8px; border-radius: 999px; white-space: nowrap;
|
|
179
|
-
border: 1px solid transparent;
|
|
180
|
-
}
|
|
181
|
-
.heat-chip .sym { font-size: 10px; line-height: 1; opacity: .9; }
|
|
182
|
-
.heat-chip .val { font-weight: 600; letter-spacing: .01em; }
|
|
183
|
-
.heat-chip.likes { color: #a14a38; background: #f3ddd4; }
|
|
184
|
-
.heat-chip.replies { color: #3d4a57; background: #dce3ea; }
|
|
185
|
-
.heat-chip.bookmarks {
|
|
186
|
-
color: #8a6a3a; background: #ead9b8;
|
|
187
|
-
border-color: rgba(138, 106, 58, .28);
|
|
188
|
-
}
|
|
189
|
-
.heat-chip.views { color: #8a8276; background: #ebe4d6; }
|
|
190
|
-
.heat-chip.empty { opacity: .55; font-weight: 400; }
|
|
191
|
-
.heat-note { font-size: 11px; color: var(--faint); margin: 6px 0 0; }
|
|
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
|
-
}
|
|
294
|
-
.row .id {
|
|
295
|
-
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
296
|
-
font-size: 11px; color: var(--accent); letter-spacing: .02em;
|
|
297
|
-
}
|
|
298
|
-
.row .label { font-size: 15px; margin-top: 4px; }
|
|
299
|
-
.row .meta { color: var(--muted); font-size: 12px; margin-top: 6px; }
|
|
300
|
-
.row .side {
|
|
301
|
-
display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
|
|
302
|
-
gap: 10px; min-height: 52px;
|
|
303
|
-
}
|
|
304
|
-
.plat-mark {
|
|
305
|
-
width: 28px; height: 28px; border-radius: 8px;
|
|
306
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
307
|
-
font-size: 11px; font-weight: 700; letter-spacing: -.02em; line-height: 1;
|
|
308
|
-
flex-shrink: 0;
|
|
309
|
-
}
|
|
310
|
-
.pills { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; align-items: flex-start; max-width: 280px; }
|
|
311
|
-
.pill-stack { display: flex; flex-direction: column; gap: 6px; align-items: flex-end; }
|
|
312
|
-
.pill-row { display: inline-flex; flex-wrap: nowrap; gap: 6px; align-items: center; }
|
|
313
|
-
.score {
|
|
314
|
-
font-family: "Iowan Old Style", Georgia, serif;
|
|
315
|
-
font-size: 22px; color: var(--sumi); line-height: 1;
|
|
316
|
-
}
|
|
317
|
-
.pill {
|
|
318
|
-
display: inline-block; font-size: 11px; padding: 3px 8px; border-radius: 999px;
|
|
319
|
-
background: var(--chip); color: var(--muted); white-space: nowrap;
|
|
320
|
-
}
|
|
321
|
-
.pill.gap { color: var(--gap); background: #f3ddd4; }
|
|
322
|
-
.pill.ok { color: var(--ok); background: #dce6d8; }
|
|
323
|
-
.pill.hit-n { color: #3d4a57; background: #dce3ea; }
|
|
324
|
-
.pill.hit-win { color: var(--ok); background: #dce6d8; }
|
|
325
|
-
.pill.hit-loss { color: var(--gap); background: #f3ddd4; }
|
|
326
|
-
.ipgrid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 0; max-width: 42em; }
|
|
327
|
-
.ipcard {
|
|
328
|
-
background: var(--paper); border: 1px solid var(--line); border-radius: 16px;
|
|
329
|
-
padding: 16px 18px; cursor: pointer; text-align: left;
|
|
330
|
-
}
|
|
331
|
-
.ipcard:hover, .ipcard.on { border-color: var(--accent); box-shadow: var(--shadow); }
|
|
332
|
-
.ipcard .hid { font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--accent); }
|
|
333
|
-
.ipcard .ttl {
|
|
334
|
-
font-family: "Iowan Old Style", "Songti SC", Georgia, serif;
|
|
335
|
-
font-size: 17px; line-height: 1.35; margin: 6px 0 0;
|
|
336
|
-
}
|
|
337
|
-
.prof-stats {
|
|
338
|
-
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 0 0 18px;
|
|
339
|
-
}
|
|
340
|
-
.prof-stats > div {
|
|
341
|
-
background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
|
|
342
|
-
padding: 10px 12px;
|
|
343
|
-
}
|
|
344
|
-
.prof-stats b {
|
|
345
|
-
display: block; font-family: "Iowan Old Style", Georgia, serif;
|
|
346
|
-
font-size: 22px; font-weight: 500; line-height: 1.1; margin-bottom: 4px;
|
|
347
|
-
}
|
|
348
|
-
.prof-stats span { font-size: 11px; color: var(--faint); }
|
|
349
|
-
.prof-pillars { display: grid; gap: 8px; margin: 0 0 18px; }
|
|
350
|
-
.prof-pill {
|
|
351
|
-
display: grid; grid-template-columns: 36px 1fr; gap: 10px; align-items: baseline;
|
|
352
|
-
padding: 10px 12px; background: var(--paper); border: 1px solid var(--line); border-radius: 12px;
|
|
353
|
-
font-size: 13px;
|
|
354
|
-
}
|
|
355
|
-
.prof-pill .hid { color: var(--accent); font-family: ui-monospace, Menlo, monospace; font-size: 11px; }
|
|
356
|
-
.prof-accts button { margin-top: 0; margin-bottom: 8px; }
|
|
357
|
-
.prof-docs { display: flex; flex-direction: column; gap: 6px; margin: 0 0 12px; }
|
|
358
|
-
.gap-list { display: grid; gap: 8px; margin: 0 0 4px; }
|
|
359
|
-
.gap-list p {
|
|
360
|
-
margin: 0; padding: 10px 12px; border-radius: 12px; font-size: 13px;
|
|
361
|
-
background: #f3ddd4; color: var(--gap); border: 1px solid rgba(161, 74, 56, .18);
|
|
362
|
-
}
|
|
363
|
-
.stage-file-lite {
|
|
364
|
-
display: grid; grid-template-columns: 76px 1fr auto; gap: 10px; align-items: center;
|
|
365
|
-
width: 100%; text-align: left; cursor: pointer;
|
|
366
|
-
font-family: inherit; font-size: 13px; color: var(--text);
|
|
367
|
-
background: var(--paper); border: 1px solid var(--line); border-radius: 10px;
|
|
368
|
-
padding: 10px 12px;
|
|
369
|
-
}
|
|
370
|
-
.stage-file-lite:hover { border-color: var(--accent-dim); background: var(--bg-3); }
|
|
371
|
-
.stage-file-lite .sk { font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--ok); }
|
|
372
|
-
.stage-file-lite .sp {
|
|
373
|
-
font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--faint);
|
|
374
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
375
|
-
}
|
|
376
|
-
.stage-file-lite .sa { font-size: 12px; color: var(--accent); }
|
|
377
|
-
.empty {
|
|
378
|
-
color: var(--faint); padding: 48px 8px;
|
|
379
|
-
text-align: center;
|
|
380
|
-
}
|
|
381
|
-
.empty strong { display: block; color: var(--muted); font-weight: 500; margin-bottom: 6px; }
|
|
382
|
-
.insp h2 {
|
|
383
|
-
font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
|
|
384
|
-
color: var(--faint); font-weight: 600; margin: 22px 0 10px;
|
|
385
|
-
}
|
|
386
|
-
.insp h2:first-child { margin-top: 0; }
|
|
387
|
-
.insp .kicker { font-family: ui-monospace, Menlo, monospace; font-size: 12px; color: var(--accent); margin: 0 0 6px; }
|
|
388
|
-
.insp .title {
|
|
389
|
-
font-family: "Iowan Old Style", "Songti SC", Georgia, serif;
|
|
390
|
-
font-size: 22px; line-height: 1.3; margin: 0 0 16px;
|
|
391
|
-
}
|
|
392
|
-
.insp .title-btn {
|
|
393
|
-
font: inherit; color: inherit; text-align: left; text-decoration: none;
|
|
394
|
-
border-bottom: 1px solid var(--accent-dim);
|
|
395
|
-
}
|
|
396
|
-
.insp .title-btn:hover { color: var(--accent); }
|
|
397
|
-
.kv { display: grid; grid-template-columns: 76px 1fr; gap: 8px 12px; font-size: 13px; margin: 0 0 16px; }
|
|
398
|
-
.kv dt { color: var(--faint); }
|
|
399
|
-
.kv dd { margin: 0; }
|
|
400
|
-
.trace { margin: 4px 0 20px; }
|
|
401
|
-
.tstep {
|
|
402
|
-
display: grid; grid-template-columns: 18px 1fr; gap: 10px;
|
|
403
|
-
position: relative; align-items: stretch;
|
|
404
|
-
}
|
|
405
|
-
.tspine {
|
|
406
|
-
position: relative;
|
|
407
|
-
width: 18px;
|
|
408
|
-
justify-self: stretch;
|
|
409
|
-
}
|
|
410
|
-
.tstep .dot {
|
|
411
|
-
position: relative; z-index: 1;
|
|
412
|
-
display: block; box-sizing: border-box;
|
|
413
|
-
width: 10px; height: 10px; margin: 5px auto 0;
|
|
414
|
-
border-radius: 50%; background: var(--line);
|
|
415
|
-
}
|
|
416
|
-
.tstep .dot.on { background: var(--accent); }
|
|
417
|
-
.tstep .dot.here { background: var(--ok); box-shadow: 0 0 0 4px rgba(156,186,134,.18); }
|
|
418
|
-
.tstep .rail {
|
|
419
|
-
position: absolute;
|
|
420
|
-
left: 50%;
|
|
421
|
-
top: 15px;
|
|
422
|
-
bottom: -5px;
|
|
423
|
-
width: 2px;
|
|
424
|
-
margin-left: -1px;
|
|
425
|
-
background: var(--line);
|
|
426
|
-
border-radius: 1px;
|
|
427
|
-
pointer-events: none;
|
|
428
|
-
}
|
|
429
|
-
.tstep:last-child .rail { display: none; }
|
|
430
|
-
.tcard { padding: 0 0 16px; min-height: 28px; }
|
|
431
|
-
.tcard .note { margin-bottom: 6px; letter-spacing: .12em; text-transform: uppercase; font-size: 10px; }
|
|
432
|
-
.tcard button {
|
|
433
|
-
background: var(--bg-3); border: 1px solid var(--line); color: var(--text);
|
|
434
|
-
border-radius: 10px; padding: 8px 10px; cursor: pointer; text-align: left; width: 100%; margin-top: 6px;
|
|
435
|
-
}
|
|
436
|
-
.tcard button:hover { border-color: var(--accent-dim); }
|
|
437
|
-
.vacant { color: var(--faint); font-size: 12px; font-style: italic; padding: 2px 0 4px; }
|
|
438
|
-
.note { font-size: 12px; color: var(--muted); }
|
|
439
|
-
.warn {
|
|
440
|
-
color: var(--gap); font-size: 12px; background: #f3ddd4; border-radius: 10px; padding: 10px 12px;
|
|
441
|
-
}
|
|
442
|
-
.subtabs { display: inline-flex; background: var(--bg-2); border: 1px solid var(--line); border-radius: 999px; padding: 3px; margin-bottom: 16px; }
|
|
443
|
-
.subtabs button { border-radius: 999px; padding: 6px 14px; }
|
|
444
|
-
footer.hint { margin-top: 28px; color: var(--faint); font-size: 12px; }
|
|
445
|
-
.stages { margin: 8px 0 14px; }
|
|
446
|
-
.stages .stage-lab {
|
|
447
|
-
font-size: 11px; color: var(--faint); letter-spacing: .1em; text-transform: uppercase; margin-bottom: 8px;
|
|
448
|
-
}
|
|
449
|
-
.md-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 4px; }
|
|
450
|
-
.md-actions button {
|
|
451
|
-
font-size: 12px; padding: 8px 12px; border-radius: 999px;
|
|
452
|
-
border: 1px solid var(--accent-dim); background: var(--bg-2); color: var(--sumi); cursor: pointer;
|
|
453
|
-
}
|
|
454
|
-
.md-actions button:hover { background: var(--chip); }
|
|
455
|
-
.serve-banner {
|
|
456
|
-
display: none; position: sticky; top: 0; z-index: 40;
|
|
457
|
-
background: #f3ddd4; color: var(--gap); font-size: 13px;
|
|
458
|
-
padding: 10px 18px; border-bottom: 1px solid var(--line);
|
|
459
|
-
}
|
|
460
|
-
.serve-banner.on { display: block; }
|
|
461
|
-
.serve-banner code {
|
|
462
|
-
font-size: 12px; background: rgba(0,0,0,.06); padding: 2px 6px; border-radius: 4px;
|
|
463
|
-
}
|
|
464
|
-
.md-modal {
|
|
465
|
-
display: none; position: fixed; inset: 0; z-index: 50;
|
|
466
|
-
background: rgba(36, 30, 22, .52);
|
|
467
|
-
backdrop-filter: blur(6px);
|
|
468
|
-
padding: 4vh 3vw;
|
|
469
|
-
align-items: center; justify-content: center;
|
|
470
|
-
}
|
|
471
|
-
.md-modal.on { display: flex; }
|
|
472
|
-
.md-sheet {
|
|
473
|
-
width: min(960px, 100%);
|
|
474
|
-
height: min(820px, 88vh);
|
|
475
|
-
margin: 0;
|
|
476
|
-
background:
|
|
477
|
-
linear-gradient(180deg, rgba(255,252,245,.55), transparent 48px),
|
|
478
|
-
var(--paper);
|
|
479
|
-
border: 1px solid #cbb896;
|
|
480
|
-
border-radius: 18px;
|
|
481
|
-
box-shadow:
|
|
482
|
-
0 1px 0 rgba(255,255,255,.55) inset,
|
|
483
|
-
0 28px 80px rgba(42, 32, 18, .28),
|
|
484
|
-
0 2px 0 rgba(90, 60, 20, .06);
|
|
485
|
-
display: flex; flex-direction: column; overflow: hidden;
|
|
486
|
-
position: relative;
|
|
487
|
-
}
|
|
488
|
-
.md-sheet::before {
|
|
489
|
-
content: "";
|
|
490
|
-
position: absolute; inset: 0; pointer-events: none; z-index: 0;
|
|
491
|
-
opacity: .08; mix-blend-mode: multiply; border-radius: inherit;
|
|
492
|
-
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
|
|
493
|
-
}
|
|
494
|
-
.md-sheet > * { position: relative; z-index: 1; }
|
|
495
|
-
.md-sheet.run-mode { width: min(1120px, 100%); }
|
|
496
|
-
.md-head {
|
|
497
|
-
display: flex; align-items: center; gap: 14px; padding: 16px 20px 14px;
|
|
498
|
-
border-bottom: 1px solid rgba(180, 150, 100, .35);
|
|
499
|
-
background: linear-gradient(180deg, #f3ead6, #ebe1cb);
|
|
500
|
-
flex-shrink: 0;
|
|
501
|
-
}
|
|
502
|
-
.md-head .md-title {
|
|
503
|
-
flex: 1; min-width: 0; font-family: "Iowan Old Style", "Songti SC", Georgia, serif;
|
|
504
|
-
font-size: 19px; margin: 0; letter-spacing: .01em;
|
|
505
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
506
|
-
}
|
|
507
|
-
.md-head .md-sub {
|
|
508
|
-
font-size: 12px; color: var(--muted);
|
|
509
|
-
max-width: 42%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
510
|
-
}
|
|
511
|
-
.md-head button {
|
|
512
|
-
border: 1px solid var(--line); background: var(--paper); border-radius: 999px;
|
|
513
|
-
padding: 7px 13px; cursor: pointer; font-size: 12px; color: var(--text);
|
|
514
|
-
box-shadow: 0 1px 0 rgba(255,255,255,.5) inset;
|
|
515
|
-
}
|
|
516
|
-
.md-head button:hover { border-color: var(--accent-dim); }
|
|
517
|
-
.md-layout {
|
|
518
|
-
display: grid; grid-template-columns: 1fr; flex: 1; min-height: 0;
|
|
519
|
-
}
|
|
520
|
-
.md-sheet.run-mode .md-layout {
|
|
521
|
-
grid-template-columns: 228px 1fr;
|
|
522
|
-
}
|
|
523
|
-
.md-rail {
|
|
524
|
-
display: none; overflow: auto; padding: 18px 14px 24px;
|
|
525
|
-
border-right: 1px solid rgba(180, 150, 100, .35);
|
|
526
|
-
background:
|
|
527
|
-
linear-gradient(90deg, #e8dcc3 0%, #efe4cf 72%, #f3ead8 100%);
|
|
528
|
-
}
|
|
529
|
-
.md-sheet.run-mode .md-rail { display: block; }
|
|
530
|
-
.md-rail .rail-lab {
|
|
531
|
-
font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
|
|
532
|
-
color: var(--faint); padding: 0 6px 14px;
|
|
533
|
-
}
|
|
534
|
-
.md-rail .rail-flow {
|
|
535
|
-
list-style: none; margin: 0; padding: 0;
|
|
536
|
-
}
|
|
537
|
-
.md-rail .rail-flow > li { position: relative; }
|
|
538
|
-
.md-rail button.rail-step {
|
|
539
|
-
display: grid; grid-template-columns: 28px 1fr; gap: 4px 10px;
|
|
540
|
-
width: 100%; text-align: left; cursor: pointer;
|
|
541
|
-
border: 0; background: transparent; font: inherit; color: var(--muted);
|
|
542
|
-
padding: 0 6px 0 2px; border-radius: 12px; min-height: 52px;
|
|
543
|
-
}
|
|
544
|
-
.md-rail button.rail-step:hover .rail-card {
|
|
545
|
-
background: rgba(250, 246, 236, .72);
|
|
546
|
-
border-color: rgba(180, 150, 100, .45);
|
|
547
|
-
}
|
|
548
|
-
.md-rail button.rail-step.on .rail-card {
|
|
549
|
-
background: var(--paper);
|
|
550
|
-
border-color: rgba(156, 61, 50, .35);
|
|
551
|
-
box-shadow: 0 6px 16px rgba(70, 45, 20, .08);
|
|
552
|
-
color: var(--sumi);
|
|
553
|
-
}
|
|
554
|
-
.md-rail button.rail-step.miss { opacity: .42; cursor: default; }
|
|
555
|
-
.md-rail button.rail-step.miss:hover .rail-card { background: transparent; border-color: transparent; }
|
|
556
|
-
.md-rail .rail-spine {
|
|
557
|
-
display: flex; flex-direction: column; align-items: center;
|
|
558
|
-
padding-top: 14px;
|
|
559
|
-
}
|
|
560
|
-
.md-rail .rail-spine .dot {
|
|
561
|
-
width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0;
|
|
562
|
-
background: #cfc0a2;
|
|
563
|
-
border: 2px solid #efe4cf;
|
|
564
|
-
box-shadow: 0 0 0 1px rgba(120, 95, 55, .25);
|
|
565
|
-
z-index: 1;
|
|
566
|
-
}
|
|
567
|
-
.md-rail button.rail-step.has .dot {
|
|
568
|
-
background: var(--ok);
|
|
569
|
-
box-shadow: 0 0 0 1px rgba(74, 103, 80, .35);
|
|
570
|
-
}
|
|
571
|
-
.md-rail button.rail-step.on .dot {
|
|
572
|
-
background: var(--accent);
|
|
573
|
-
box-shadow: 0 0 0 4px rgba(156, 61, 50, .16);
|
|
574
|
-
}
|
|
575
|
-
.md-rail .rail-spine .wire {
|
|
576
|
-
width: 2px; flex: 1; min-height: 28px; margin-top: 4px;
|
|
577
|
-
background: linear-gradient(180deg, #c8b68f, #d9c9a8);
|
|
578
|
-
border-radius: 2px;
|
|
579
|
-
}
|
|
580
|
-
.md-rail .rail-flow > li:last-child .wire { display: none; }
|
|
581
|
-
.md-rail .rail-card {
|
|
582
|
-
margin: 4px 0 8px; padding: 10px 11px 9px;
|
|
583
|
-
border-radius: 11px; border: 1px solid transparent;
|
|
584
|
-
transition: background .15s ease, border-color .15s ease, box-shadow .15s ease;
|
|
585
|
-
}
|
|
586
|
-
.md-rail .rail-card .idx {
|
|
587
|
-
display: inline-block; min-width: 1.6em;
|
|
588
|
-
font-family: ui-monospace, Menlo, monospace;
|
|
589
|
-
font-size: 10px; color: var(--faint); letter-spacing: .04em;
|
|
590
|
-
margin-right: 6px;
|
|
591
|
-
}
|
|
592
|
-
.md-rail button.rail-step.on .idx { color: var(--accent); }
|
|
593
|
-
.md-rail .rail-card .sn {
|
|
594
|
-
font-family: "Iowan Old Style", "Songti SC", Georgia, serif;
|
|
595
|
-
font-size: 14px; line-height: 1.25; color: inherit;
|
|
596
|
-
}
|
|
597
|
-
.md-rail .rail-card .sf {
|
|
598
|
-
display: block; margin-top: 3px;
|
|
599
|
-
font-family: ui-monospace, Menlo, monospace;
|
|
600
|
-
font-size: 10px; color: var(--faint);
|
|
601
|
-
}
|
|
602
|
-
.md-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--paper); }
|
|
603
|
-
.md-toolbar {
|
|
604
|
-
display: flex; align-items: center; gap: 10px; padding: 11px 22px;
|
|
605
|
-
border-bottom: 1px solid rgba(180, 150, 100, .28); flex-shrink: 0;
|
|
606
|
-
background: rgba(244, 236, 220, .55);
|
|
607
|
-
}
|
|
608
|
-
.md-toolbar .md-path {
|
|
609
|
-
flex: 1; min-width: 0;
|
|
610
|
-
font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--faint);
|
|
611
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
612
|
-
}
|
|
613
|
-
.md-body {
|
|
614
|
-
overflow: auto; padding: 26px 30px 40px; flex: 1;
|
|
615
|
-
font-size: 15px; line-height: 1.7; color: var(--text);
|
|
616
|
-
}
|
|
617
|
-
.md-body .acct-kv {
|
|
618
|
-
display: grid; grid-template-columns: 110px 1fr; gap: 10px 14px;
|
|
619
|
-
margin: 0 0 18px; font-size: 14px;
|
|
620
|
-
}
|
|
621
|
-
.md-body .acct-kv dt { color: var(--faint); }
|
|
622
|
-
.md-body .acct-kv dd { margin: 0; }
|
|
623
|
-
.md-body .acct-note {
|
|
624
|
-
margin: 0 0 16px; padding: 12px 14px; border-radius: 12px;
|
|
625
|
-
background: var(--bg-2); border: 1px solid var(--line); color: var(--muted); font-size: 13px;
|
|
626
|
-
}
|
|
627
|
-
.md-body .acct-voice {
|
|
628
|
-
border: 1px solid var(--line); border-radius: 12px; background: var(--bg-2);
|
|
629
|
-
}
|
|
630
|
-
.md-body .acct-voice > summary {
|
|
631
|
-
cursor: pointer; list-style: none; padding: 12px 14px;
|
|
632
|
-
font-size: 13px; color: var(--sumi); display: flex; align-items: center; gap: 8px;
|
|
633
|
-
}
|
|
634
|
-
.md-body .acct-voice > summary::-webkit-details-marker { display: none; }
|
|
635
|
-
.md-body .acct-voice > summary::before {
|
|
636
|
-
content: "▸"; color: var(--accent); font-size: 12px;
|
|
637
|
-
}
|
|
638
|
-
.md-body .acct-voice[open] > summary::before { content: "▾"; }
|
|
639
|
-
.md-body .acct-voice > summary .sp {
|
|
640
|
-
margin-left: auto; font-family: ui-monospace, Menlo, monospace;
|
|
641
|
-
font-size: 11px; color: var(--faint);
|
|
642
|
-
}
|
|
643
|
-
.md-body .acct-voice-body {
|
|
644
|
-
padding: 4px 18px 18px; border-top: 1px solid var(--line);
|
|
645
|
-
background: var(--paper); border-radius: 0 0 12px 12px;
|
|
646
|
-
}
|
|
647
|
-
.md-body h1, .md-body h2, .md-body h3 {
|
|
648
|
-
font-family: "Iowan Old Style", "Songti SC", Georgia, serif;
|
|
649
|
-
line-height: 1.3; margin: 1.2em 0 .5em;
|
|
650
|
-
}
|
|
651
|
-
.md-body h1 { font-size: 1.55em; } .md-body h2 { font-size: 1.28em; } .md-body h3 { font-size: 1.1em; }
|
|
652
|
-
.md-body p, .md-body ul, .md-body ol { margin: 0 0 .85em; }
|
|
653
|
-
.md-body ul, .md-body ol { padding-left: 1.4em; }
|
|
654
|
-
.md-body code {
|
|
655
|
-
font-family: ui-monospace, Menlo, monospace; font-size: .88em;
|
|
656
|
-
background: var(--chip); padding: .1em .35em; border-radius: 4px;
|
|
657
|
-
}
|
|
658
|
-
.md-body pre {
|
|
659
|
-
background: var(--bg-3); border: 1px solid var(--line); border-radius: 10px;
|
|
660
|
-
padding: 12px 14px; overflow: auto; font-size: 13px; line-height: 1.45;
|
|
661
|
-
}
|
|
662
|
-
.md-body pre code { background: none; padding: 0; }
|
|
663
|
-
.md-body blockquote {
|
|
664
|
-
margin: 0 0 .85em; padding: 0 0 0 14px; border-left: 3px solid var(--accent-dim); color: var(--muted);
|
|
665
|
-
}
|
|
666
|
-
.md-body a { color: var(--accent); }
|
|
667
|
-
.md-body table { border-collapse: collapse; width: 100%; font-size: 13px; margin: 0 0 1em; }
|
|
668
|
-
.md-body th, .md-body td { border: 1px solid var(--line); padding: 6px 8px; text-align: left; }
|
|
669
|
-
.md-body th { background: var(--bg-2); }
|
|
670
|
-
@media (max-width: 980px) {
|
|
671
|
-
header.app { grid-template-columns: 1fr 1fr; }
|
|
672
|
-
nav.tabs { grid-column: 1 / -1; }
|
|
673
|
-
.shell { grid-template-columns: 1fr; height: auto; }
|
|
674
|
-
.insp { border-left: 0; border-top: 1px solid var(--line); min-height: 40vh; }
|
|
675
|
-
#q { width: 100%; }
|
|
676
|
-
.md-sheet, .md-sheet.run-mode {
|
|
677
|
-
width: 100%;
|
|
678
|
-
height: min(860px, 92vh);
|
|
679
|
-
}
|
|
680
|
-
.md-sheet.run-mode .md-layout { grid-template-columns: 1fr; }
|
|
681
|
-
.md-sheet.run-mode .md-rail {
|
|
682
|
-
display: block; border-right: 0; border-bottom: 1px solid rgba(180, 150, 100, .35);
|
|
683
|
-
max-height: 34%; overflow: auto;
|
|
684
|
-
}
|
|
685
|
-
.md-rail .rail-spine .wire { min-height: 12px; }
|
|
686
|
-
}
|
|
687
|
-
</style>
|
|
688
|
-
</head>
|
|
689
|
-
<body>
|
|
690
|
-
<div class="serve-banner" id="serveBanner"></div>
|
|
691
|
-
<header class="app">
|
|
692
|
-
<div class="brand">
|
|
693
|
-
<div class="mark" aria-hidden="true"></div>
|
|
694
|
-
<h1>观察台<small>Content Engine</small></h1>
|
|
695
|
-
</div>
|
|
696
|
-
<nav class="tabs" id="tabs"></nav>
|
|
697
|
-
<div class="search"><input id="q" type="search" placeholder="" /></div>
|
|
698
|
-
<div class="tools">
|
|
699
|
-
<select id="profileSel" aria-label="profile"></select>
|
|
700
|
-
<button type="button" id="langBtn">EN</button>
|
|
701
|
-
<span class="stamp" id="stamp"></span>
|
|
702
|
-
</div>
|
|
703
|
-
</header>
|
|
704
|
-
<div class="shell">
|
|
705
|
-
<section class="list" id="list"></section>
|
|
706
|
-
<aside class="insp" id="insp"><div class="empty"><strong>从左边拾起一件东西</strong>它从哪来、有没有被写过、发过没有、值不值得再拿出来——会在这里连成一条。</div></aside>
|
|
707
|
-
</div>
|
|
708
|
-
<div class="md-modal" id="mdModal" role="dialog" aria-modal="true" aria-labelledby="mdTitle">
|
|
709
|
-
<div class="md-sheet" id="mdSheet">
|
|
710
|
-
<div class="md-head">
|
|
711
|
-
<h2 class="md-title" id="mdTitle"></h2>
|
|
712
|
-
<span class="md-sub" id="mdSub"></span>
|
|
713
|
-
<button type="button" id="mdClose">✕</button>
|
|
714
|
-
</div>
|
|
715
|
-
<div class="md-layout">
|
|
716
|
-
<nav class="md-rail" id="mdRail" aria-label="stages"></nav>
|
|
717
|
-
<div class="md-main">
|
|
718
|
-
<div class="md-toolbar">
|
|
719
|
-
<span class="md-path" id="mdPath"></span>
|
|
720
|
-
</div>
|
|
721
|
-
<div class="md-body" id="mdBody"></div>
|
|
722
|
-
</div>
|
|
723
|
-
</div>
|
|
724
|
-
</div>
|
|
725
|
-
</div>
|
|
726
|
-
<script>
|
|
727
|
-
const GRAPH = /*__GRAPH__*/null;
|
|
728
|
-
const TRACE_ORDER = ["raw", "capture", "hit", "need", "product", "recommendation", "topic", "swipe", "atom", "claim", "run", "published"];
|
|
729
|
-
const STR = {
|
|
730
|
-
zh: {
|
|
731
|
-
title: "Content Engine · 观察台",
|
|
732
|
-
brand: "观察台",
|
|
733
|
-
search: "搜索 ID、一句话、来源…",
|
|
734
|
-
overview: "总览", needs: "需求库", hits: "Hits", products: "产品库", recommendations: "推荐库", topics: "选题库", notes: "记录与知识", runs: "Run 档案", pub: "发布与复盘",
|
|
735
|
-
notes_in: "记下的", notes_lib: "进库的",
|
|
736
|
-
lead: "先认 IP,再看库存。",
|
|
737
|
-
kpi_cap: "记下的", kpi_needs: "需求", kpi_hits: "Hits", kpi_lib: "进库的", kpi_topics: "选题", kpi_out: "下场",
|
|
738
|
-
kpi_cap_sub: "Capture", kpi_needs_sub: "Needs 原话", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
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",
|
|
766
|
-
ip_section: "IP · Profiles",
|
|
767
|
-
ip_all: "全部 Profile",
|
|
768
|
-
ip_accounts: "账号 {n}",
|
|
769
|
-
vault_title: "整库",
|
|
770
|
-
vault_kicker: "未选 Profile",
|
|
771
|
-
vault_lede: "点左边一张卡片,看该 IP 的支柱、账号与档案。数字只出现在这一侧。",
|
|
772
|
-
gap_section: "断链",
|
|
773
|
-
gap_none: "没有明显断链。",
|
|
774
|
-
gap_ready_n: "ready 选题未开单 {n}",
|
|
775
|
-
gap_needs_thin_n: "需求原话不足 3 条(出图硬门闩)· 现有 {n}",
|
|
776
|
-
gap_needs_n: "需求原话未选题 {n}",
|
|
777
|
-
gap_lib_n: "Capture 未入库 {n}",
|
|
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: "来源配方",
|
|
786
|
-
kv_profile: "Profile",
|
|
787
|
-
kind_profile: "Profile", kind_account: "账号",
|
|
788
|
-
profile_pillars: "支柱",
|
|
789
|
-
profile_accounts: "账号",
|
|
790
|
-
profile_docs: "档案文件",
|
|
791
|
-
profile_stats: "资产概览",
|
|
792
|
-
profile_voice_path: "Voice 路径",
|
|
793
|
-
doc_readme: "README",
|
|
794
|
-
doc_voice: "Voice",
|
|
795
|
-
doc_audience: "Audience",
|
|
796
|
-
doc_pillars: "Pillars",
|
|
797
|
-
doc_lanes: "Lanes",
|
|
798
|
-
empty_profile_t: "点选一个 Profile",
|
|
799
|
-
empty_profile_b: "右侧会显示该 IP 的支柱、账号、档案与断链。",
|
|
800
|
-
acct_handle: "Handle",
|
|
801
|
-
acct_voice: "Voice",
|
|
802
|
-
acct_platforms: "平台",
|
|
803
|
-
acct_default_plat: "默认平台",
|
|
804
|
-
acct_overrides: "Overrides",
|
|
805
|
-
acct_status: "状态",
|
|
806
|
-
acct_note: "Account 是挂在 Profile 上的发布身份;支柱 / audience / voice 在 Profile 档案里。",
|
|
807
|
-
acct_voice_md: "Voice Markdown",
|
|
808
|
-
gap_ready: "缺口:ready 选题未开单",
|
|
809
|
-
gap_lib: "缺口:Capture 未入库",
|
|
810
|
-
gap_review: "缺口:已发但无法复盘(result ≠ win/flat/loss)",
|
|
811
|
-
gap_unshipped: "无发布行",
|
|
812
|
-
gap_unshipped_box: "有 Run、没有发布行",
|
|
813
|
-
gap_superseded: "未发过 · 已取代",
|
|
814
|
-
gap_killed: "未发过 · 已放弃",
|
|
815
|
-
hint: "外链 src_likes 等是来源帖热度,不计入你的成绩。无趋势图:指标样本不够。",
|
|
816
|
-
none: "无", unused: "未开单", no_lib: "未入库", thin: "数据不足", no_ev: "无本库证据",
|
|
817
|
-
lib_use_hist: "使用史在 published/_index(swipe / atoms 列)。零件文件不建 Evidence 表。",
|
|
818
|
-
lib_claim_ev: "命题证据在本 claim 的 Evidence 表;命中率看 n/win/loss。",
|
|
819
|
-
f_status: "状态(登记)", f_pillar: "Pillar", f_src: "来源", f_use: "使用", f_unused: "未开单", f_promoted: "已开单",
|
|
820
|
-
f_sort: "排序",
|
|
821
|
-
f_sort_recommend: "报题优先",
|
|
822
|
-
f_sort_bookmarks: "Bookmarks ↓",
|
|
823
|
-
f_sort_replies: "Replies ↓",
|
|
824
|
-
f_sort_likes: "Likes ↓",
|
|
825
|
-
f_sort_views: "Views ↓",
|
|
826
|
-
f_sort_date: "日期 ↓",
|
|
827
|
-
f_sort_relevance: "Relevance ↓",
|
|
828
|
-
f_lib_kind: "种类", f_lib_subtype: "子类", f_lib_status: "状态",
|
|
829
|
-
kv_hits: "命中",
|
|
830
|
-
kv_refs: "开单引用",
|
|
831
|
-
kv_heat_at: "热度快照",
|
|
832
|
-
lib_refs: "{n} 次引用",
|
|
833
|
-
lib_n: "n {v}",
|
|
834
|
-
lib_win: "win {v}",
|
|
835
|
-
lib_loss: "loss {v}",
|
|
836
|
-
f_all: "全部",
|
|
837
|
-
f_run_ship: "发布", f_run_shipped: "已发布", f_run_unshipped: "未发布", f_run_plat: "平台",
|
|
838
|
-
f_run_topic: "选题", f_run_no_topic: "未绑选题",
|
|
839
|
-
f_run_topic_n: "{id} · {n}",
|
|
840
|
-
no_run: "无匹配 Run",
|
|
841
|
-
f_cap_ingest: "入库",
|
|
842
|
-
f_cap_ingested: "已入库",
|
|
843
|
-
f_cap_not_ingested: "未入库",
|
|
844
|
-
no_topic: "无匹配选题", no_note: "无匹配记录", no_need: "尚无需求原话", no_pub: "尚无已发帖",
|
|
845
|
-
need_empty_hint: "对 Agent 说「记需求:……」写入第一条读者原话。不要编造。",
|
|
846
|
-
no_stage: "无阶段文件",
|
|
847
|
-
empty_t: "从左边拾起一件东西",
|
|
848
|
-
empty_b: "它从哪来、有没有被写过、发过没有、值不值得再拿出来——会在这里连成一条。",
|
|
849
|
-
obj: "对象", trace: "追溯", vacant: "未绑定",
|
|
850
|
-
read_md: "阅读 Markdown",
|
|
851
|
-
open_run: "打开阶段阅读",
|
|
852
|
-
open_stage: "打开",
|
|
853
|
-
stages_h: "阶段流程",
|
|
854
|
-
stages_hint: "点标题或下方按钮,打开阶段阅读。",
|
|
855
|
-
md_loading: "加载中…",
|
|
856
|
-
md_missing: "文件不存在或无法读取。",
|
|
857
|
-
md_need_serve: "需要本地 HTTP。在 Content Engine 根目录运行:",
|
|
858
|
-
serve_cmd: "python3 workbench/serve.py",
|
|
859
|
-
serve_banner: "请用本地服务打开观察台(file:// 无法读取 Markdown)。运行:",
|
|
860
|
-
readonly: "只读 · 由 indexes 生成 · 正文经 HTTP 即时读取,不改写库",
|
|
861
|
-
no_stage: "无文件",
|
|
862
|
-
src_url: "来源 URL:", src_heat: "来源帖热度,不是你的帖",
|
|
863
|
-
heat_hint: "来源帖热度 · 非我方成绩",
|
|
864
|
-
raw_section: "关联 Raw",
|
|
865
|
-
raw_link: "原始帖子",
|
|
866
|
-
raw_file: "Raw 文件",
|
|
867
|
-
raw_none: "未绑定 Raw / 无原始链接",
|
|
868
|
-
thin_warn: "数据不足:result 不是 win / flat / loss,不画趋势。",
|
|
869
|
-
topic_bind: "选题绑定:{t} · swipe {s} · claim {c}",
|
|
870
|
-
supersedes: "取代 {id}", superseded_by: "被取代 {id}",
|
|
871
|
-
kind_raw: "Raw", kind_capture: "Capture", kind_hit: "Hit", kind_need: "需求", kind_product: "产品", kind_recommendation: "推荐对象", kind_topic: "选题", kind_swipe: "Swipe",
|
|
872
|
-
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "发布",
|
|
873
|
-
kv_kind: "类型", kv_date: "日期", kv_status: "状态", kv_plat: "平台", kv_score: "分数", kv_relevance: "关联度", kv_ownership: "归属度", kv_product_relevance: "产品关联",
|
|
874
|
-
kv_use: "使用", kv_src: "来源", kv_result: "结果", kv_pillar: "支柱",
|
|
875
|
-
pub_lead: "这一页只列已经写进 published/_index 的 URL。Run 状态 ≠ 已发帖。",
|
|
876
|
-
pub_lede: "按 Profile 作用域过滤;没有 URL 的 Run 不会出现在这里。",
|
|
877
|
-
raw_yes: "有", raw_no: "无",
|
|
878
|
-
},
|
|
879
|
-
en: {
|
|
880
|
-
title: "Content Engine · Observatory",
|
|
881
|
-
brand: "Observatory",
|
|
882
|
-
search: "Search ID, one-liner, source…",
|
|
883
|
-
overview: "Overview", needs: "Needs", hits: "Hits", products: "Products", recommendations: "Recommendations", topics: "Topics", notes: "Notes & library", runs: "Run files", pub: "Shipped & review",
|
|
884
|
-
notes_in: "Captured", notes_lib: "Catalog",
|
|
885
|
-
lead: "Pick an IP, then inspect the stock.",
|
|
886
|
-
kpi_cap: "Captured", kpi_needs: "Needs", kpi_hits: "Hits", kpi_lib: "Catalog", kpi_topics: "Topics", kpi_out: "Outcomes",
|
|
887
|
-
kpi_cap_sub: "Capture", kpi_needs_sub: "Audience quotes", kpi_lib_sub: "Swipe / Atom / Claim",
|
|
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",
|
|
915
|
-
ip_section: "IP · Profiles",
|
|
916
|
-
ip_all: "All profiles",
|
|
917
|
-
ip_accounts: "{n} account(s)",
|
|
918
|
-
vault_title: "Whole vault",
|
|
919
|
-
vault_kicker: "No Profile selected",
|
|
920
|
-
vault_lede: "Click a card on the left for that IP’s pillars, accounts, and files. Numbers live only on this side.",
|
|
921
|
-
gap_section: "Breaks",
|
|
922
|
-
gap_none: "No obvious breaks.",
|
|
923
|
-
gap_ready_n: "{n} ready topics with no run",
|
|
924
|
-
gap_needs_thin_n: "Needs under 3 quotes (graphics hard gate) · have {n}",
|
|
925
|
-
gap_needs_n: "{n} needs not yet in a topic",
|
|
926
|
-
gap_lib_n: "{n} captures not ingested",
|
|
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",
|
|
935
|
-
kv_profile: "Profile",
|
|
936
|
-
kind_profile: "Profile", kind_account: "Account",
|
|
937
|
-
profile_pillars: "Pillars",
|
|
938
|
-
profile_accounts: "Accounts",
|
|
939
|
-
profile_docs: "Profile files",
|
|
940
|
-
profile_stats: "Asset snapshot",
|
|
941
|
-
profile_voice_path: "Voice path",
|
|
942
|
-
doc_readme: "README",
|
|
943
|
-
doc_voice: "Voice",
|
|
944
|
-
doc_audience: "Audience",
|
|
945
|
-
doc_pillars: "Pillars",
|
|
946
|
-
doc_lanes: "Lanes",
|
|
947
|
-
empty_profile_t: "Pick a Profile",
|
|
948
|
-
empty_profile_b: "The inspector shows that IP’s pillars, accounts, files, and breaks.",
|
|
949
|
-
acct_handle: "Handle",
|
|
950
|
-
acct_voice: "Voice",
|
|
951
|
-
acct_platforms: "Platforms",
|
|
952
|
-
acct_default_plat: "Default platform",
|
|
953
|
-
acct_overrides: "Overrides",
|
|
954
|
-
acct_status: "Status",
|
|
955
|
-
acct_note: "An Account is a publish identity on a Profile; pillars / audience / voice live in the Profile files.",
|
|
956
|
-
acct_voice_md: "Voice Markdown",
|
|
957
|
-
gap_ready: "Gap: ready topics with no run",
|
|
958
|
-
gap_lib: "Gap: Capture not ingested",
|
|
959
|
-
gap_review: "Gap: shipped but not reviewable (result ≠ win/flat/loss)",
|
|
960
|
-
gap_unshipped: "no ship row",
|
|
961
|
-
gap_unshipped_box: "Runs with no published row",
|
|
962
|
-
gap_superseded: "never shipped · superseded",
|
|
963
|
-
gap_killed: "never shipped · killed",
|
|
964
|
-
hint: "src_likes etc. are source-post heat, not your results. No trend charts: the sample is too thin.",
|
|
965
|
-
none: "None", unused: "unused", no_lib: "not ingested", thin: "too thin", no_ev: "no in-vault evidence",
|
|
966
|
-
lib_use_hist: "Use history is published/_index (swipe / atoms columns). No Evidence table in part files.",
|
|
967
|
-
lib_claim_ev: "Proposition evidence is this claim’s Evidence table; hit rate is n/win/loss.",
|
|
968
|
-
f_status: "Registry status", f_pillar: "Pillar", f_src: "Source", f_use: "Use", f_unused: "unused", f_promoted: "promoted",
|
|
969
|
-
f_sort: "Sort",
|
|
970
|
-
f_sort_recommend: "Report first",
|
|
971
|
-
f_sort_bookmarks: "Bookmarks ↓",
|
|
972
|
-
f_sort_replies: "Replies ↓",
|
|
973
|
-
f_sort_likes: "Likes ↓",
|
|
974
|
-
f_sort_views: "Views ↓",
|
|
975
|
-
f_sort_date: "Date ↓",
|
|
976
|
-
f_sort_relevance: "Relevance ↓",
|
|
977
|
-
f_lib_kind: "Kind", f_lib_subtype: "Subtype", f_lib_status: "Status",
|
|
978
|
-
kv_hits: "Hits",
|
|
979
|
-
kv_refs: "Run uses",
|
|
980
|
-
kv_heat_at: "Heat snapshot",
|
|
981
|
-
lib_refs: "{n} cited",
|
|
982
|
-
lib_n: "n {v}",
|
|
983
|
-
lib_win: "win {v}",
|
|
984
|
-
lib_loss: "loss {v}",
|
|
985
|
-
f_all: "All",
|
|
986
|
-
f_run_ship: "Shipped", f_run_shipped: "Shipped", f_run_unshipped: "Unshipped", f_run_plat: "Platform",
|
|
987
|
-
f_run_topic: "Topic", f_run_no_topic: "No topic",
|
|
988
|
-
f_run_topic_n: "{id} · {n}",
|
|
989
|
-
no_run: "No matching runs",
|
|
990
|
-
f_cap_ingest: "Ingest",
|
|
991
|
-
f_cap_ingested: "Ingested",
|
|
992
|
-
f_cap_not_ingested: "Not ingested",
|
|
993
|
-
no_topic: "No matching topics", no_note: "No matching notes", no_need: "No audience quotes yet", no_pub: "No shipped posts",
|
|
994
|
-
need_empty_hint: "Ask the Agent: “capture audience quotes: …” with a verbatim quote. Do not invent quotes.",
|
|
995
|
-
no_stage: "no stage files",
|
|
996
|
-
empty_t: "Pick something on the left",
|
|
997
|
-
empty_b: "Where it came from, whether it was written, whether it shipped, whether it is worth reusing — that chain shows up here.",
|
|
998
|
-
obj: "Object", trace: "Trace", vacant: "unbound",
|
|
999
|
-
read_md: "Read Markdown",
|
|
1000
|
-
open_run: "Open stages",
|
|
1001
|
-
open_stage: "Open",
|
|
1002
|
-
stages_h: "Stages",
|
|
1003
|
-
stages_hint: "Click the title or the button below to open the stage reader.",
|
|
1004
|
-
md_loading: "Loading…",
|
|
1005
|
-
md_missing: "File missing or unreadable.",
|
|
1006
|
-
md_need_serve: "Needs local HTTP. From the Content Engine root run:",
|
|
1007
|
-
serve_cmd: "python3 workbench/serve.py",
|
|
1008
|
-
serve_banner: "Open the observatory via local serve (file:// cannot fetch Markdown). Run:",
|
|
1009
|
-
readonly: "Read-only · indexes for structure · live Markdown over HTTP",
|
|
1010
|
-
no_stage: "missing",
|
|
1011
|
-
src_url: "Source URL: ", src_heat: "Source-post heat, not your post",
|
|
1012
|
-
heat_hint: "Source-post heat · not our score",
|
|
1013
|
-
raw_section: "Linked Raw",
|
|
1014
|
-
raw_link: "Original post",
|
|
1015
|
-
raw_file: "Raw file",
|
|
1016
|
-
raw_none: "No Raw / no source URL",
|
|
1017
|
-
thin_warn: "Too thin to review: result is not win / flat / loss. No chart.",
|
|
1018
|
-
topic_bind: "Topic: {t} · swipe {s} · claim {c}",
|
|
1019
|
-
supersedes: "supersedes {id}", superseded_by: "superseded by {id}",
|
|
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",
|
|
1021
|
-
kind_atom: "Atom", kind_claim: "Claim", kind_run: "Run", kind_published: "Shipped",
|
|
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",
|
|
1023
|
-
kv_use: "Use", kv_src: "Source", kv_result: "Result", kv_pillar: "Pillar",
|
|
1024
|
-
pub_lead: "This view lists URLs written in published/_index. Run status is not the same as shipped.",
|
|
1025
|
-
pub_lede: "Filtered by Profile scope; runs without a URL do not appear here.",
|
|
1026
|
-
raw_yes: "yes", raw_no: "no",
|
|
1027
|
-
},
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
let lang = localStorage.getItem("wb-lang") || "zh";
|
|
1031
|
-
let profileScope = localStorage.getItem("wb-profile") || "";
|
|
1032
|
-
function t(k, vars) {
|
|
1033
|
-
let s = (STR[lang] && STR[lang][k]) || STR.zh[k] || k;
|
|
1034
|
-
if (vars) Object.entries(vars).forEach(([a, b]) => { s = s.replaceAll("{" + a + "}", b); });
|
|
1035
|
-
return s;
|
|
1036
|
-
}
|
|
1037
|
-
function kindName(k) { return t("kind_" + k); }
|
|
1038
|
-
function profileList() { return GRAPH.profiles || []; }
|
|
1039
|
-
function activePillars() {
|
|
1040
|
-
const by = GRAPH.pillars_by_profile || {};
|
|
1041
|
-
if (profileScope && by[profileScope]) return by[profileScope];
|
|
1042
|
-
const merged = {};
|
|
1043
|
-
Object.values(by).forEach(p => Object.assign(merged, p));
|
|
1044
|
-
return Object.keys(merged).length ? merged : (GRAPH.pillars || {});
|
|
1045
|
-
}
|
|
1046
|
-
function pillarName(id) {
|
|
1047
|
-
const p = activePillars()[id] || (GRAPH.pillars || {})[id];
|
|
1048
|
-
if (!p) return id;
|
|
1049
|
-
return lang === "zh" ? p.zh : p.en;
|
|
1050
|
-
}
|
|
1051
|
-
function formatPillar(cell) {
|
|
1052
|
-
if (!cell) return "";
|
|
1053
|
-
const ids = [...new Set(String(cell).match(/P\d+/g) || [])];
|
|
1054
|
-
if (!ids.length) return cell;
|
|
1055
|
-
const sep = lang === "zh" ? ":" : ": ";
|
|
1056
|
-
return ids.map(id => `${id}${sep}${pillarName(id)}`).join(" · ");
|
|
1057
|
-
}
|
|
1058
|
-
function pillarIds() {
|
|
1059
|
-
return Object.keys(activePillars());
|
|
1060
|
-
}
|
|
1061
|
-
function nodeProfiles(n) {
|
|
1062
|
-
if (!n) return [];
|
|
1063
|
-
if (n.kind === "profile") return [n.ident];
|
|
1064
|
-
if (n.kind === "account") return n.profile_id ? [n.profile_id] : [];
|
|
1065
|
-
if (n.profile) return [n.profile];
|
|
1066
|
-
if (n.profiles) {
|
|
1067
|
-
return String(n.profiles).split(",").map(s => s.trim()).filter(s => s && s !== "*");
|
|
1068
|
-
}
|
|
1069
|
-
if (n.kind === "published" && n.run_id) {
|
|
1070
|
-
const run = GRAPH.nodes.find(x => x.kind === "run" && x.ident === n.run_id);
|
|
1071
|
-
return run && run.profile ? [run.profile] : [];
|
|
1072
|
-
}
|
|
1073
|
-
if (n.kind === "raw" && n.capture_id) {
|
|
1074
|
-
const c = GRAPH.nodes.find(x => x.kind === "capture" && x.ident === n.capture_id);
|
|
1075
|
-
return c && c.profile ? [c.profile] : [];
|
|
1076
|
-
}
|
|
1077
|
-
return [];
|
|
1078
|
-
}
|
|
1079
|
-
function inScope(n) {
|
|
1080
|
-
if (!profileScope) return true;
|
|
1081
|
-
if (n.kind === "profile") return n.ident === profileScope;
|
|
1082
|
-
if (n.kind === "account") return n.profile_id === profileScope;
|
|
1083
|
-
if (n.lib && n.profiles) {
|
|
1084
|
-
const parts = String(n.profiles).split(",").map(s => s.trim());
|
|
1085
|
-
if (parts.includes("*") || parts.includes(profileScope)) return true;
|
|
1086
|
-
return false;
|
|
1087
|
-
}
|
|
1088
|
-
const ps = nodeProfiles(n);
|
|
1089
|
-
if (!ps.length) return true;
|
|
1090
|
-
return ps.includes(profileScope);
|
|
1091
|
-
}
|
|
1092
|
-
function scoped(kind) { return GRAPH.nodes.filter(n => n.kind === kind && inScope(n)); }
|
|
1093
|
-
|
|
1094
|
-
const byId = Object.fromEntries(GRAPH.nodes.map(n => [n.id, n]));
|
|
1095
|
-
const adj = new Map();
|
|
1096
|
-
for (const e of GRAPH.edges) {
|
|
1097
|
-
if (!adj.has(e.from)) adj.set(e.from, []);
|
|
1098
|
-
if (!adj.has(e.to)) adj.set(e.to, []);
|
|
1099
|
-
adj.get(e.from).push(e.to);
|
|
1100
|
-
adj.get(e.to).push(e.from);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
let view = "overview";
|
|
1104
|
-
let notesTab = "notes";
|
|
1105
|
-
let libKind = "";
|
|
1106
|
-
let libSubtype = "";
|
|
1107
|
-
let libStatus = "";
|
|
1108
|
-
let runShip = "";
|
|
1109
|
-
let runPlat = "";
|
|
1110
|
-
let runTopic = "";
|
|
1111
|
-
let pubTopic = "";
|
|
1112
|
-
let pubPlat = "";
|
|
1113
|
-
let topicStatus = "";
|
|
1114
|
-
let topicPillar = "";
|
|
1115
|
-
let topicSrc = "";
|
|
1116
|
-
let topicUsage = "";
|
|
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"];
|
|
1128
|
-
let selected = null;
|
|
1129
|
-
const $list = document.getElementById("list");
|
|
1130
|
-
const $insp = document.getElementById("insp");
|
|
1131
|
-
const $q = document.getElementById("q");
|
|
1132
|
-
const tabs = document.getElementById("tabs");
|
|
1133
|
-
const $modal = document.getElementById("mdModal");
|
|
1134
|
-
const $sheet = document.getElementById("mdSheet");
|
|
1135
|
-
const $mdTitle = document.getElementById("mdTitle");
|
|
1136
|
-
const $mdSub = document.getElementById("mdSub");
|
|
1137
|
-
const $mdPath = document.getElementById("mdPath");
|
|
1138
|
-
const $mdBody = document.getElementById("mdBody");
|
|
1139
|
-
const $mdRail = document.getElementById("mdRail");
|
|
1140
|
-
const STAGE_ORDER = ["idea", "brief", "packet", "draft", "editor", "rubric", "pack", "feedback"];
|
|
1141
|
-
let runModal = null; // { id, stage }
|
|
1142
|
-
let mdFetchToken = 0;
|
|
1143
|
-
document.getElementById("stamp").textContent = GRAPH.generated_at;
|
|
1144
|
-
|
|
1145
|
-
function vaultFetchUrl(rel) {
|
|
1146
|
-
return "../" + String(rel || "").replace(/^\/+/, "");
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
function isHttpServe() {
|
|
1150
|
-
return location.protocol === "http:" || location.protocol === "https:";
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
function refreshServeBanner() {
|
|
1154
|
-
const el = document.getElementById("serveBanner");
|
|
1155
|
-
if (!isHttpServe()) {
|
|
1156
|
-
el.classList.add("on");
|
|
1157
|
-
el.innerHTML = `${t("serve_banner")} <code>${esc(t("serve_cmd"))}</code>`;
|
|
1158
|
-
} else {
|
|
1159
|
-
el.classList.remove("on");
|
|
1160
|
-
el.textContent = "";
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
function closeMd() {
|
|
1165
|
-
$modal.classList.remove("on");
|
|
1166
|
-
$sheet.classList.remove("run-mode");
|
|
1167
|
-
$mdBody.innerHTML = "";
|
|
1168
|
-
$mdRail.innerHTML = "";
|
|
1169
|
-
$mdPath.textContent = "";
|
|
1170
|
-
$mdSub.textContent = "";
|
|
1171
|
-
runModal = null;
|
|
1172
|
-
mdFetchToken += 1;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
async function loadMdIntoBody(rel) {
|
|
1176
|
-
const token = ++mdFetchToken;
|
|
1177
|
-
$mdPath.textContent = rel || "";
|
|
1178
|
-
$mdBody.innerHTML = `<p class="note">${esc(t("md_loading"))}</p>`;
|
|
1179
|
-
if (!rel) {
|
|
1180
|
-
$mdBody.innerHTML = `<p class="md-err">${esc(t("md_missing"))}</p>`;
|
|
1181
|
-
return;
|
|
1182
|
-
}
|
|
1183
|
-
if (!isHttpServe()) {
|
|
1184
|
-
$mdBody.innerHTML = `<p class="md-err">${esc(t("md_need_serve"))}</p><p><code>${esc(t("serve_cmd"))}</code></p>`;
|
|
1185
|
-
return;
|
|
1186
|
-
}
|
|
1187
|
-
try {
|
|
1188
|
-
const res = await fetch(vaultFetchUrl(rel));
|
|
1189
|
-
if (!res.ok) throw new Error(String(res.status));
|
|
1190
|
-
const text = await res.text();
|
|
1191
|
-
if (token !== mdFetchToken) return;
|
|
1192
|
-
$mdBody.innerHTML = renderMarkdown(text);
|
|
1193
|
-
} catch {
|
|
1194
|
-
if (token !== mdFetchToken) return;
|
|
1195
|
-
$mdBody.innerHTML = `<p class="md-err">${esc(t("md_missing"))}</p><p class="note">${esc(rel)}</p>`;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function paintRunRail(n, active) {
|
|
1200
|
-
const files = n.stage_files || {};
|
|
1201
|
-
const steps = STAGE_ORDER.map((k, i) => {
|
|
1202
|
-
const p = files[k];
|
|
1203
|
-
const cls = ["rail-step", p ? "has" : "miss", k === active ? "on" : ""].filter(Boolean).join(" ");
|
|
1204
|
-
const idx = String(i + 1).padStart(2, "0");
|
|
1205
|
-
const fileHint = p ? p.split("/").pop() : t("no_stage");
|
|
1206
|
-
return `<li><button type="button" class="${cls}" data-stage="${esc(k)}" ${p ? "" : "disabled"}>
|
|
1207
|
-
<span class="rail-spine" aria-hidden="true"><span class="dot"></span><span class="wire"></span></span>
|
|
1208
|
-
<span class="rail-card">
|
|
1209
|
-
<span class="sn"><span class="idx">${idx}</span>${esc(k)}</span>
|
|
1210
|
-
<span class="sf">${esc(fileHint)}</span>
|
|
1211
|
-
</span>
|
|
1212
|
-
</button></li>`;
|
|
1213
|
-
}).join("");
|
|
1214
|
-
$mdRail.innerHTML = `<div class="rail-lab">${t("stages_h")}</div><ol class="rail-flow">${steps}</ol>`;
|
|
1215
|
-
$mdRail.querySelectorAll("button[data-stage]:not([disabled])").forEach(btn => {
|
|
1216
|
-
btn.onclick = () => openRun(n, btn.getAttribute("data-stage"));
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
function defaultStage(n) {
|
|
1221
|
-
const files = n.stage_files || {};
|
|
1222
|
-
return STAGE_ORDER.find(k => files[k]) || STAGE_ORDER[0];
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
function openRun(n, stage) {
|
|
1226
|
-
if (!n || n.kind !== "run") return;
|
|
1227
|
-
const files = n.stage_files || {};
|
|
1228
|
-
const st = stage && files[stage] ? stage : defaultStage(n);
|
|
1229
|
-
runModal = { id: n.id, stage: st };
|
|
1230
|
-
$sheet.classList.add("run-mode");
|
|
1231
|
-
$mdTitle.textContent = n.ident;
|
|
1232
|
-
$mdSub.textContent = n.label || "";
|
|
1233
|
-
paintRunRail(n, st);
|
|
1234
|
-
$modal.classList.add("on");
|
|
1235
|
-
loadMdIntoBody(files[st] || "");
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
async function openMd(rel, title) {
|
|
1239
|
-
if (!rel) return;
|
|
1240
|
-
runModal = null;
|
|
1241
|
-
$sheet.classList.remove("run-mode");
|
|
1242
|
-
$mdRail.innerHTML = "";
|
|
1243
|
-
$mdTitle.textContent = title || rel.split("/").pop();
|
|
1244
|
-
$mdSub.textContent = "";
|
|
1245
|
-
$modal.classList.add("on");
|
|
1246
|
-
await loadMdIntoBody(rel);
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function accountVoicePath(n) {
|
|
1250
|
-
const v = String(n.voice || "").trim();
|
|
1251
|
-
if (!v || v === "profile") {
|
|
1252
|
-
return n.profile_id ? `profiles/${n.profile_id}/voice.md` : "";
|
|
1253
|
-
}
|
|
1254
|
-
return v;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function openAccount(n) {
|
|
1258
|
-
if (!n || n.kind !== "account") return;
|
|
1259
|
-
runModal = null;
|
|
1260
|
-
$sheet.classList.remove("run-mode");
|
|
1261
|
-
$mdRail.innerHTML = "";
|
|
1262
|
-
$mdTitle.textContent = n.ident;
|
|
1263
|
-
$mdSub.textContent = t("kind_account");
|
|
1264
|
-
$mdPath.textContent = "";
|
|
1265
|
-
const voicePath = accountVoicePath(n);
|
|
1266
|
-
const rows = [
|
|
1267
|
-
[t("acct_handle"), n.ident],
|
|
1268
|
-
[t("kv_profile"), n.profile_id],
|
|
1269
|
-
[t("acct_status"), n.status],
|
|
1270
|
-
[t("acct_platforms"), n.platforms],
|
|
1271
|
-
[t("acct_default_plat"), n.default_platform],
|
|
1272
|
-
[t("acct_voice"), voicePath || n.voice || t("none")],
|
|
1273
|
-
[t("acct_overrides"), n.overrides || t("none")],
|
|
1274
|
-
].filter(([, v]) => v);
|
|
1275
|
-
const voiceFold = voicePath
|
|
1276
|
-
? `<details class="acct-voice" id="acctVoice">
|
|
1277
|
-
<summary>${esc(t("acct_voice_md"))}<span class="sp">${esc(voicePath.split("/").pop())}</span></summary>
|
|
1278
|
-
<div class="acct-voice-body" id="acctVoiceBody"><p class="note">${esc(t("md_loading"))}</p></div>
|
|
1279
|
-
</details>`
|
|
1280
|
-
: "";
|
|
1281
|
-
$mdBody.innerHTML = `<dl class="acct-kv">${rows.map(([k, v]) =>
|
|
1282
|
-
`<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`
|
|
1283
|
-
).join("")}</dl>
|
|
1284
|
-
<p class="acct-note">${esc(t("acct_note"))}</p>
|
|
1285
|
-
${voiceFold}`;
|
|
1286
|
-
$modal.classList.add("on");
|
|
1287
|
-
const fold = $mdBody.querySelector("#acctVoice");
|
|
1288
|
-
const body = $mdBody.querySelector("#acctVoiceBody");
|
|
1289
|
-
if (fold && body) {
|
|
1290
|
-
fold.addEventListener("toggle", async () => {
|
|
1291
|
-
if (!fold.open || body.dataset.loaded === "1") return;
|
|
1292
|
-
if (!isHttpServe()) {
|
|
1293
|
-
body.innerHTML = `<p class="md-err">${esc(t("md_need_serve"))}</p>`;
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
try {
|
|
1297
|
-
const res = await fetch(vaultFetchUrl(voicePath));
|
|
1298
|
-
if (!res.ok) throw new Error(String(res.status));
|
|
1299
|
-
body.innerHTML = renderMarkdown(await res.text());
|
|
1300
|
-
body.dataset.loaded = "1";
|
|
1301
|
-
} catch {
|
|
1302
|
-
body.innerHTML = `<p class="md-err">${esc(t("md_missing"))}</p><p class="note">${esc(voicePath)}</p>`;
|
|
1303
|
-
}
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
function inlineMd(text) {
|
|
1309
|
-
return esc(text)
|
|
1310
|
-
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
1311
|
-
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
1312
|
-
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
function renderMarkdown(src) {
|
|
1316
|
-
const lines = String(src || "").replace(/\r\n/g, "\n").split("\n");
|
|
1317
|
-
const out = [];
|
|
1318
|
-
let i = 0;
|
|
1319
|
-
let para = [];
|
|
1320
|
-
let listType = null;
|
|
1321
|
-
const flushPara = () => {
|
|
1322
|
-
if (!para.length) return;
|
|
1323
|
-
out.push("<p>" + inlineMd(para.join(" ")) + "</p>");
|
|
1324
|
-
para = [];
|
|
1325
|
-
};
|
|
1326
|
-
const flushList = () => {
|
|
1327
|
-
if (!listType) return;
|
|
1328
|
-
out.push(listType === "ul" ? "</ul>" : "</ol>");
|
|
1329
|
-
listType = null;
|
|
1330
|
-
};
|
|
1331
|
-
while (i < lines.length) {
|
|
1332
|
-
const line = lines[i];
|
|
1333
|
-
if (line.startsWith("```")) {
|
|
1334
|
-
flushPara(); flushList();
|
|
1335
|
-
const buf = [];
|
|
1336
|
-
i += 1;
|
|
1337
|
-
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
1338
|
-
buf.push(lines[i]);
|
|
1339
|
-
i += 1;
|
|
1340
|
-
}
|
|
1341
|
-
out.push("<pre><code>" + esc(buf.join("\n")) + "</code></pre>");
|
|
1342
|
-
i += 1;
|
|
1343
|
-
continue;
|
|
1344
|
-
}
|
|
1345
|
-
if (/^\|.+\|$/.test(line) && i + 1 < lines.length && /^\|[\s:\-|]+\|$/.test(lines[i + 1])) {
|
|
1346
|
-
flushPara(); flushList();
|
|
1347
|
-
const rows = [];
|
|
1348
|
-
while (i < lines.length && /^\|.+\|$/.test(lines[i])) {
|
|
1349
|
-
if (!/^\|[\s:\-|]+\|$/.test(lines[i])) rows.push(lines[i]);
|
|
1350
|
-
i += 1;
|
|
1351
|
-
}
|
|
1352
|
-
const cells = (row) => row.replace(/^\||\|$/g, "").split("|").map(c => c.trim());
|
|
1353
|
-
const head = cells(rows[0] || "");
|
|
1354
|
-
const body = rows.slice(1);
|
|
1355
|
-
out.push("<table><thead><tr>" + head.map(c => "<th>" + inlineMd(c) + "</th>").join("") + "</tr></thead>");
|
|
1356
|
-
out.push("<tbody>" + body.map(r => "<tr>" + cells(r).map(c => "<td>" + inlineMd(c) + "</td>").join("") + "</tr>").join("") + "</tbody></table>");
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
if (!line.trim()) {
|
|
1360
|
-
flushPara(); flushList();
|
|
1361
|
-
i += 1;
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
const hm = /^(#{1,3})\s+(.+)$/.exec(line);
|
|
1365
|
-
if (hm) {
|
|
1366
|
-
flushPara(); flushList();
|
|
1367
|
-
const lvl = hm[1].length;
|
|
1368
|
-
out.push(`<h${lvl}>` + inlineMd(hm[2]) + `</h${lvl}>`);
|
|
1369
|
-
i += 1;
|
|
1370
|
-
continue;
|
|
1371
|
-
}
|
|
1372
|
-
if (/^>\s?/.test(line)) {
|
|
1373
|
-
flushPara(); flushList();
|
|
1374
|
-
out.push("<blockquote><p>" + inlineMd(line.replace(/^>\s?/, "")) + "</p></blockquote>");
|
|
1375
|
-
i += 1;
|
|
1376
|
-
continue;
|
|
1377
|
-
}
|
|
1378
|
-
const ul = /^[-*]\s+(.+)$/.exec(line);
|
|
1379
|
-
const ol = /^\d+\.\s+(.+)$/.exec(line);
|
|
1380
|
-
if (ul || ol) {
|
|
1381
|
-
flushPara();
|
|
1382
|
-
const kind = ul ? "ul" : "ol";
|
|
1383
|
-
if (listType !== kind) {
|
|
1384
|
-
flushList();
|
|
1385
|
-
listType = kind;
|
|
1386
|
-
out.push(kind === "ul" ? "<ul>" : "<ol>");
|
|
1387
|
-
}
|
|
1388
|
-
out.push("<li>" + inlineMd((ul || ol)[1]) + "</li>");
|
|
1389
|
-
i += 1;
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
|
-
flushList();
|
|
1393
|
-
para.push(line);
|
|
1394
|
-
i += 1;
|
|
1395
|
-
}
|
|
1396
|
-
flushPara();
|
|
1397
|
-
flushList();
|
|
1398
|
-
return out.join("\n") || "<p></p>";
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
function mdFileForNode(n) {
|
|
1402
|
-
if (!n) return "";
|
|
1403
|
-
if (n.kind === "run") return "";
|
|
1404
|
-
if (n.open_file) return n.open_file;
|
|
1405
|
-
if (n.path && String(n.path).endsWith(".md")) return n.path;
|
|
1406
|
-
return "";
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
function runMetaHtml(n) {
|
|
1410
|
-
const files = n.stage_files || {};
|
|
1411
|
-
const have = STAGE_ORDER.filter(k => files[k]).join(" · ") || t("no_stage");
|
|
1412
|
-
const sel = n.selection || {};
|
|
1413
|
-
return `<p class="note">${t("stages_h")}:${esc(have)}</p>
|
|
1414
|
-
<p class="note">${t("topic_bind", { t: n.topic_id || t("none"), s: sel.swipe, c: sel.claim })}</p>
|
|
1415
|
-
${n.supersedes ? `<p class="note">${t("supersedes", { id: n.supersedes })}</p>` : ""}
|
|
1416
|
-
${n.superseded_by ? `<p class="note">${t("superseded_by", { id: n.superseded_by })}</p>` : ""}`;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
function mdActionsHtml(n) {
|
|
1420
|
-
if (n.kind === "run") {
|
|
1421
|
-
return `<div class="md-actions"><button type="button" data-run-open="${esc(n.id)}">${t("open_run")}</button></div>
|
|
1422
|
-
<p class="note">${t("stages_hint")}</p>`;
|
|
1423
|
-
}
|
|
1424
|
-
const path = mdFileForNode(n);
|
|
1425
|
-
if (!path) return "";
|
|
1426
|
-
return `<div class="md-actions"><button type="button" data-md="${esc(path)}" data-md-title="${esc(n.ident)}">${t("read_md")}</button></div>`;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
document.addEventListener("click", (e) => {
|
|
1430
|
-
const runBtn = e.target.closest("[data-run-open]");
|
|
1431
|
-
if (runBtn && !runBtn.closest("#mdModal")) {
|
|
1432
|
-
e.preventDefault();
|
|
1433
|
-
e.stopPropagation();
|
|
1434
|
-
const node = byId[runBtn.getAttribute("data-run-open")];
|
|
1435
|
-
if (node) openRun(node);
|
|
1436
|
-
return;
|
|
1437
|
-
}
|
|
1438
|
-
const acctBtn = e.target.closest("[data-account-open]");
|
|
1439
|
-
if (acctBtn && !acctBtn.closest("#mdModal")) {
|
|
1440
|
-
e.preventDefault();
|
|
1441
|
-
e.stopPropagation();
|
|
1442
|
-
const node = byId[acctBtn.getAttribute("data-account-open")];
|
|
1443
|
-
if (node) openAccount(node);
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
const el = e.target.closest("[data-md]");
|
|
1447
|
-
if (!el || el.closest("#mdModal")) return;
|
|
1448
|
-
const rel = el.getAttribute("data-md");
|
|
1449
|
-
if (!rel) return;
|
|
1450
|
-
e.preventDefault();
|
|
1451
|
-
e.stopPropagation();
|
|
1452
|
-
openMd(rel, el.getAttribute("data-md-title") || "");
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
document.getElementById("mdClose").onclick = closeMd;
|
|
1456
|
-
$modal.addEventListener("click", (e) => { if (e.target === $modal) closeMd(); });
|
|
1457
|
-
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeMd(); });
|
|
1458
|
-
|
|
1459
|
-
function applyChrome() {
|
|
1460
|
-
document.documentElement.lang = lang === "zh" ? "zh-CN" : "en";
|
|
1461
|
-
document.title = t("title");
|
|
1462
|
-
document.querySelector("header h1").innerHTML = `${t("brand")}<small>Content Engine</small>`;
|
|
1463
|
-
$q.placeholder = t("search");
|
|
1464
|
-
document.getElementById("langBtn").textContent = lang === "zh" ? "EN" : "中文";
|
|
1465
|
-
refreshServeBanner();
|
|
1466
|
-
const sel = document.getElementById("profileSel");
|
|
1467
|
-
const ids = profileList().map(p => p.id);
|
|
1468
|
-
if (profileScope && !ids.includes(profileScope)) profileScope = "";
|
|
1469
|
-
sel.innerHTML = `<option value="">${t("ip_all")}</option>` +
|
|
1470
|
-
ids.map(id => `<option value="${id}" ${id === profileScope ? "selected" : ""}>${id}</option>`).join("");
|
|
1471
|
-
tabs.innerHTML = "";
|
|
1472
|
-
for (const id of ["overview", "hits", "needs", "products", "recommendations", "topics", "notes", "runs", "pub"]) {
|
|
1473
|
-
const b = document.createElement("button");
|
|
1474
|
-
b.textContent = t(id);
|
|
1475
|
-
b.dataset.view = id;
|
|
1476
|
-
b.onclick = () => { view = id; render(); };
|
|
1477
|
-
tabs.appendChild(b);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
document.getElementById("langBtn").onclick = () => {
|
|
1481
|
-
lang = lang === "zh" ? "en" : "zh";
|
|
1482
|
-
localStorage.setItem("wb-lang", lang);
|
|
1483
|
-
applyChrome();
|
|
1484
|
-
render();
|
|
1485
|
-
};
|
|
1486
|
-
document.getElementById("profileSel").onchange = (e) => {
|
|
1487
|
-
profileScope = e.target.value;
|
|
1488
|
-
localStorage.setItem("wb-profile", profileScope);
|
|
1489
|
-
if (profileScope) selected = `profile:${profileScope}`;
|
|
1490
|
-
else if (selected && byId[selected]?.kind === "profile") selected = null;
|
|
1491
|
-
applyChrome();
|
|
1492
|
-
render();
|
|
1493
|
-
};
|
|
1494
|
-
|
|
1495
|
-
$q.addEventListener("input", () => render());
|
|
1496
|
-
|
|
1497
|
-
function nodesOf(kind) { return GRAPH.nodes.filter(n => n.kind === kind && inScope(n)); }
|
|
1498
|
-
function nodesOfAll(kind) { return GRAPH.nodes.filter(n => n.kind === kind); }
|
|
1499
|
-
function runIsShipped(r) {
|
|
1500
|
-
if ((r.ship_urls || []).length) return true;
|
|
1501
|
-
return nodesOf("published").some(p =>
|
|
1502
|
-
(p.run_id && p.run_id === r.ident) ||
|
|
1503
|
-
(r.folder && p.run_folder && p.run_folder === r.folder)
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
function runClosedWithoutShip(r) {
|
|
1507
|
-
return r.status === "superseded" || r.status === "killed";
|
|
1508
|
-
}
|
|
1509
|
-
function unshippedRuns() {
|
|
1510
|
-
return nodesOf("run").filter(r => !runIsShipped(r) && !runClosedWithoutShip(r));
|
|
1511
|
-
}
|
|
1512
|
-
function citesPart(p, n) {
|
|
1513
|
-
const id = n.ident;
|
|
1514
|
-
const cell = n.kind === "swipe" ? p.swipe : n.kind === "atom" ? p.atoms : n.kind === "claim" ? p.claim : "";
|
|
1515
|
-
const v = String(cell || "").trim();
|
|
1516
|
-
if (!v || v.toLowerCase() === "none") return false;
|
|
1517
|
-
return v.split(/[,;]/).some(tok => (tok.split(":").pop() || "").trim() === id);
|
|
1518
|
-
}
|
|
1519
|
-
function publishedFor(n, ctx) {
|
|
1520
|
-
if (n.kind === "published") return [n];
|
|
1521
|
-
if (n.kind === "swipe" || n.kind === "atom" || n.kind === "claim") {
|
|
1522
|
-
return nodesOf("published").filter(p => citesPart(p, n));
|
|
1523
|
-
}
|
|
1524
|
-
const runs = n.kind === "run" ? [n] : ctx.filter(x => x.kind === "run");
|
|
1525
|
-
return nodesOf("published").filter(p => runs.some(r =>
|
|
1526
|
-
(p.run_id && p.run_id === r.ident) || (r.folder && p.run_folder === r.folder)
|
|
1527
|
-
));
|
|
1528
|
-
}
|
|
1529
|
-
function qmatch(n) {
|
|
1530
|
-
const q = $q.value.trim().toLowerCase();
|
|
1531
|
-
if (!q) return true;
|
|
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();
|
|
1533
|
-
return blob.includes(q);
|
|
1534
|
-
}
|
|
1535
|
-
function connected(id) {
|
|
1536
|
-
const seen = new Set([id]);
|
|
1537
|
-
const q = [id];
|
|
1538
|
-
while (q.length) {
|
|
1539
|
-
const cur = q.shift();
|
|
1540
|
-
for (const n of adj.get(cur) || []) {
|
|
1541
|
-
if (!seen.has(n)) { seen.add(n); q.push(n); }
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return [...seen].map(i => byId[i]).filter(Boolean);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function select(id) {
|
|
1548
|
-
const n = byId[id];
|
|
1549
|
-
if (view === "overview" && n && n.kind === "account") {
|
|
1550
|
-
openAccount(n);
|
|
1551
|
-
return;
|
|
1552
|
-
}
|
|
1553
|
-
selected = id;
|
|
1554
|
-
render();
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function rowEl(n, extra) {
|
|
1558
|
-
const d = document.createElement("div");
|
|
1559
|
-
d.className = "row" + (selected === n.id ? " sel" : "");
|
|
1560
|
-
d.dataset.kind = n.kind;
|
|
1561
|
-
d.onclick = () => select(n.id);
|
|
1562
|
-
const score = n.kind === "topic" && (n.craft || n.total)
|
|
1563
|
-
? `<div class="score">${esc(n.fit ? `${n.craft}/${n.fit}` : n.total)}</div>`
|
|
1564
|
-
: "";
|
|
1565
|
-
const clean = n.kind === "run" || n.kind === "published";
|
|
1566
|
-
const meta = clean ? "" : (extra || metaLine(n));
|
|
1567
|
-
d.innerHTML = `<div class="stripe"></div>
|
|
1568
|
-
<div>
|
|
1569
|
-
<div class="id">${esc(n.ident)}</div>
|
|
1570
|
-
<div class="label">${esc(n.label)}</div>
|
|
1571
|
-
${meta ? `<div class="meta">${esc(meta)}</div>` : ""}
|
|
1572
|
-
</div>
|
|
1573
|
-
<div class="side">
|
|
1574
|
-
<div class="pills">${score}${pills(n)}</div>
|
|
1575
|
-
${clean ? platMarkHtml(n) : ""}
|
|
1576
|
-
</div>`;
|
|
1577
|
-
return d;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function runPlatform(n) {
|
|
1581
|
-
return String(n.primary_platform || n.platform || n.platforms || "").split(",")[0].trim().toLowerCase();
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
function platMarkHtml(n) {
|
|
1585
|
-
const id = runPlatform(n);
|
|
1586
|
-
const marks = {
|
|
1587
|
-
x: { t: "𝕏", bg: "#171411", fg: "#f4ecdc" },
|
|
1588
|
-
twitter: { t: "𝕏", bg: "#171411", fg: "#f4ecdc" },
|
|
1589
|
-
tiktok: { t: "Tk", bg: "#111111", fg: "#69c9d0" },
|
|
1590
|
-
instagram: { t: "Ig", bg: "#c13584", fg: "#fff" },
|
|
1591
|
-
linkedin: { t: "in", bg: "#0a66c2", fg: "#fff" },
|
|
1592
|
-
threads: { t: "Th", bg: "#1c1915", fg: "#f4ecdc" },
|
|
1593
|
-
xiaohongshu: { t: "红", bg: "#ff2442", fg: "#fff" },
|
|
1594
|
-
};
|
|
1595
|
-
const m = marks[id] || { t: (id || "?").slice(0, 2), bg: "#6e675c", fg: "#faf6ec" };
|
|
1596
|
-
return `<span class="plat-mark" title="${esc(id || "")}" style="background:${m.bg};color:${m.fg}">${m.t}</span>`;
|
|
1597
|
-
}
|
|
1598
|
-
function metaLine(n) {
|
|
1599
|
-
return [n.date, n.profile, n.status, formatPillar(n.pillar), n.platform || n.platforms, n.type || n.atom_type || n.form].filter(Boolean).join(" · ");
|
|
1600
|
-
}
|
|
1601
|
-
function numCompact(v) {
|
|
1602
|
-
if (v === null || v === undefined || v === "") return null;
|
|
1603
|
-
const n = Number(String(v).replace(/,/g, ""));
|
|
1604
|
-
if (!Number.isFinite(n)) return null;
|
|
1605
|
-
if (n >= 10000) return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}k`;
|
|
1606
|
-
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`.replace(/\.0k$/, "k");
|
|
1607
|
-
return String(Math.round(n));
|
|
1608
|
-
}
|
|
1609
|
-
function heatMetric(n, key) {
|
|
1610
|
-
return numCompact(n[`src_${key}`]);
|
|
1611
|
-
}
|
|
1612
|
-
function hasHeat(n) {
|
|
1613
|
-
return ["likes", "replies", "bookmarks", "views"].some(k => heatMetric(n, k) !== null);
|
|
1614
|
-
}
|
|
1615
|
-
function heatStripHtml(n, opts = {}) {
|
|
1616
|
-
// No strip when every metric is missing — empty "—" chips inflate row height.
|
|
1617
|
-
if (!hasHeat(n)) return "";
|
|
1618
|
-
const hint = opts.hint ? `<span class="heat-note">${esc(t("heat_hint"))}</span>` : "";
|
|
1619
|
-
const items = [
|
|
1620
|
-
{ key: "likes", sym: "♥", title: "likes" },
|
|
1621
|
-
{ key: "replies", sym: "💬", title: "replies" },
|
|
1622
|
-
{ key: "bookmarks", sym: "🔖", title: "bookmarks" },
|
|
1623
|
-
{ key: "views", sym: "👁", title: "views" },
|
|
1624
|
-
];
|
|
1625
|
-
const chips = items.map(({ key, sym, title }) => {
|
|
1626
|
-
const val = heatMetric(n, key);
|
|
1627
|
-
const shown = val === null ? "—" : val;
|
|
1628
|
-
const empty = val === null ? " empty" : "";
|
|
1629
|
-
return `<span class="heat-chip ${key}${empty}" title="${esc(title)}"><span class="sym">${sym}</span><span class="val">${esc(shown)}</span></span>`;
|
|
1630
|
-
}).join("");
|
|
1631
|
-
return `<div class="heat" title="${esc(t("heat_hint"))}">${chips}</div>${hint}`;
|
|
1632
|
-
}
|
|
1633
|
-
function listRowEl(n, extra) {
|
|
1634
|
-
const d = document.createElement("div");
|
|
1635
|
-
d.className = "row" + (selected === n.id ? " sel" : "");
|
|
1636
|
-
d.dataset.kind = n.kind;
|
|
1637
|
-
d.onclick = () => select(n.id);
|
|
1638
|
-
const meta = extra || metaLine(n);
|
|
1639
|
-
d.innerHTML = `<div class="stripe"></div>
|
|
1640
|
-
<div>
|
|
1641
|
-
<div class="id">${esc(n.ident)}</div>
|
|
1642
|
-
<div class="label">${esc(n.label)}</div>
|
|
1643
|
-
${meta ? `<div class="meta">${esc(meta)}</div>` : ""}
|
|
1644
|
-
${heatStripHtml(n)}
|
|
1645
|
-
</div>
|
|
1646
|
-
<div class="side">
|
|
1647
|
-
<div class="pills">${pills(n)}</div>
|
|
1648
|
-
${platMarkHtml(n)}
|
|
1649
|
-
</div>`;
|
|
1650
|
-
return d;
|
|
1651
|
-
}
|
|
1652
|
-
function pills(n) {
|
|
1653
|
-
const bits = [];
|
|
1654
|
-
if (n.kind === "need") {
|
|
1655
|
-
const st = n.status || "";
|
|
1656
|
-
const cls = st === "promoted" ? "ok" : (st === "captured" ? "gap" : "");
|
|
1657
|
-
if (st) bits.push(`<span class="pill ${cls}">${esc(st)}</span>`);
|
|
1658
|
-
if (n.source_type) bits.push(`<span class="pill">${esc(n.source_type)}</span>`);
|
|
1659
|
-
if (n.frequency) bits.push(`<span class="pill">×${esc(n.frequency)}</span>`);
|
|
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
|
-
}
|
|
1669
|
-
if (n.kind === "topic") {
|
|
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>`);
|
|
1673
|
-
bits.push(n.usage === "unused" ? `<span class="pill gap">${t("unused")}</span>` : `<span class="pill ok">${esc(n.usage)}</span>`);
|
|
1674
|
-
}
|
|
1675
|
-
if (n.kind === "run") {
|
|
1676
|
-
const cls = n.status === "published" ? "ok" : n.status === "superseded" || n.status === "killed" ? "gap" : "";
|
|
1677
|
-
bits.push(`<span class="pill ${cls}">${esc(n.status)}</span>`);
|
|
1678
|
-
if (!runIsShipped(n)) {
|
|
1679
|
-
if (n.status === "superseded") bits.push(`<span class="pill">${t("gap_superseded")}</span>`);
|
|
1680
|
-
else if (n.status === "killed") bits.push(`<span class="pill">${t("gap_killed")}</span>`);
|
|
1681
|
-
else bits.push(`<span class="pill gap">${t("gap_unshipped")}</span>`);
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
if (n.kind === "capture" && n.library_cell === "none") bits.push(`<span class="pill gap">${t("no_lib")}</span>`);
|
|
1685
|
-
if (n.kind === "published") {
|
|
1686
|
-
if (n.result) bits.push(`<span class="pill">${esc(n.result)}</span>`);
|
|
1687
|
-
if (!n.reviewable) bits.push(`<span class="pill gap">${t("thin")}</span>`);
|
|
1688
|
-
}
|
|
1689
|
-
if (n.kind === "swipe" || n.kind === "atom" || n.kind === "claim") {
|
|
1690
|
-
const st = n.status || "";
|
|
1691
|
-
const cls = n.kind === "claim"
|
|
1692
|
-
? (st === "supported" ? "ok" : (st === "weakened" || st === "retired" ? "gap" : ""))
|
|
1693
|
-
: (st === "working" ? "ok" : (st === "dead" ? "gap" : ""));
|
|
1694
|
-
const row1 = [];
|
|
1695
|
-
if (st) row1.push(`<span class="pill ${cls}">${esc(st)}</span>`);
|
|
1696
|
-
row1.push(`<span class="pill hit-win">${esc(t("lib_win", { v: n.win ?? 0 }))}</span>`);
|
|
1697
|
-
row1.push(`<span class="pill hit-loss">${esc(t("lib_loss", { v: n.loss ?? 0 }))}</span>`);
|
|
1698
|
-
row1.push(`<span class="pill hit-n">${esc(t("lib_n", { v: n.n ?? 0 }))}</span>`);
|
|
1699
|
-
const row2 = [`<span class="pill">${esc(t("lib_refs", { n: n.refs ?? 0 }))}</span>`];
|
|
1700
|
-
if (n.kind === "claim" && !n.has_evidence) row2.push(`<span class="pill gap">${t("no_ev")}</span>`);
|
|
1701
|
-
bits.push(`<span class="pill-stack"><span class="pill-row">${row1.join("")}</span><span class="pill-row">${row2.join("")}</span></span>`);
|
|
1702
|
-
}
|
|
1703
|
-
return bits.join("");
|
|
1704
|
-
}
|
|
1705
|
-
function esc(s) {
|
|
1706
|
-
return String(s ?? "").replace(/[&<>"]/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
function render() {
|
|
1710
|
-
for (const b of tabs.querySelectorAll("button")) b.classList.toggle("on", b.dataset.view === view);
|
|
1711
|
-
if (view === "overview") renderOverview();
|
|
1712
|
-
if (view === "needs") renderNeeds();
|
|
1713
|
-
if (view === "hits") renderHits();
|
|
1714
|
-
if (view === "products") renderProducts();
|
|
1715
|
-
if (view === "recommendations") renderRecommendations();
|
|
1716
|
-
if (view === "topics") renderTopics();
|
|
1717
|
-
if (view === "notes") renderNotes();
|
|
1718
|
-
if (view === "runs") renderRuns();
|
|
1719
|
-
if (view === "pub") renderPub();
|
|
1720
|
-
renderInsp();
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
function renderNeedsList(into) {
|
|
1724
|
-
const cards = document.createElement("div");
|
|
1725
|
-
cards.className = "cards";
|
|
1726
|
-
const needs = nodesOf("need").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
1727
|
-
needs.forEach(n => {
|
|
1728
|
-
const meta = [
|
|
1729
|
-
n.source_type || "",
|
|
1730
|
-
n.status || "",
|
|
1731
|
-
n.frequency ? `×${n.frequency}` : "",
|
|
1732
|
-
n.topic_ids && n.topic_ids !== "none" ? n.topic_ids : "",
|
|
1733
|
-
].filter(Boolean).join(" · ");
|
|
1734
|
-
cards.appendChild(listRowEl(n, meta));
|
|
1735
|
-
});
|
|
1736
|
-
if (!needs.length) {
|
|
1737
|
-
cards.innerHTML = `<p class='empty'>${t("no_need")}</p><p class="note">${esc(t("need_empty_hint"))}</p>`;
|
|
1738
|
-
}
|
|
1739
|
-
into.appendChild(cards);
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
function renderNeeds() {
|
|
1743
|
-
$list.innerHTML = "";
|
|
1744
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">${esc(t("need_empty_hint"))}</p>`);
|
|
1745
|
-
renderNeedsList($list);
|
|
1746
|
-
}
|
|
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
|
-
|
|
2041
|
-
function renderProducts() {
|
|
2042
|
-
$list.innerHTML = "";
|
|
2043
|
-
const products = nodesOf("product").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
2044
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">Needs describe demand. Products describe the offer. Topics connect them only when the fit is honest.</p>`);
|
|
2045
|
-
const cards = document.createElement("div");
|
|
2046
|
-
cards.className = "cards";
|
|
2047
|
-
products.forEach(product => {
|
|
2048
|
-
const meta = [
|
|
2049
|
-
product.status,
|
|
2050
|
-
product.audience,
|
|
2051
|
-
product.price,
|
|
2052
|
-
product.effective_from,
|
|
2053
|
-
].filter(Boolean).join(" · ");
|
|
2054
|
-
cards.appendChild(rowEl(product, meta));
|
|
2055
|
-
});
|
|
2056
|
-
if (!products.length) cards.innerHTML = `<p class='empty'>No products in products/_index.md</p>`;
|
|
2057
|
-
$list.appendChild(cards);
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
function renderRecommendations() {
|
|
2061
|
-
$list.innerHTML = "";
|
|
2062
|
-
const recommendations = nodesOf("recommendation").filter(qmatch).sort((a, b) => String(a.label).localeCompare(String(b.label)));
|
|
2063
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">Recommendations are third-party review subjects, not owned Products.</p>`);
|
|
2064
|
-
const cards = document.createElement("div");
|
|
2065
|
-
cards.className = "cards";
|
|
2066
|
-
recommendations.forEach(item => {
|
|
2067
|
-
const meta = [
|
|
2068
|
-
item.status,
|
|
2069
|
-
item.category,
|
|
2070
|
-
item.relationship,
|
|
2071
|
-
item.last_researched,
|
|
2072
|
-
].filter(Boolean).join(" · ");
|
|
2073
|
-
cards.appendChild(rowEl(item, meta));
|
|
2074
|
-
});
|
|
2075
|
-
if (!recommendations.length) cards.innerHTML = `<p class='empty'>No recommendations in recommendations/_index.md</p>`;
|
|
2076
|
-
$list.appendChild(cards);
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
function renderOverview() {
|
|
2080
|
-
$list.innerHTML = "";
|
|
2081
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">${t("lead")}</p>`);
|
|
2082
|
-
const ipSec = document.createElement("h2");
|
|
2083
|
-
ipSec.className = "note";
|
|
2084
|
-
ipSec.style.cssText = "letter-spacing:.12em;text-transform:uppercase;margin:8px 0 10px;font-size:11px;";
|
|
2085
|
-
ipSec.textContent = `${t("ip_section")} · ${profileList().length}`;
|
|
2086
|
-
$list.appendChild(ipSec);
|
|
2087
|
-
const ipgrid = document.createElement("div");
|
|
2088
|
-
ipgrid.className = "ipgrid";
|
|
2089
|
-
profileList().forEach(p => {
|
|
2090
|
-
const card = document.createElement("button");
|
|
2091
|
-
card.type = "button";
|
|
2092
|
-
card.className = "ipcard" + (profileScope === p.id ? " on" : "");
|
|
2093
|
-
card.innerHTML = `<div class="hid">${esc(p.id)}</div>
|
|
2094
|
-
<div class="ttl">${esc(p.one_liner || p.id)}</div>`;
|
|
2095
|
-
card.onclick = () => {
|
|
2096
|
-
const pid = `profile:${p.id}`;
|
|
2097
|
-
if (profileScope === p.id) {
|
|
2098
|
-
profileScope = "";
|
|
2099
|
-
if (selected === pid) selected = null;
|
|
2100
|
-
} else {
|
|
2101
|
-
profileScope = p.id;
|
|
2102
|
-
selected = pid;
|
|
2103
|
-
}
|
|
2104
|
-
localStorage.setItem("wb-profile", profileScope);
|
|
2105
|
-
applyChrome();
|
|
2106
|
-
render();
|
|
2107
|
-
};
|
|
2108
|
-
ipgrid.appendChild(card);
|
|
2109
|
-
});
|
|
2110
|
-
$list.appendChild(ipgrid);
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
function chipRow(label, opts, current, onPick) {
|
|
2114
|
-
const row = document.createElement("div");
|
|
2115
|
-
row.className = "filt-row";
|
|
2116
|
-
const lab = document.createElement("span");
|
|
2117
|
-
lab.className = "filt-lab";
|
|
2118
|
-
lab.textContent = label;
|
|
2119
|
-
row.appendChild(lab);
|
|
2120
|
-
const tabs = document.createElement("div");
|
|
2121
|
-
tabs.className = "subtabs";
|
|
2122
|
-
[{ id: "", label: t("f_all") }, ...opts].forEach(o => {
|
|
2123
|
-
const b = document.createElement("button");
|
|
2124
|
-
b.type = "button";
|
|
2125
|
-
b.textContent = o.label;
|
|
2126
|
-
if ((o.id || "") === (current || "")) b.classList.add("on");
|
|
2127
|
-
b.onclick = () => onPick(o.id);
|
|
2128
|
-
tabs.appendChild(b);
|
|
2129
|
-
});
|
|
2130
|
-
row.appendChild(tabs);
|
|
2131
|
-
return row;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
function filterBar(html, onReady) {
|
|
2135
|
-
const wrap = document.createElement("div");
|
|
2136
|
-
wrap.innerHTML = html;
|
|
2137
|
-
wrap.className = "filters";
|
|
2138
|
-
$list.appendChild(wrap);
|
|
2139
|
-
onReady(wrap);
|
|
2140
|
-
}
|
|
2141
|
-
|
|
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(" · ");
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
function renderTopics() {
|
|
2159
|
-
$list.innerHTML = "";
|
|
2160
|
-
if (topicSrc && !TOPIC_SOURCES.includes(topicSrc)) topicSrc = "";
|
|
2161
|
-
const statuses = [...new Set(nodesOf("topic").map(x => x.status).filter(Boolean))];
|
|
2162
|
-
filterBar(
|
|
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>
|
|
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>
|
|
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>
|
|
2166
|
-
<select id="f-u"><option value="">${t("f_use")}</option>
|
|
2167
|
-
<option value="unused" ${topicUsage === "unused" ? "selected" : ""}>${t("f_unused")}</option>
|
|
2168
|
-
<option value="promoted" ${topicUsage === "promoted" ? "selected" : ""}>${t("f_promoted")}</option></select>`,
|
|
2169
|
-
(w) => {
|
|
2170
|
-
const st = w.querySelector("#f-st");
|
|
2171
|
-
const pill = w.querySelector("#f-p");
|
|
2172
|
-
const src = w.querySelector("#f-src");
|
|
2173
|
-
const use = w.querySelector("#f-u");
|
|
2174
|
-
const paint = () => {
|
|
2175
|
-
topicStatus = st.value;
|
|
2176
|
-
topicPillar = pill.value;
|
|
2177
|
-
topicSrc = src.value;
|
|
2178
|
-
topicUsage = use.value;
|
|
2179
|
-
[...$list.querySelectorAll(".cards")].forEach(n => n.remove());
|
|
2180
|
-
const cards = document.createElement("div");
|
|
2181
|
-
cards.className = "cards";
|
|
2182
|
-
let rows = nodesOf("topic").filter(qmatch).sort((a, b) => Number(b.craft || b.total) - Number(a.craft || a.total));
|
|
2183
|
-
if (topicStatus) rows = rows.filter(x => x.status === topicStatus);
|
|
2184
|
-
if (topicPillar) rows = rows.filter(x => (x.pillar || "").includes(topicPillar));
|
|
2185
|
-
if (topicSrc) rows = rows.filter(x => x.source_type === topicSrc);
|
|
2186
|
-
if (topicUsage) rows = rows.filter(x => x.usage === topicUsage);
|
|
2187
|
-
rows.forEach(x => {
|
|
2188
|
-
cards.appendChild(rowEl(x, topicMeta(x)));
|
|
2189
|
-
});
|
|
2190
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_topic")}</p>`;
|
|
2191
|
-
$list.appendChild(cards);
|
|
2192
|
-
};
|
|
2193
|
-
w.querySelectorAll("select").forEach(s => s.onchange = paint);
|
|
2194
|
-
paint();
|
|
2195
|
-
}
|
|
2196
|
-
);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
function renderNotes() {
|
|
2200
|
-
$list.innerHTML = "";
|
|
2201
|
-
if (notesTab === "needs") notesTab = "notes";
|
|
2202
|
-
const sub = document.createElement("div");
|
|
2203
|
-
sub.className = "subtabs";
|
|
2204
|
-
sub.innerHTML = `<button data-t="notes">${t("notes_in")}</button><button data-t="lib">${t("notes_lib")}</button>`;
|
|
2205
|
-
$list.appendChild(sub);
|
|
2206
|
-
sub.querySelectorAll("button").forEach(b => b.classList.toggle("on", b.dataset.t === notesTab));
|
|
2207
|
-
sub.onclick = (e) => {
|
|
2208
|
-
const tab = e.target.dataset.t;
|
|
2209
|
-
if (!tab) return;
|
|
2210
|
-
notesTab = tab;
|
|
2211
|
-
renderNotes();
|
|
2212
|
-
renderInsp();
|
|
2213
|
-
};
|
|
2214
|
-
if (notesTab === "notes") {
|
|
2215
|
-
const box = document.createElement("div");
|
|
2216
|
-
box.className = "filt";
|
|
2217
|
-
box.appendChild(chipRow(t("f_cap_ingest"), [
|
|
2218
|
-
{ id: "ingested", label: t("f_cap_ingested") },
|
|
2219
|
-
{ id: "not_ingested", label: t("f_cap_not_ingested") },
|
|
2220
|
-
], capIngest, (id) => {
|
|
2221
|
-
capIngest = id;
|
|
2222
|
-
renderNotes();
|
|
2223
|
-
renderInsp();
|
|
2224
|
-
}));
|
|
2225
|
-
$list.appendChild(box);
|
|
2226
|
-
const cards = document.createElement("div");
|
|
2227
|
-
cards.className = "cards";
|
|
2228
|
-
let caps = nodesOf("capture").filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
2229
|
-
if (capIngest === "ingested") caps = caps.filter(c => c.status === "ingested");
|
|
2230
|
-
if (capIngest === "not_ingested") caps = caps.filter(c => c.status !== "ingested");
|
|
2231
|
-
caps.forEach(c => cards.appendChild(listRowEl(c, `${c.type} · ${c.status} · raw ${c.has_raw ? t("raw_yes") : t("raw_no")}`)));
|
|
2232
|
-
if (!caps.length) cards.innerHTML = `<p class='empty'>${t("no_note")}</p>`;
|
|
2233
|
-
$list.appendChild(cards);
|
|
2234
|
-
return;
|
|
2235
|
-
}
|
|
2236
|
-
const items = GRAPH.nodes.filter(n => n.lib && inScope(n));
|
|
2237
|
-
const subtypeOf = (n) => n.atom_type || n.form || n.claim_type || "";
|
|
2238
|
-
const kinds = [...new Set(items.map(n => n.kind))];
|
|
2239
|
-
const pool = items.filter(n => !libKind || n.kind === libKind);
|
|
2240
|
-
const subs = [...new Set(pool.map(subtypeOf).filter(Boolean))].sort();
|
|
2241
|
-
const statusVocab = {
|
|
2242
|
-
swipe: ["trial", "working", "dead"],
|
|
2243
|
-
atom: ["trial", "working", "dead"],
|
|
2244
|
-
claim: ["hypothesis", "supported", "weakened", "retired"],
|
|
2245
|
-
};
|
|
2246
|
-
const statuses = statusVocab[libKind] || [];
|
|
2247
|
-
const box = document.createElement("div");
|
|
2248
|
-
box.className = "filt";
|
|
2249
|
-
box.appendChild(chipRow(t("f_lib_kind"), kinds.map(k => ({ id: k, label: kindName(k) })), libKind, (id) => {
|
|
2250
|
-
libKind = id;
|
|
2251
|
-
libSubtype = "";
|
|
2252
|
-
libStatus = "";
|
|
2253
|
-
renderNotes();
|
|
2254
|
-
renderInsp();
|
|
2255
|
-
}));
|
|
2256
|
-
box.appendChild(chipRow(t("f_lib_subtype"), subs.map(s => ({ id: s, label: s })), libSubtype, (id) => {
|
|
2257
|
-
libSubtype = id;
|
|
2258
|
-
renderNotes();
|
|
2259
|
-
renderInsp();
|
|
2260
|
-
}));
|
|
2261
|
-
if (libKind) {
|
|
2262
|
-
box.appendChild(chipRow(t("f_lib_status"), statuses.map(s => ({ id: s, label: s })), libStatus, (id) => {
|
|
2263
|
-
libStatus = id;
|
|
2264
|
-
renderNotes();
|
|
2265
|
-
renderInsp();
|
|
2266
|
-
}));
|
|
2267
|
-
}
|
|
2268
|
-
$list.appendChild(box);
|
|
2269
|
-
const cards = document.createElement("div");
|
|
2270
|
-
cards.className = "cards";
|
|
2271
|
-
let rows = items.filter(qmatch);
|
|
2272
|
-
if (libKind) rows = rows.filter(n => n.kind === libKind);
|
|
2273
|
-
if (libSubtype) rows = rows.filter(n => subtypeOf(n) === libSubtype);
|
|
2274
|
-
if (libStatus) rows = rows.filter(n => n.status === libStatus);
|
|
2275
|
-
rows.forEach(n => cards.appendChild(rowEl(n, `${kindName(n.kind)}${subtypeOf(n) ? " · " + subtypeOf(n) : ""}`)));
|
|
2276
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_note")}</p>`;
|
|
2277
|
-
$list.appendChild(cards);
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
function topicIdOf(n) {
|
|
2281
|
-
if (n.topic_id) return n.topic_id;
|
|
2282
|
-
if (n.kind === "published" && n.run_id) {
|
|
2283
|
-
return byId[`run:${n.run_id}`]?.topic_id || "";
|
|
2284
|
-
}
|
|
2285
|
-
return "";
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
function topicSelectHtml(nodes, current) {
|
|
2289
|
-
const ids = [...new Set(nodes.map(topicIdOf).filter(Boolean))].sort();
|
|
2290
|
-
const hasNone = nodes.some(n => !topicIdOf(n));
|
|
2291
|
-
const opts = [
|
|
2292
|
-
`<option value="">${t("f_run_topic")} · ${t("f_all")}</option>`,
|
|
2293
|
-
...ids.map(id => {
|
|
2294
|
-
const n = nodes.filter(x => topicIdOf(x) === id).length;
|
|
2295
|
-
return `<option value="${esc(id)}" ${id === current ? "selected" : ""}>${esc(t("f_run_topic_n", { id, n }))}</option>`;
|
|
2296
|
-
}),
|
|
2297
|
-
];
|
|
2298
|
-
if (hasNone) {
|
|
2299
|
-
opts.push(`<option value="__none__" ${current === "__none__" ? "selected" : ""}>${esc(t("f_run_no_topic"))}</option>`);
|
|
2300
|
-
}
|
|
2301
|
-
return `<select id="f-topic" aria-label="${esc(t("f_run_topic"))}">${opts.join("")}</select>`;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
function renderRuns() {
|
|
2305
|
-
$list.innerHTML = "";
|
|
2306
|
-
const runs = nodesOf("run");
|
|
2307
|
-
const plats = ["x", "tiktok", "instagram", "linkedin", "threads", "xiaohongshu"];
|
|
2308
|
-
const box = document.createElement("div");
|
|
2309
|
-
box.className = "filt";
|
|
2310
|
-
const top = document.createElement("div");
|
|
2311
|
-
top.className = "filters";
|
|
2312
|
-
top.style.marginBottom = "0";
|
|
2313
|
-
top.innerHTML = topicSelectHtml(runs, runTopic);
|
|
2314
|
-
box.appendChild(top);
|
|
2315
|
-
box.appendChild(chipRow(t("f_run_ship"), [
|
|
2316
|
-
{ id: "shipped", label: t("f_run_shipped") },
|
|
2317
|
-
{ id: "unshipped", label: t("f_run_unshipped") },
|
|
2318
|
-
], runShip, (id) => { runShip = id; renderRuns(); renderInsp(); }));
|
|
2319
|
-
box.appendChild(chipRow(t("f_run_plat"), plats.map(p => ({ id: p, label: p })), runPlat, (id) => {
|
|
2320
|
-
runPlat = id; renderRuns(); renderInsp();
|
|
2321
|
-
}));
|
|
2322
|
-
$list.appendChild(box);
|
|
2323
|
-
top.querySelector("#f-topic").onchange = (e) => {
|
|
2324
|
-
runTopic = e.target.value;
|
|
2325
|
-
renderRuns();
|
|
2326
|
-
renderInsp();
|
|
2327
|
-
};
|
|
2328
|
-
const cards = document.createElement("div");
|
|
2329
|
-
cards.className = "cards";
|
|
2330
|
-
let rows = runs.filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
2331
|
-
if (runTopic === "__none__") rows = rows.filter(r => !topicIdOf(r));
|
|
2332
|
-
else if (runTopic) rows = rows.filter(r => topicIdOf(r) === runTopic);
|
|
2333
|
-
if (runShip === "shipped") rows = rows.filter(runIsShipped);
|
|
2334
|
-
if (runShip === "unshipped") rows = rows.filter(r => !runIsShipped(r));
|
|
2335
|
-
if (runPlat) {
|
|
2336
|
-
rows = rows.filter(r =>
|
|
2337
|
-
String(r.platforms || r.primary_platform || "").split(",").map(s => s.trim().toLowerCase()).includes(runPlat)
|
|
2338
|
-
);
|
|
2339
|
-
}
|
|
2340
|
-
rows.forEach(r => cards.appendChild(rowEl(r)));
|
|
2341
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_run")}</p>`;
|
|
2342
|
-
$list.appendChild(cards);
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
function renderPub() {
|
|
2346
|
-
$list.innerHTML = "";
|
|
2347
|
-
$list.insertAdjacentHTML("beforeend", `<p class="lead">${t("pub_lead")}</p><p class="lede">${t("pub_lede")}</p>`);
|
|
2348
|
-
const pubs = nodesOf("published");
|
|
2349
|
-
const plats = ["x", "tiktok", "instagram", "linkedin", "threads", "xiaohongshu"];
|
|
2350
|
-
const box = document.createElement("div");
|
|
2351
|
-
box.className = "filt";
|
|
2352
|
-
const top = document.createElement("div");
|
|
2353
|
-
top.className = "filters";
|
|
2354
|
-
top.style.marginBottom = "0";
|
|
2355
|
-
top.innerHTML = topicSelectHtml(pubs, pubTopic);
|
|
2356
|
-
box.appendChild(top);
|
|
2357
|
-
box.appendChild(chipRow(t("f_run_plat"), plats.map(p => ({ id: p, label: p })), pubPlat, (id) => {
|
|
2358
|
-
pubPlat = id; renderPub(); renderInsp();
|
|
2359
|
-
}));
|
|
2360
|
-
$list.appendChild(box);
|
|
2361
|
-
top.querySelector("#f-topic").onchange = (e) => {
|
|
2362
|
-
pubTopic = e.target.value;
|
|
2363
|
-
renderPub();
|
|
2364
|
-
renderInsp();
|
|
2365
|
-
};
|
|
2366
|
-
const cards = document.createElement("div");
|
|
2367
|
-
cards.className = "cards";
|
|
2368
|
-
let rows = pubs.filter(qmatch).sort((a, b) => (b.date || "").localeCompare(a.date || ""));
|
|
2369
|
-
if (pubTopic === "__none__") rows = rows.filter(p => !topicIdOf(p));
|
|
2370
|
-
else if (pubTopic) rows = rows.filter(p => topicIdOf(p) === pubTopic);
|
|
2371
|
-
if (pubPlat) {
|
|
2372
|
-
rows = rows.filter(p => String(p.platform || "").trim().toLowerCase() === pubPlat);
|
|
2373
|
-
}
|
|
2374
|
-
if (!rows.length) cards.innerHTML = `<p class='empty'>${t("no_pub")}</p>`;
|
|
2375
|
-
rows.forEach(p => cards.appendChild(rowEl(p)));
|
|
2376
|
-
$list.appendChild(cards);
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
function rawLinksFor(n, ctx) {
|
|
2380
|
-
const out = [];
|
|
2381
|
-
if (n.kind === "raw" && n.source_url) {
|
|
2382
|
-
out.push({ id: n.id, ident: n.ident, label: n.label, url: n.source_url, path: n.path });
|
|
2383
|
-
return out;
|
|
2384
|
-
}
|
|
2385
|
-
if (n.kind === "capture") {
|
|
2386
|
-
ctx.filter(x => x.kind === "raw").forEach(r => {
|
|
2387
|
-
out.push({ id: r.id, ident: r.ident, label: r.label, url: r.source_url || "", path: r.path });
|
|
2388
|
-
});
|
|
2389
|
-
}
|
|
2390
|
-
return out;
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
function sourceBlockHtml(n, ctx) {
|
|
2394
|
-
const topicUrl = n.src_url || "";
|
|
2395
|
-
const ownUrl = n.source_url || "";
|
|
2396
|
-
const raws = rawLinksFor(n, ctx);
|
|
2397
|
-
let html = "";
|
|
2398
|
-
if (topicUrl) {
|
|
2399
|
-
html += `<p class="note">${t("src_url")}<a href="${esc(topicUrl)}" target="_blank" rel="noreferrer">${esc(topicUrl)}</a><br>${t("src_heat")}
|
|
2400
|
-
${n.src_likes ? ` likes ${esc(n.src_likes)} · replies ${esc(n.src_replies)} · bookmarks ${esc(n.src_bookmarks)}` : ""}</p>`;
|
|
2401
|
-
}
|
|
2402
|
-
if (n.kind === "capture" || n.kind === "raw") {
|
|
2403
|
-
html += `<h2>${t("raw_section")}</h2>`;
|
|
2404
|
-
if (!raws.length && !ownUrl) {
|
|
2405
|
-
html += `<p class="vacant">${t("raw_none")}</p>`;
|
|
2406
|
-
} else if (n.kind === "raw") {
|
|
2407
|
-
if (ownUrl) html += `<p class="note">${t("raw_link")}:<a href="${esc(ownUrl)}" target="_blank" rel="noreferrer">${esc(ownUrl)}</a></p>`;
|
|
2408
|
-
} else {
|
|
2409
|
-
raws.forEach(r => {
|
|
2410
|
-
html += `<div class="note" style="margin:0 0 10px">
|
|
2411
|
-
<div><button type="button" class="ghost-link" data-goto="${esc(r.id)}">${esc(r.ident)}</button> · ${esc(r.label.slice(0, 80))}</div>
|
|
2412
|
-
${r.url ? `<div>${t("raw_link")}:<a href="${esc(r.url)}" target="_blank" rel="noreferrer">${esc(r.url)}</a></div>` : `<div class="vacant">${t("raw_none")}</div>`}
|
|
2413
|
-
${r.path ? `<div>${t("raw_file")}:<button type="button" class="ghost-link" data-md="${esc(r.path)}" data-md-title="${esc(r.ident)}">${esc(r.path)}</button></div>` : ""}
|
|
2414
|
-
</div>`;
|
|
2415
|
-
});
|
|
2416
|
-
}
|
|
2417
|
-
} else if (ownUrl && !topicUrl) {
|
|
2418
|
-
html += `<p class="note">${t("src_url")}<a href="${esc(ownUrl)}" target="_blank" rel="noreferrer">${esc(ownUrl)}</a></p>`;
|
|
2419
|
-
}
|
|
2420
|
-
return html;
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
function profileCounts(profileId) {
|
|
2424
|
-
return {
|
|
2425
|
-
capture: nodesOfAll("capture").filter(n => n.profile === profileId).length,
|
|
2426
|
-
need: nodesOfAll("need").filter(n => n.profile === profileId).length,
|
|
2427
|
-
hit: nodesOfAll("hit").filter(n => n.profile === profileId).length,
|
|
2428
|
-
topic: nodesOfAll("topic").filter(n => n.profile === profileId).length,
|
|
2429
|
-
lib: GRAPH.nodes.filter(n => n.lib && (String(n.profiles || "").includes(profileId) || String(n.profiles || "").includes("*"))).length,
|
|
2430
|
-
run: nodesOfAll("run").filter(n => n.profile === profileId).length,
|
|
2431
|
-
published: nodesOfAll("published").filter(n => n.profile === profileId).length,
|
|
2432
|
-
};
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
function countsFor(profileId) {
|
|
2436
|
-
if (profileId) return profileCounts(profileId);
|
|
2437
|
-
return {
|
|
2438
|
-
capture: nodesOfAll("capture").length,
|
|
2439
|
-
need: nodesOfAll("need").length,
|
|
2440
|
-
hit: nodesOfAll("hit").length,
|
|
2441
|
-
topic: nodesOfAll("topic").length,
|
|
2442
|
-
lib: GRAPH.nodes.filter(n => n.lib).length,
|
|
2443
|
-
run: nodesOfAll("run").length,
|
|
2444
|
-
published: nodesOfAll("published").length,
|
|
2445
|
-
};
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
function gapsFor(profileId) {
|
|
2449
|
-
const topics = profileId ? nodesOfAll("topic").filter(n => n.profile === profileId) : nodesOfAll("topic");
|
|
2450
|
-
const needs = profileId ? nodesOfAll("need").filter(n => n.profile === profileId) : nodesOfAll("need");
|
|
2451
|
-
const hits = profileId ? nodesOfAll("hit").filter(n => n.profile === profileId) : nodesOfAll("hit");
|
|
2452
|
-
const caps = profileId ? nodesOfAll("capture").filter(n => n.profile === profileId) : nodesOfAll("capture");
|
|
2453
|
-
const pubs = profileId ? nodesOfAll("published").filter(n => n.profile === profileId) : nodesOfAll("published");
|
|
2454
|
-
const needCount = needs.length;
|
|
2455
|
-
const gaps = [
|
|
2456
|
-
[hits.filter(h => h.status === "triage").length, "gap_hits_n"],
|
|
2457
|
-
[topics.filter(x => x.status === "ready" && x.usage === "unused").length, "gap_ready_n"],
|
|
2458
|
-
[needs.filter(n => n.status === "captured" && (!n.topic_ids || n.topic_ids === "none")).length, "gap_needs_n"],
|
|
2459
|
-
[caps.filter(c => c.status !== "ingested").length, "gap_lib_n"],
|
|
2460
|
-
[pubs.filter(p => !p.reviewable).length, "gap_review_n"],
|
|
2461
|
-
].filter(([n]) => n > 0);
|
|
2462
|
-
if (GRAPH.needs_index_present && needCount < 3) {
|
|
2463
|
-
const idx = gaps.findIndex(([, k]) => k === "gap_needs_n");
|
|
2464
|
-
gaps.splice(idx < 0 ? gaps.length : idx, 0, [needCount, "gap_needs_thin_n"]);
|
|
2465
|
-
}
|
|
2466
|
-
return gaps;
|
|
2467
|
-
}
|
|
2468
|
-
|
|
2469
|
-
function statsHtml(counts) {
|
|
2470
|
-
return `<div class="prof-stats">
|
|
2471
|
-
<div><b>${counts.capture}</b><span>${t("kpi_cap")}</span></div>
|
|
2472
|
-
<div><b>${counts.need || 0}</b><span>${t("kpi_needs")}</span></div>
|
|
2473
|
-
<div><b>${counts.hit || 0}</b><span>${t("kpi_hits")}</span></div>
|
|
2474
|
-
<div><b>${counts.topic}</b><span>${t("kpi_topics")}</span></div>
|
|
2475
|
-
<div><b>${counts.lib}</b><span>${t("kpi_lib")}</span></div>
|
|
2476
|
-
<div><b>${counts.run}</b><span>Run</span></div>
|
|
2477
|
-
<div><b>${counts.published}</b><span>${t("kind_published")}</span></div>
|
|
2478
|
-
</div>`;
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
function gapsHtml(profileId) {
|
|
2482
|
-
const gaps = gapsFor(profileId);
|
|
2483
|
-
if (!gaps.length) return `<p class="vacant">${t("gap_none")}</p>`;
|
|
2484
|
-
return `<div class="gap-list">${gaps.map(([n, k]) => `<p>${esc(t(k, { n }))}</p>`).join("")}</div>`;
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
function renderVaultInsp() {
|
|
2488
|
-
$insp.innerHTML = `<h2>${t("vault_title")}</h2>
|
|
2489
|
-
<p class="kicker">${t("vault_kicker")}</p>
|
|
2490
|
-
<p class="note" style="margin:0 0 16px">${esc(t("vault_lede"))}</p>
|
|
2491
|
-
<h2>${t("profile_stats")}</h2>
|
|
2492
|
-
${statsHtml(countsFor(""))}
|
|
2493
|
-
<h2>${t("gap_section")}</h2>
|
|
2494
|
-
${gapsHtml("")}
|
|
2495
|
-
<p class="note">${t("readonly")}</p>`;
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
function renderProfileInsp(n) {
|
|
2499
|
-
const pillars = (GRAPH.pillars_by_profile || {})[n.ident] || {};
|
|
2500
|
-
const accounts = nodesOfAll("account").filter(a => a.profile_id === n.ident);
|
|
2501
|
-
const files = n.files || {};
|
|
2502
|
-
const docOrder = [
|
|
2503
|
-
["readme", t("doc_readme")],
|
|
2504
|
-
["voice", t("doc_voice")],
|
|
2505
|
-
["audience", t("doc_audience")],
|
|
2506
|
-
["pillars", t("doc_pillars")],
|
|
2507
|
-
["lanes", t("doc_lanes")],
|
|
2508
|
-
["keywords", t("doc_keywords")],
|
|
2509
|
-
["handles", t("doc_handles")],
|
|
2510
|
-
];
|
|
2511
|
-
const pillarRows = Object.entries(pillars).map(([id, p]) => {
|
|
2512
|
-
const title = lang === "zh" ? p.zh : p.en;
|
|
2513
|
-
return `<div class="prof-pill"><span class="hid">${esc(id)}</span><span>${esc(title)}</span></div>`;
|
|
2514
|
-
}).join("") || `<p class="vacant">${t("vacant")}</p>`;
|
|
2515
|
-
const acctRows = accounts.map(a =>
|
|
2516
|
-
`<button type="button" data-account-open="${esc(a.id)}">${esc(a.ident)} · ${esc(a.platforms || a.default_platform || "")} · ${esc(a.status || "")}</button>`
|
|
2517
|
-
).join("") || `<p class="vacant">${t("vacant")}</p>`;
|
|
2518
|
-
const docRows = docOrder.filter(([k]) => files[k]).map(([k, label]) =>
|
|
2519
|
-
`<button type="button" class="stage-file-lite" data-md="${esc(files[k])}" data-md-title="${esc(label)}">
|
|
2520
|
-
<span class="sk">${esc(label)}</span><span class="sp">${esc(files[k].split("/").pop())}</span><span class="sa">${t("read_md")}</span>
|
|
2521
|
-
</button>`
|
|
2522
|
-
).join("") || `<p class="vacant">${t("vacant")}</p>`;
|
|
2523
|
-
$insp.innerHTML = `<h2>${t("kind_profile")}</h2>
|
|
2524
|
-
<p class="kicker">${esc(n.ident)}</p>
|
|
2525
|
-
<p class="title">${esc(n.label)}</p>
|
|
2526
|
-
<h2>${t("profile_stats")}</h2>
|
|
2527
|
-
${statsHtml(countsFor(n.ident))}
|
|
2528
|
-
<h2>${t("gap_section")}</h2>
|
|
2529
|
-
${gapsHtml(n.ident)}
|
|
2530
|
-
<h2>${t("profile_pillars")}</h2>
|
|
2531
|
-
<div class="prof-pillars">${pillarRows}</div>
|
|
2532
|
-
<h2>${t("profile_accounts")}</h2>
|
|
2533
|
-
<div class="tcard prof-accts">${acctRows}</div>
|
|
2534
|
-
<h2>${t("profile_docs")}</h2>
|
|
2535
|
-
<div class="prof-docs">${docRows}</div>
|
|
2536
|
-
<p class="note">${t("readonly")}</p>`;
|
|
2537
|
-
$insp.querySelectorAll("[data-account-open]").forEach(btn => {
|
|
2538
|
-
btn.onclick = (e) => {
|
|
2539
|
-
e.preventDefault();
|
|
2540
|
-
e.stopPropagation();
|
|
2541
|
-
const node = byId[btn.getAttribute("data-account-open")];
|
|
2542
|
-
if (node) openAccount(node);
|
|
2543
|
-
};
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
function renderInsp() {
|
|
2548
|
-
if (!selected || !byId[selected]) {
|
|
2549
|
-
if (view === "overview") {
|
|
2550
|
-
renderVaultInsp();
|
|
2551
|
-
} else {
|
|
2552
|
-
$insp.innerHTML = `<div class='empty'><strong>${t("empty_t")}</strong>${t("empty_b")}</div>`;
|
|
2553
|
-
}
|
|
2554
|
-
return;
|
|
2555
|
-
}
|
|
2556
|
-
const n = byId[selected];
|
|
2557
|
-
if (n.kind === "profile") {
|
|
2558
|
-
renderProfileInsp(n);
|
|
2559
|
-
return;
|
|
2560
|
-
}
|
|
2561
|
-
const ctx = connected(n.id);
|
|
2562
|
-
const byKind = {};
|
|
2563
|
-
for (const k of TRACE_ORDER) byKind[k] = ctx.filter(x => x.kind === k);
|
|
2564
|
-
const kv = Object.entries({
|
|
2565
|
-
[t("kv_kind")]: kindName(n.kind),
|
|
2566
|
-
[t("kv_date")]: n.date,
|
|
2567
|
-
[t("kv_status")]: n.status,
|
|
2568
|
-
[t("kv_refs")]: (n.kind === "swipe" || n.kind === "atom" || n.kind === "claim") ? String(n.refs ?? 0) : "",
|
|
2569
|
-
[t("kv_hits")]: (n.kind === "swipe" || n.kind === "atom" || n.kind === "claim") && n.n != null
|
|
2570
|
-
? `${n.win || 0}w / ${n.loss || 0}l / n=${n.n}`
|
|
2571
|
-
: "",
|
|
2572
|
-
[t("kv_pillar")]: formatPillar(n.pillar),
|
|
2573
|
-
[t("kv_lane")]: n.kind === "topic" ? (n.lane || "") : "",
|
|
2574
|
-
[t("kv_mode")]: n.kind === "topic" ? (n.generation_mode || "") : "",
|
|
2575
|
-
[t("kv_plat")]: n.platform || n.platforms,
|
|
2576
|
-
[t("kv_profile")]: n.profile || n.profile_id || (n.profiles || ""),
|
|
2577
|
-
[t("kv_score")]: n.fit ? `${n.craft} opp / ${n.fit} aud` : n.total,
|
|
2578
|
-
[t("kv_use")]: n.usage,
|
|
2579
|
-
[t("kv_src")]: n.kind === "hit" ? (n.source || "") : n.source_type,
|
|
2580
|
-
[t("kv_hit_kind")]: n.kind === "hit" ? (n.hit_kind || "") : "",
|
|
2581
|
-
[t("kv_signal")]: n.kind === "hit" ? (n.signal || "") : "",
|
|
2582
|
-
Keyword: n.kind === "hit" ? (n.keyword_id || "") : "",
|
|
2583
|
-
[t("kv_hit_source")]: n.kind === "hit" ? (n.source || "") : "",
|
|
2584
|
-
[t("kv_needs")]: n.kind === "topic" ? (n.need_ids || "") : "",
|
|
2585
|
-
Quote: n.kind === "need" ? (n.quote || "") : "",
|
|
2586
|
-
Excerpt: n.kind === "hit" ? (n.excerpt || "") : "",
|
|
2587
|
-
Speaker: n.kind === "need" ? (n.speaker || "") : "",
|
|
2588
|
-
Frequency: n.kind === "need" ? (n.frequency || "") : "",
|
|
2589
|
-
Topics: n.kind === "need" ? (n.topic_ids || "") : "",
|
|
2590
|
-
Products: n.kind === "topic" ? (n.product_ids || "") : "",
|
|
2591
|
-
"Product fit": n.kind === "topic" ? (n.product_fit || "") : "",
|
|
2592
|
-
[t("kv_recs")]: n.kind === "topic" ? (n.recommendation_ids || "") : "",
|
|
2593
|
-
Audience: n.kind === "product" ? (n.audience || "") : "",
|
|
2594
|
-
Price: n.kind === "product" ? (n.price || "") : "",
|
|
2595
|
-
"Effective from": n.kind === "product" ? (n.effective_from || "") : "",
|
|
2596
|
-
Category: n.kind === "recommendation" ? (n.category || "") : "",
|
|
2597
|
-
Relationship: n.kind === "recommendation" ? (n.relationship || "") : "",
|
|
2598
|
-
"Last researched": n.kind === "recommendation" ? (n.last_researched || "") : "",
|
|
2599
|
-
Handle: n.source_handle || n.src_handle || "",
|
|
2600
|
-
[t("kv_heat_at")]: (n.kind === "capture" || n.kind === "hit") ? (n.engagement_at || "") : "",
|
|
2601
|
-
[t("kv_result")]: n.result,
|
|
2602
|
-
}).filter(([, v]) => v);
|
|
2603
|
-
const kwMeta = n.kind === "hit" && n.keyword_id
|
|
2604
|
-
? (GRAPH.keywords || []).find(k => k.id === n.keyword_id && (!n.profile || k.profile === n.profile))
|
|
2605
|
-
: null;
|
|
2606
|
-
const kwBlock = n.kind === "hit" && n.keyword_id
|
|
2607
|
-
? `<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>`
|
|
2608
|
-
: "";
|
|
2609
|
-
const heatBlock = (n.kind === "capture" || n.kind === "hit")
|
|
2610
|
-
? `${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>` : ""}`
|
|
2611
|
-
: "";
|
|
2612
|
-
$insp.innerHTML = `<h2>${t("obj")}</h2>
|
|
2613
|
-
<p class="kicker">${esc(kindName(n.kind))} · ${esc(n.ident)}</p>
|
|
2614
|
-
<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>
|
|
2615
|
-
<dl class="kv">${kv.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join("")}</dl>
|
|
2616
|
-
${kwBlock}
|
|
2617
|
-
${heatBlock}
|
|
2618
|
-
${n.kind === "swipe" || n.kind === "atom" ? `<p class="note">${esc(t("lib_use_hist"))}</p>` : ""}
|
|
2619
|
-
${n.kind === "claim" ? `<p class="note">${esc(t("lib_claim_ev"))}</p>` : ""}
|
|
2620
|
-
${sourceBlockHtml(n, ctx)}
|
|
2621
|
-
${n.url && n.kind === "published" ? `<p><a href="${esc(n.url)}" target="_blank" rel="noreferrer">${esc(n.url)}</a></p>
|
|
2622
|
-
${n.reviewable ? "" : `<p class='warn'>${t("thin_warn")}</p>`}` : ""}
|
|
2623
|
-
${n.kind === "run" ? runMetaHtml(n) : ""}
|
|
2624
|
-
${mdActionsHtml(n)}
|
|
2625
|
-
<h2>${t("trace")}</h2>
|
|
2626
|
-
<div class="trace" id="trace"></div>
|
|
2627
|
-
<p class="note">${t("readonly")}</p>`;
|
|
2628
|
-
$insp.querySelectorAll("[data-goto]").forEach(btn => {
|
|
2629
|
-
btn.onclick = () => select(btn.getAttribute("data-goto"));
|
|
2630
|
-
});
|
|
2631
|
-
bindKwFilters($insp);
|
|
2632
|
-
const trace = $insp.querySelector("#trace");
|
|
2633
|
-
for (const k of TRACE_ORDER) {
|
|
2634
|
-
const items = k === "published" ? publishedFor(n, ctx) : (byKind[k] || []);
|
|
2635
|
-
const step = document.createElement("div");
|
|
2636
|
-
step.className = "tstep";
|
|
2637
|
-
const here = items.some(x => x.id === n.id);
|
|
2638
|
-
const on = items.length > 0;
|
|
2639
|
-
step.innerHTML = `<div class="tspine"><div class="dot${on ? " on" : ""}${here ? " here" : ""}"></div><div class="rail"></div></div>`;
|
|
2640
|
-
const body = document.createElement("div");
|
|
2641
|
-
body.className = "tcard";
|
|
2642
|
-
const lab = document.createElement("div");
|
|
2643
|
-
lab.className = "note";
|
|
2644
|
-
lab.textContent = kindName(k);
|
|
2645
|
-
body.appendChild(lab);
|
|
2646
|
-
if (!items.length) {
|
|
2647
|
-
const v = document.createElement("div");
|
|
2648
|
-
v.className = "vacant";
|
|
2649
|
-
v.textContent = t("vacant");
|
|
2650
|
-
body.appendChild(v);
|
|
2651
|
-
} else {
|
|
2652
|
-
items.forEach(item => {
|
|
2653
|
-
const b = document.createElement("button");
|
|
2654
|
-
if (item.source_url && item.kind === "raw") {
|
|
2655
|
-
b.innerHTML = `${esc(item.ident)} ${esc(item.label.slice(0, 48))}<br><span class="note">${esc(item.source_url)}</span>`;
|
|
2656
|
-
} else {
|
|
2657
|
-
b.textContent = `${item.ident} ${item.label.slice(0, 72)}`;
|
|
2658
|
-
}
|
|
2659
|
-
b.onclick = () => {
|
|
2660
|
-
if (item.kind === "run") {
|
|
2661
|
-
selected = item.id;
|
|
2662
|
-
render();
|
|
2663
|
-
openRun(item);
|
|
2664
|
-
return;
|
|
2665
|
-
}
|
|
2666
|
-
select(item.id);
|
|
2667
|
-
const md = mdFileForNode(item);
|
|
2668
|
-
if (md) openMd(md, item.ident);
|
|
2669
|
-
};
|
|
2670
|
-
body.appendChild(b);
|
|
2671
|
-
});
|
|
2672
|
-
}
|
|
2673
|
-
step.appendChild(body);
|
|
2674
|
-
trace.appendChild(step);
|
|
2675
|
-
}
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
applyChrome();
|
|
2679
|
-
render();
|
|
2680
|
-
|
|
2681
|
-
</script>
|
|
2682
|
-
</body>
|
|
2683
|
-
</html>
|