akm-cli 0.9.27 → 0.9.28-alpha.2
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/CHANGELOG.md +65 -0
- package/STABILITY.md +4 -0
- package/dist/assets/hints/cli-hints-full.md +3 -0
- package/dist/assets/hints/cli-hints-short.md +1 -0
- package/dist/assets/prompts/consolidate-pair.md +1 -1
- package/dist/assets/templates/html/metrics.html +977 -0
- package/dist/cli/shared.js +5 -4
- package/dist/cli.js +11 -1
- package/dist/commands/health/accept-rate.js +8 -4
- package/dist/commands/health/html-report.js +3 -8
- package/dist/commands/health/llm-usage.js +17 -6
- package/dist/commands/health/renderers.js +4 -4
- package/dist/commands/improve/improve-report.js +4 -2
- package/dist/commands/improve/improve.js +22 -10
- package/dist/commands/improve/loop-stages.js +8 -1
- package/dist/commands/improve/preparation.js +16 -0
- package/dist/commands/improve/stage.js +1 -1
- package/dist/commands/metrics/collect.js +439 -0
- package/dist/commands/metrics/html-report.js +82 -0
- package/dist/commands/metrics/md-report.js +44 -0
- package/dist/commands/metrics/metrics-cli.js +213 -0
- package/dist/commands/metrics/report-view.js +243 -0
- package/dist/commands/metrics/types.js +4 -0
- package/dist/commands/read/search.js +5 -0
- package/dist/indexer/indexer.js +64 -14
- package/dist/indexer/usage/usage-events.js +3 -1
- package/dist/integrations/session-logs/pre-filter.js +1 -0
- package/dist/llm/usage-persist.js +22 -11
- package/dist/llm/usage-telemetry.js +4 -0
- package/dist/output/html-render.js +15 -8
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/metrics.js +39 -0
- package/dist/output/text.js +2 -0
- package/dist/scripts/akm-migrate-node.js +281 -8
- package/dist/scripts/akm-migrate.js +281 -8
- package/dist/storage/repositories/index-utility-repository.js +24 -0
- package/dist/storage/repositories/metrics-repository.js +80 -0
- package/docs/reference/cli.md +63 -4
- package/docs/reference/data-and-telemetry.md +41 -8
- package/package.json +1 -1
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>%%REPORT_TITLE%%</title>
|
|
7
|
+
%%ECHARTS_TAG%%
|
|
8
|
+
<style>
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0d1117;
|
|
13
|
+
--surface: #161b22;
|
|
14
|
+
--surface2:#21262d;
|
|
15
|
+
--border: #30363d;
|
|
16
|
+
--text: #e6edf3;
|
|
17
|
+
--muted: #9ea8b5;
|
|
18
|
+
--accent: #58a6ff;
|
|
19
|
+
--green: #3fb950;
|
|
20
|
+
--yellow: #d29922;
|
|
21
|
+
--red: #f85149;
|
|
22
|
+
--purple: #bc8cff;
|
|
23
|
+
--neutral: #8b949e;
|
|
24
|
+
}
|
|
25
|
+
@media (prefers-color-scheme: light) {
|
|
26
|
+
:root {
|
|
27
|
+
--bg: #ffffff;
|
|
28
|
+
--surface: #f6f8fa;
|
|
29
|
+
--surface2:#eaeef2;
|
|
30
|
+
--border: #d0d7de;
|
|
31
|
+
--text: #1f2328;
|
|
32
|
+
--muted: #57606a;
|
|
33
|
+
--accent: #0969da;
|
|
34
|
+
--green: #1a7f37;
|
|
35
|
+
--yellow: #9a6700;
|
|
36
|
+
--red: #cf222e;
|
|
37
|
+
--purple: #8250df;
|
|
38
|
+
--neutral: #6e7781;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
body {
|
|
43
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', sans-serif;
|
|
44
|
+
background: var(--bg);
|
|
45
|
+
color: var(--text);
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
line-height: 1.6;
|
|
48
|
+
}
|
|
49
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; word-break: break-all; }
|
|
50
|
+
button { font: inherit; color: var(--text); background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 3px 10px; cursor: pointer; }
|
|
51
|
+
button:hover { border-color: var(--accent); }
|
|
52
|
+
button:focus-visible, input:focus-visible, th:focus-visible, tr.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
53
|
+
input[type=text], input[type=date] { font: inherit; color: var(--text); background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 3px 8px; }
|
|
54
|
+
|
|
55
|
+
header { background: var(--surface); border-bottom: 1px solid var(--border); padding: 16px 20px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
56
|
+
header .logo { font-size: 20px; font-weight: 700; color: var(--accent); letter-spacing: -0.5px; display: flex; flex-direction: column; line-height: 1.1; }
|
|
57
|
+
header .logo-version { font-size: 10px; font-weight: 500; color: var(--muted); letter-spacing: 0; }
|
|
58
|
+
header .meta { color: var(--muted); font-size: 13px; min-width: 0; }
|
|
59
|
+
header .meta .title { font-weight: 600; color: var(--text); }
|
|
60
|
+
|
|
61
|
+
main { padding: 20px 32px; max-width: 1400px; margin: 0 auto; }
|
|
62
|
+
@media (max-width: 768px) { main { padding: 16px; } .filter-bar { position: static; } }
|
|
63
|
+
.section { margin-bottom: 32px; }
|
|
64
|
+
h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
65
|
+
h2 .actions { margin-left: auto; font-size: 12px; font-weight: 400; }
|
|
66
|
+
h3 { font-size: 13px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
|
|
67
|
+
.hint { color: var(--muted); font-size: 12px; font-weight: 400; }
|
|
68
|
+
|
|
69
|
+
.notes { background: var(--surface); border: 1px solid var(--border); border-left: 3px solid var(--yellow); border-radius: 8px; padding: 10px 14px; margin-bottom: 20px; font-size: 13px; }
|
|
70
|
+
.notes ul { margin-left: 18px; }
|
|
71
|
+
.notes:empty, .notes[hidden] { display: none; }
|
|
72
|
+
|
|
73
|
+
.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
74
|
+
.kpi-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; }
|
|
75
|
+
.kpi-card .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
76
|
+
.kpi-card .value { font-size: 26px; font-weight: 700; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
77
|
+
.kpi-card .sub { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
|
78
|
+
.kpi-card.green { border-left: 3px solid var(--green); }
|
|
79
|
+
.kpi-card.red { border-left: 3px solid var(--red); }
|
|
80
|
+
.kpi-card.yellow { border-left: 3px solid var(--yellow); }
|
|
81
|
+
@media (max-width: 600px) { .kpi-card .value { font-size: 22px; } }
|
|
82
|
+
|
|
83
|
+
.filter-bar { position: sticky; top: 0; z-index: 5; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; margin-bottom: 24px; display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; }
|
|
84
|
+
.filter-bar .group { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: center; }
|
|
85
|
+
.filter-bar .group > .name { color: var(--muted); font-size: 12px; font-weight: 600; }
|
|
86
|
+
.chip { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; cursor: pointer; }
|
|
87
|
+
.filter-bar[data-disabled] { opacity: 0.5; pointer-events: none; }
|
|
88
|
+
|
|
89
|
+
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
90
|
+
@media (max-width: 900px) { .grid2 { grid-template-columns: 1fr; } }
|
|
91
|
+
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; min-width: 0; }
|
|
92
|
+
.panel.full { grid-column: 1 / -1; }
|
|
93
|
+
.ec { width: 100%; height: 260px; }
|
|
94
|
+
.ec.tall { height: 320px; }
|
|
95
|
+
.ec-empty { color: var(--muted); font-size: 12px; padding: 24px 0; text-align: center; }
|
|
96
|
+
|
|
97
|
+
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
98
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
99
|
+
thead th { text-align: left; padding: 8px 12px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); border-bottom: 1px solid var(--border); white-space: nowrap; user-select: none; }
|
|
100
|
+
thead th[data-sort] { cursor: pointer; }
|
|
101
|
+
thead th[data-sort]:hover { color: var(--text); }
|
|
102
|
+
tbody tr { border-bottom: 1px solid var(--border); }
|
|
103
|
+
tbody tr:last-child { border-bottom: none; }
|
|
104
|
+
tbody td { padding: 7px 12px; vertical-align: top; }
|
|
105
|
+
tr.clickable { cursor: pointer; }
|
|
106
|
+
tr.clickable:hover { background: var(--surface2); }
|
|
107
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
108
|
+
.pos { color: var(--green); }
|
|
109
|
+
.neg { color: var(--red); }
|
|
110
|
+
.more, .empty { color: var(--muted); font-size: 12px; padding: 8px 12px; }
|
|
111
|
+
.tag { display: inline-flex; padding: 1px 7px; border-radius: 4px; font-size: 11px; background: var(--surface2); color: var(--muted); margin: 0 4px 2px 0; }
|
|
112
|
+
|
|
113
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
|
|
114
|
+
.cards dl { display: grid; grid-template-columns: 1fr auto; gap: 2px 12px; font-size: 13px; }
|
|
115
|
+
.cards dt { color: var(--muted); }
|
|
116
|
+
.cards dd { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
117
|
+
|
|
118
|
+
#scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 20; }
|
|
119
|
+
#drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(560px, 100vw); background: var(--bg); border-left: 1px solid var(--border); z-index: 21; overflow-y: auto; padding: 18px 20px; }
|
|
120
|
+
#drawer[hidden], #scrim[hidden] { display: none; }
|
|
121
|
+
#drawer h2 { margin-bottom: 4px; }
|
|
122
|
+
#drawer .close { margin-left: auto; }
|
|
123
|
+
#drawer ul { margin: 0 0 14px 18px; font-size: 13px; }
|
|
124
|
+
|
|
125
|
+
footer { padding: 16px 32px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--border); display: flex; gap: 12px; flex-wrap: wrap; }
|
|
126
|
+
@media (max-width: 600px) { footer { padding: 12px 16px; } }
|
|
127
|
+
</style>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
<header>
|
|
131
|
+
<div class="logo">akm<span class="logo-version">v%%AKM_VERSION%%</span></div>
|
|
132
|
+
<div class="meta">
|
|
133
|
+
<div class="title">Metrics</div>
|
|
134
|
+
<div>%%WINDOW_HTML%%</div>
|
|
135
|
+
<div>%%FILTERS_HTML%%</div>
|
|
136
|
+
</div>
|
|
137
|
+
</header>
|
|
138
|
+
|
|
139
|
+
<main>
|
|
140
|
+
<div class="notes" id="notes" role="status"></div>
|
|
141
|
+
|
|
142
|
+
<div class="kpi-grid" id="kpis"></div>
|
|
143
|
+
|
|
144
|
+
<div class="filter-bar" id="filterbar" aria-label="Filters">
|
|
145
|
+
<div class="group"><span class="name">From</span><input type="date" id="f-from" aria-label="From date"></div>
|
|
146
|
+
<div class="group"><span class="name">To</span><input type="date" id="f-to" aria-label="To date"></div>
|
|
147
|
+
<div class="group" id="f-bundles"><span class="name">Bundle</span></div>
|
|
148
|
+
<div class="group" id="f-sources"><span class="name">Source</span></div>
|
|
149
|
+
<div class="group" id="f-types"><span class="name">Events</span></div>
|
|
150
|
+
<div class="group"><input type="text" id="f-text" placeholder="Filter by ref or query" aria-label="Filter by ref or query" size="24"></div>
|
|
151
|
+
<button id="f-reset" type="button">Reset</button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div class="section">
|
|
155
|
+
<h2>Usage over time <span class="hint">drag the slider to select a date range</span></h2>
|
|
156
|
+
<div class="panel"><div class="ec tall" id="ch-timeline"></div></div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<div class="section">
|
|
160
|
+
<h2>Top assets <span class="hint">click a row for details · selects are window-wide</span>
|
|
161
|
+
<span class="actions"><button type="button" data-export="assets">Download CSV</button></span></h2>
|
|
162
|
+
<div class="panel"><div class="table-wrap" id="tb-assets"></div></div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<div class="section">
|
|
166
|
+
<h2>Queries</h2>
|
|
167
|
+
<div class="grid2">
|
|
168
|
+
<div class="panel">
|
|
169
|
+
<h3>Top queries <button type="button" data-export="queries" style="float:right">Download CSV</button></h3>
|
|
170
|
+
<div class="table-wrap" id="tb-queries"></div>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="panel">
|
|
173
|
+
<h3>Zero-result queries (content gaps) <button type="button" data-export="zero" style="float:right">Download CSV</button></h3>
|
|
174
|
+
<div class="table-wrap" id="tb-zero"></div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="section">
|
|
180
|
+
<h2>Feedback</h2>
|
|
181
|
+
<div class="grid2">
|
|
182
|
+
<div class="panel"><h3>Per day</h3><div class="ec" id="ch-feedback"></div></div>
|
|
183
|
+
<div class="panel"><h3>By tag</h3><div class="ec" id="ch-tags"></div></div>
|
|
184
|
+
<div class="panel full">
|
|
185
|
+
<h3>Recent negative feedback <button type="button" data-export="negatives" style="float:right">Download CSV</button></h3>
|
|
186
|
+
<div class="table-wrap" id="tb-negatives"></div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class="section">
|
|
192
|
+
<h2>Utility <span class="hint">window-wide; not affected by filters</span></h2>
|
|
193
|
+
<div class="grid2">
|
|
194
|
+
<div class="panel"><h3>Distribution</h3><div class="ec" id="ch-utility"></div><div class="hint" id="utility-never"></div></div>
|
|
195
|
+
<div class="panel">
|
|
196
|
+
<h3>Lowest <button type="button" data-export="lowest" style="float:right">Download CSV</button></h3>
|
|
197
|
+
<div class="table-wrap" id="tb-lowest"></div>
|
|
198
|
+
<h3 style="margin-top:14px">Highest <button type="button" data-export="highest" style="float:right">Download CSV</button></h3>
|
|
199
|
+
<div class="table-wrap" id="tb-highest"></div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<div class="section">
|
|
205
|
+
<h2>LLM usage <span class="hint" id="llm-hint"></span></h2>
|
|
206
|
+
<div class="grid2">
|
|
207
|
+
<div class="panel"><h3>Tokens by stage</h3><div class="ec" id="ch-llm-stage"></div></div>
|
|
208
|
+
<div class="panel"><h3>Tokens by process</h3><div class="ec" id="ch-llm-process"></div></div>
|
|
209
|
+
<div class="panel"><h3>Tokens by engine</h3><div class="ec" id="ch-llm-engine"></div></div>
|
|
210
|
+
<div class="panel"><h3>Calls and mean latency per day</h3><div class="ec" id="ch-llm-time"></div></div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div class="section">
|
|
215
|
+
<h2>Tasks, proposals, workflows <span class="hint">window-wide</span></h2>
|
|
216
|
+
<div class="cards" id="cards"></div>
|
|
217
|
+
</div>
|
|
218
|
+
</main>
|
|
219
|
+
|
|
220
|
+
<footer>
|
|
221
|
+
<span>Generated <time data-iso="%%GENERATED_AT%%">%%GENERATED_AT%%</time></span>
|
|
222
|
+
<span>akm v%%AKM_VERSION%%</span>
|
|
223
|
+
</footer>
|
|
224
|
+
|
|
225
|
+
<div id="scrim" hidden></div>
|
|
226
|
+
<aside id="drawer" role="dialog" aria-modal="true" aria-labelledby="drawer-title" hidden></aside>
|
|
227
|
+
|
|
228
|
+
<script type="application/json" id="akm-data">%%DATA_JSON%%</script>
|
|
229
|
+
<script>
|
|
230
|
+
/* CORE-BEGIN */
|
|
231
|
+
// Pure data functions: no DOM access, so they can be exercised on their own.
|
|
232
|
+
function esc(s) {
|
|
233
|
+
return String(s == null ? '' : s)
|
|
234
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
235
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
236
|
+
}
|
|
237
|
+
function bundleOf(ref) {
|
|
238
|
+
if (!ref) return '';
|
|
239
|
+
var i = ref.indexOf('//');
|
|
240
|
+
return i > 0 ? ref.slice(0, i) : '';
|
|
241
|
+
}
|
|
242
|
+
function dayOf(at) { return String(at).slice(0, 10); }
|
|
243
|
+
function tokensOf(r) {
|
|
244
|
+
return r.totalTokens != null ? r.totalTokens : (r.promptTokens || 0) + (r.completionTokens || 0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Every day from `first` to `last` inclusive (UTC), so a chart axis has no gaps.
|
|
248
|
+
function dayRange(first, last) {
|
|
249
|
+
var out = [];
|
|
250
|
+
var d = Date.parse(first + 'T00:00:00Z');
|
|
251
|
+
var end = Date.parse(last + 'T00:00:00Z');
|
|
252
|
+
if (isNaN(d) || isNaN(end)) return out;
|
|
253
|
+
while (d <= end && out.length < 3660) {
|
|
254
|
+
out.push(new Date(d).toISOString().slice(0, 10));
|
|
255
|
+
d += 86400000;
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// f: { from, to ('YYYY-MM-DD', inclusive), bundles, sources, types (Sets; absent = no filter), text }
|
|
261
|
+
// Rows without a ref (search/curate summary rows) belong to no bundle, so a
|
|
262
|
+
// bundle filter hides them. aggregate() then counts searches from the per-hit
|
|
263
|
+
// rows instead (see its hitMode option).
|
|
264
|
+
function filterRows(rows, f) {
|
|
265
|
+
var q = (f.text || '').trim().toLowerCase();
|
|
266
|
+
return rows.filter(function (r) {
|
|
267
|
+
if (f.types && !f.types.has(r.eventType)) return false;
|
|
268
|
+
if (f.sources && !f.sources.has(r.source)) return false;
|
|
269
|
+
if (f.bundles && !f.bundles.has(bundleOf(r.ref))) return false;
|
|
270
|
+
var d = dayOf(r.at);
|
|
271
|
+
if (f.from && d < f.from) return false;
|
|
272
|
+
if (f.to && d > f.to) return false;
|
|
273
|
+
if (q) {
|
|
274
|
+
var hay = ((r.ref || '') + ' ' + (r.query || '')).toLowerCase();
|
|
275
|
+
if (hay.indexOf(q) < 0) return false;
|
|
276
|
+
}
|
|
277
|
+
return true;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Re-derive the usage and feedback sections from raw usage rows. Show and
|
|
282
|
+
// feedback rows always carry a ref. Per-hit search rows count as `searchHits`
|
|
283
|
+
// for the asset. A search or curate is one unit, counted two ways:
|
|
284
|
+
// - default: from its summary row (no ref);
|
|
285
|
+
// - opts.hitMode: from its per-hit rows, because a bundle or ref filter
|
|
286
|
+
// removes the summary rows. One unit per distinct (type, query, time) group
|
|
287
|
+
// of rows that carry a ref, and no zero-result (a hit means results). This
|
|
288
|
+
// is the rule collect.ts buildUsage applies server-side.
|
|
289
|
+
// Only searches feed the query tables, as on the server.
|
|
290
|
+
function aggregate(rows, opts) {
|
|
291
|
+
var hitMode = !!(opts && opts.hitMode), seenGroups = Object.create(null);
|
|
292
|
+
var daily = new Map(), assets = new Map(), queries = new Map(), tags = Object.create(null), sources = Object.create(null);
|
|
293
|
+
var t = { searches: 0, shows: 0, curates: 0, feedback: 0, positive: 0, negative: 0, zeroResult: 0 };
|
|
294
|
+
var negatives = [];
|
|
295
|
+
rows.forEach(function (r) {
|
|
296
|
+
var d = dayOf(r.at);
|
|
297
|
+
var day = daily.get(d);
|
|
298
|
+
if (!day) { day = { day: d, search: 0, show: 0, curate: 0, feedback: 0, positive: 0, negative: 0 }; daily.set(d, day); }
|
|
299
|
+
sources[r.source] = (sources[r.source] || 0) + 1;
|
|
300
|
+
var a = null;
|
|
301
|
+
if (r.ref) {
|
|
302
|
+
a = assets.get(r.ref);
|
|
303
|
+
if (!a) { a = { ref: r.ref, shows: 0, searchHits: 0, positive: 0, negative: 0, lastUsedAt: r.at }; assets.set(r.ref, a); }
|
|
304
|
+
if (r.at > a.lastUsedAt) a.lastUsedAt = r.at;
|
|
305
|
+
}
|
|
306
|
+
if (r.eventType === 'show') {
|
|
307
|
+
t.shows++; day.show++;
|
|
308
|
+
if (a) a.shows++;
|
|
309
|
+
} else if (r.eventType === 'feedback') {
|
|
310
|
+
t.feedback++; day.feedback++;
|
|
311
|
+
var pos = r.signal === 'positive', neg = r.signal === 'negative';
|
|
312
|
+
if (pos) { t.positive++; day.positive++; }
|
|
313
|
+
if (neg) { t.negative++; day.negative++; negatives.push({ ref: r.ref || '', at: r.at, reason: r.reason || '', tags: r.tags || [] }); }
|
|
314
|
+
if (a) { if (pos) a.positive++; if (neg) a.negative++; }
|
|
315
|
+
(r.tags || []).forEach(function (tag) {
|
|
316
|
+
var e = tags[tag] || (tags[tag] = { positive: 0, negative: 0 });
|
|
317
|
+
if (pos) e.positive++;
|
|
318
|
+
if (neg) e.negative++;
|
|
319
|
+
});
|
|
320
|
+
} else if (r.eventType === 'search' || r.eventType === 'curate') {
|
|
321
|
+
if (r.ref && r.eventType === 'search' && a) a.searchHits++;
|
|
322
|
+
var unit = !r.ref;
|
|
323
|
+
if (hitMode) {
|
|
324
|
+
unit = false;
|
|
325
|
+
if (r.ref) {
|
|
326
|
+
var group = JSON.stringify([r.eventType, r.query || null, r.at]);
|
|
327
|
+
unit = !seenGroups[group];
|
|
328
|
+
seenGroups[group] = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (unit) {
|
|
332
|
+
var rc = hitMode ? null : r.resultCount;
|
|
333
|
+
if (r.eventType === 'search') { t.searches++; day.search++; } else { t.curates++; day.curate++; }
|
|
334
|
+
if (r.eventType === 'search' && rc === 0) t.zeroResult++;
|
|
335
|
+
if (r.eventType === 'search' && r.query) {
|
|
336
|
+
var qe = queries.get(r.query);
|
|
337
|
+
if (!qe) { qe = { query: r.query, count: 0, resultSum: 0, resultN: 0, zero: 0, lastAt: r.at }; queries.set(r.query, qe); }
|
|
338
|
+
qe.count++;
|
|
339
|
+
if (rc != null) { qe.resultSum += rc; qe.resultN++; if (rc === 0) qe.zero++; }
|
|
340
|
+
if (r.at > qe.lastAt) qe.lastAt = r.at;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
var queryRows = Array.from(queries.values()).map(function (q) {
|
|
346
|
+
return { query: q.query, count: q.count, zero: q.zero, avgResults: q.resultN ? q.resultSum / q.resultN : null, lastAt: q.lastAt };
|
|
347
|
+
});
|
|
348
|
+
negatives.sort(function (x, y) { return x.at < y.at ? 1 : x.at > y.at ? -1 : 0; });
|
|
349
|
+
return {
|
|
350
|
+
totals: t,
|
|
351
|
+
daily: Array.from(daily.values()).sort(function (x, y) { return x.day < y.day ? -1 : 1; }),
|
|
352
|
+
assets: Array.from(assets.values()),
|
|
353
|
+
queries: queryRows,
|
|
354
|
+
zeroQueries: queryRows.filter(function (q) { return q.zero > 0; })
|
|
355
|
+
.map(function (q) { return { query: q.query, count: q.zero, avgResults: 0, lastAt: q.lastAt }; }),
|
|
356
|
+
tags: tags,
|
|
357
|
+
sources: sources,
|
|
358
|
+
negatives: negatives
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// LLM rows are per-day sums (calls, durationMs and tokens already added up)
|
|
363
|
+
// and carry no ref, source or event type, so only the date range applies.
|
|
364
|
+
function aggregateLlm(rows, f) {
|
|
365
|
+
var by = { stage: Object.create(null), process: Object.create(null), engine: Object.create(null) }, daily = new Map();
|
|
366
|
+
var t = { calls: 0, tokens: 0, failures: 0, durationMs: 0 };
|
|
367
|
+
rows.forEach(function (r) {
|
|
368
|
+
var d = dayOf(r.day);
|
|
369
|
+
if (f.from && d < f.from) return;
|
|
370
|
+
if (f.to && d > f.to) return;
|
|
371
|
+
var tok = tokensOf(r), calls = r.calls || 0, failed = r.outcome === 'error' ? calls : 0;
|
|
372
|
+
t.calls += calls; t.tokens += tok; t.durationMs += r.durationMs || 0; t.failures += failed;
|
|
373
|
+
[['stage', r.stage], ['process', r.process], ['engine', r.engine]].forEach(function (p) {
|
|
374
|
+
var k = p[1] || 'unattributed';
|
|
375
|
+
var e = by[p[0]][k] || (by[p[0]][k] = { name: k, calls: 0, tokens: 0, failures: 0 });
|
|
376
|
+
e.calls += calls; e.tokens += tok; e.failures += failed;
|
|
377
|
+
});
|
|
378
|
+
var day = daily.get(d) || { day: d, calls: 0, durationMs: 0 };
|
|
379
|
+
day.calls += calls; day.durationMs += r.durationMs || 0;
|
|
380
|
+
daily.set(d, day);
|
|
381
|
+
});
|
|
382
|
+
function list(o) {
|
|
383
|
+
return Object.keys(o).map(function (k) { return o[k]; })
|
|
384
|
+
.sort(function (x, y) { return y.tokens - x.tokens || (x.name < y.name ? -1 : 1); });
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
totals: t,
|
|
388
|
+
byStage: list(by.stage), byProcess: list(by.process), byEngine: list(by.engine),
|
|
389
|
+
daily: Array.from(daily.values()).sort(function (x, y) { return x.day < y.day ? -1 : 1; })
|
|
390
|
+
.map(function (d) { return { day: d.day, calls: d.calls, avgMs: d.calls ? Math.round(d.durationMs / d.calls) : 0 }; })
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Nulls and undefined sort last in either direction.
|
|
395
|
+
function sortRows(rows, key, dir) {
|
|
396
|
+
var sign = dir === 'asc' ? 1 : -1;
|
|
397
|
+
return rows.slice().sort(function (a, b) {
|
|
398
|
+
var x = a[key], y = b[key];
|
|
399
|
+
var xn = x == null, yn = y == null;
|
|
400
|
+
if (xn || yn) return xn && yn ? 0 : xn ? 1 : -1;
|
|
401
|
+
if (typeof x === 'number' && typeof y === 'number') return (x - y) * sign;
|
|
402
|
+
return String(x).localeCompare(String(y)) * sign;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// RFC 4180 quoting. A text cell that a spreadsheet would read as a formula
|
|
407
|
+
// gets a leading apostrophe.
|
|
408
|
+
function csvCell(v) {
|
|
409
|
+
if (v == null) return '';
|
|
410
|
+
var s = Array.isArray(v) ? v.join('; ') : String(v);
|
|
411
|
+
if (typeof v === 'string' && /^[=+\-@\t\r]/.test(s)) s = "'" + s;
|
|
412
|
+
return /[",\n\r]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
413
|
+
}
|
|
414
|
+
function toCsv(columns, rows) {
|
|
415
|
+
var lines = [columns.map(function (c) { return csvCell(c.label); }).join(',')];
|
|
416
|
+
rows.forEach(function (r) { lines.push(columns.map(function (c) { return csvCell(r[c.key]); }).join(',')); });
|
|
417
|
+
return lines.join('\n');
|
|
418
|
+
}
|
|
419
|
+
/* CORE-END */
|
|
420
|
+
|
|
421
|
+
(function () {
|
|
422
|
+
'use strict';
|
|
423
|
+
var DATA = JSON.parse(document.getElementById('akm-data').textContent);
|
|
424
|
+
var QUERIES = (DATA.rows && DATA.rows.queries) || [];
|
|
425
|
+
// Per-hit rows carry their query as an index into rows.queries; put the text back.
|
|
426
|
+
var ALL = ((DATA.rows && DATA.rows.usage) || []).map(function (r) {
|
|
427
|
+
if (r.q == null) return r;
|
|
428
|
+
var row = Object.assign({}, r, { query: QUERIES[r.q] });
|
|
429
|
+
delete row.q;
|
|
430
|
+
return row;
|
|
431
|
+
});
|
|
432
|
+
var LLM = (DATA.rows && DATA.rows.llm) || [];
|
|
433
|
+
var HAS_ROWS = ALL.length > 0;
|
|
434
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
435
|
+
var EVENT_TYPES = ['search', 'show', 'curate', 'feedback'];
|
|
436
|
+
|
|
437
|
+
// ── Formatting ───────────────────────────────────────────────────────────
|
|
438
|
+
function fmtN(n) { return n == null ? '—' : Number(n).toLocaleString(); }
|
|
439
|
+
function fmtPct(n) { return n == null ? '—' : (n * 100).toFixed(1) + '%'; }
|
|
440
|
+
function fmtMs(n) {
|
|
441
|
+
if (n == null) return '—';
|
|
442
|
+
if (n < 1000) return Math.round(n) + ' ms';
|
|
443
|
+
if (n < 60000) return (n / 1000).toFixed(1) + ' s';
|
|
444
|
+
return (n / 60000).toFixed(1) + ' min';
|
|
445
|
+
}
|
|
446
|
+
function fmtNum2(n) { return n == null ? '—' : Number(n).toFixed(2); }
|
|
447
|
+
function timeTag(iso) {
|
|
448
|
+
return iso ? '<time data-iso="' + esc(iso) + '">' + esc(String(iso).slice(0, 16).replace('T', ' ')) + '</time>' : '—';
|
|
449
|
+
}
|
|
450
|
+
function localize(root) {
|
|
451
|
+
var pad = function (n) { return String(n).padStart(2, '0'); };
|
|
452
|
+
root.querySelectorAll('time[data-iso]').forEach(function (el) {
|
|
453
|
+
var d = new Date(el.dataset.iso);
|
|
454
|
+
if (!isNaN(d)) el.textContent = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// ECharts tooltips are HTML; escape every data-derived string (category
|
|
458
|
+
// names are queries, tags and stage names). `marker` is ECharts' own markup.
|
|
459
|
+
function tipFormat(params) {
|
|
460
|
+
var list = Array.isArray(params) ? params : [params];
|
|
461
|
+
return esc(list[0].axisValueLabel || list[0].name) + '<br>' + list.map(function (p) {
|
|
462
|
+
return p.marker + esc(p.seriesName) + ': ' + esc(Array.isArray(p.value) ? p.value[p.value.length - 1] : p.value);
|
|
463
|
+
}).join('<br>');
|
|
464
|
+
}
|
|
465
|
+
function css(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
|
|
466
|
+
|
|
467
|
+
// ── State ────────────────────────────────────────────────────────────────
|
|
468
|
+
var universe = {
|
|
469
|
+
bundles: Array.from(new Set(ALL.map(function (r) { return bundleOf(r.ref); }))).sort(),
|
|
470
|
+
sources: Array.from(new Set(ALL.map(function (r) { return r.source; }))).sort()
|
|
471
|
+
};
|
|
472
|
+
var state = {
|
|
473
|
+
from: '', to: '', text: '',
|
|
474
|
+
bundles: new Set(universe.bundles),
|
|
475
|
+
sources: new Set(universe.sources),
|
|
476
|
+
types: new Set(EVENT_TYPES),
|
|
477
|
+
sorts: {}
|
|
478
|
+
};
|
|
479
|
+
var tables = {};
|
|
480
|
+
var utilityByRef = {}, outcomeByRef = {}, selectsByRef = {}, valenceByRef = {};
|
|
481
|
+
((DATA.utility && DATA.utility.lowest) || []).concat((DATA.utility && DATA.utility.highest) || [])
|
|
482
|
+
.forEach(function (u) { utilityByRef[u.ref] = u; });
|
|
483
|
+
((DATA.outcomes && DATA.outcomes.lowestOutcome) || []).forEach(function (o) { outcomeByRef[o.ref] = o; });
|
|
484
|
+
((DATA.usage && DATA.usage.topAssets) || []).forEach(function (a) { selectsByRef[a.ref] = a.selects; });
|
|
485
|
+
((DATA.feedback && DATA.feedback.byAsset) || []).forEach(function (a) { valenceByRef[a.ref] = a.valence; });
|
|
486
|
+
|
|
487
|
+
function activeFilter(withDates) {
|
|
488
|
+
var f = { text: state.text };
|
|
489
|
+
if (withDates) { f.from = state.from; f.to = state.to; }
|
|
490
|
+
if (state.bundles.size !== universe.bundles.length) f.bundles = state.bundles;
|
|
491
|
+
if (state.sources.size !== universe.sources.length) f.sources = state.sources;
|
|
492
|
+
if (state.types.size !== EVENT_TYPES.length) f.types = state.types;
|
|
493
|
+
return f;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// The server dropped the summary rows when it filtered by bundle or ref, and a
|
|
497
|
+
// bundle chip subset drops them here; either way searches are counted from hits.
|
|
498
|
+
function hitMode() {
|
|
499
|
+
var f = DATA.filters || {};
|
|
500
|
+
return !!((f.bundles && f.bundles.length > 0) || f.ref || state.bundles.size !== universe.bundles.length);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── Charts ───────────────────────────────────────────────────────────────
|
|
504
|
+
var charts = {};
|
|
505
|
+
function chart(id, option, onReady) {
|
|
506
|
+
var el = $(id);
|
|
507
|
+
if (!window.echarts) {
|
|
508
|
+
el.innerHTML = '<div class="ec-empty">Charts need the ECharts library from the CDN; this page could not load it.</div>';
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
var c = charts[id] || echarts.getInstanceByDom(el);
|
|
512
|
+
if (!c) { c = echarts.init(el, null, { renderer: 'canvas' }); charts[id] = c; if (onReady) onReady(c); }
|
|
513
|
+
c.setOption(Object.assign({
|
|
514
|
+
backgroundColor: 'transparent',
|
|
515
|
+
textStyle: { color: css('--muted') },
|
|
516
|
+
tooltip: { trigger: 'axis', formatter: tipFormat, backgroundColor: css('--surface'), borderColor: css('--border'), textStyle: { color: css('--text') } },
|
|
517
|
+
grid: { left: 48, right: 16, top: 28, bottom: 28, containLabel: false }
|
|
518
|
+
}, option), true);
|
|
519
|
+
return c;
|
|
520
|
+
}
|
|
521
|
+
function axisStyle() {
|
|
522
|
+
return { axisLine: { lineStyle: { color: css('--border') } }, axisLabel: { color: css('--muted') }, splitLine: { lineStyle: { color: css('--surface2') } } };
|
|
523
|
+
}
|
|
524
|
+
function emptyChart(id, text) {
|
|
525
|
+
var c = charts[id];
|
|
526
|
+
if (c) { c.dispose(); delete charts[id]; }
|
|
527
|
+
$(id).innerHTML = '<div class="ec-empty">' + esc(text) + '</div>';
|
|
528
|
+
}
|
|
529
|
+
function hasData(id, any, text) {
|
|
530
|
+
if (!any) { emptyChart(id, text || 'No data in the current view'); return false; }
|
|
531
|
+
if (!charts[id]) $(id).innerHTML = '';
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
var TYPE_COLORS = function () { return { search: css('--accent'), show: css('--green'), curate: css('--purple'), feedback: css('--yellow') }; };
|
|
536
|
+
|
|
537
|
+
function renderTimeline() {
|
|
538
|
+
var agg = aggregate(filterRows(ALL, activeFilter(false)), { hitMode: hitMode() });
|
|
539
|
+
if (!hasData('ch-timeline', agg.daily.length > 0)) return;
|
|
540
|
+
var days = dayRange(agg.daily[0].day, agg.daily[agg.daily.length - 1].day);
|
|
541
|
+
var byDay = {};
|
|
542
|
+
agg.daily.forEach(function (d) { byDay[d.day] = d; });
|
|
543
|
+
var colors = TYPE_COLORS();
|
|
544
|
+
// A stacked area with hidden symbols draws nothing for a single day, so
|
|
545
|
+
// one day renders as a stacked bar instead.
|
|
546
|
+
var single = days.length === 1;
|
|
547
|
+
var series = EVENT_TYPES.map(function (t) {
|
|
548
|
+
return { name: t, type: single ? 'bar' : 'line', stack: 'events', areaStyle: { opacity: 0.55 }, showSymbol: false,
|
|
549
|
+
lineStyle: { width: 1 }, itemStyle: { color: colors[t] },
|
|
550
|
+
data: days.map(function (d) { return byDay[d] ? byDay[d][t] : 0; }) };
|
|
551
|
+
});
|
|
552
|
+
var startIdx = state.from ? Math.max(0, days.findIndex(function (d) { return d >= state.from; })) : 0;
|
|
553
|
+
var endIdx = days.length - 1;
|
|
554
|
+
if (state.to) { for (var i = days.length - 1; i >= 0; i--) { if (days[i] <= state.to) { endIdx = i; break; } } }
|
|
555
|
+
chart('ch-timeline', {
|
|
556
|
+
legend: { top: 0, textStyle: { color: css('--muted') } },
|
|
557
|
+
grid: { left: 48, right: 16, top: 32, bottom: 64 },
|
|
558
|
+
xAxis: Object.assign({ type: 'category', boundaryGap: single, data: days }, axisStyle()),
|
|
559
|
+
yAxis: Object.assign({ type: 'value', minInterval: 1 }, axisStyle()),
|
|
560
|
+
dataZoom: [
|
|
561
|
+
{ type: 'slider', height: 22, bottom: 8, startValue: startIdx, endValue: endIdx },
|
|
562
|
+
{ type: 'inside', startValue: startIdx, endValue: endIdx }
|
|
563
|
+
],
|
|
564
|
+
series: series
|
|
565
|
+
}, function (c) {
|
|
566
|
+
c.on('datazoom', function () {
|
|
567
|
+
var opt = c.getOption();
|
|
568
|
+
var dz = opt.dataZoom && opt.dataZoom[0];
|
|
569
|
+
var cats = opt.xAxis[0].data;
|
|
570
|
+
if (!dz || !cats.length) return;
|
|
571
|
+
var s = Math.max(0, Math.round(dz.startValue)), e = Math.min(cats.length - 1, Math.round(dz.endValue));
|
|
572
|
+
state.from = s === 0 ? '' : cats[s];
|
|
573
|
+
state.to = e === cats.length - 1 ? '' : cats[e];
|
|
574
|
+
$('f-from').value = state.from;
|
|
575
|
+
$('f-to').value = state.to;
|
|
576
|
+
render({ skipTimeline: true });
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function barOption(names, series, horizontal) {
|
|
582
|
+
var cat = Object.assign({ type: 'category', data: names }, axisStyle());
|
|
583
|
+
var val = Object.assign({ type: 'value', minInterval: 1 }, axisStyle());
|
|
584
|
+
return {
|
|
585
|
+
legend: series.length > 1 ? { top: 0, textStyle: { color: css('--muted') } } : undefined,
|
|
586
|
+
grid: horizontal ? { left: 110, right: 16, top: 28, bottom: 24 } : { left: 48, right: 16, top: 28, bottom: 36 },
|
|
587
|
+
xAxis: horizontal ? val : cat,
|
|
588
|
+
yAxis: horizontal ? Object.assign({}, cat, { inverse: true }) : val,
|
|
589
|
+
series: series
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function renderFeedbackCharts(agg) {
|
|
594
|
+
if (hasData('ch-feedback', agg.daily.some(function (d) { return d.feedback > 0; }), 'No feedback in the current view')) {
|
|
595
|
+
var days = agg.daily.map(function (d) { return d.day; });
|
|
596
|
+
chart('ch-feedback', barOption(days, [
|
|
597
|
+
{ name: 'positive', type: 'bar', stack: 'f', itemStyle: { color: css('--green') }, data: agg.daily.map(function (d) { return d.positive; }) },
|
|
598
|
+
{ name: 'negative', type: 'bar', stack: 'f', itemStyle: { color: css('--red') }, data: agg.daily.map(function (d) { return d.negative; }) }
|
|
599
|
+
]));
|
|
600
|
+
}
|
|
601
|
+
var tagNames = Object.keys(agg.tags).sort(function (a, b) {
|
|
602
|
+
return (agg.tags[b].positive + agg.tags[b].negative) - (agg.tags[a].positive + agg.tags[a].negative) || (a < b ? -1 : 1);
|
|
603
|
+
}).slice(0, 15);
|
|
604
|
+
if (hasData('ch-tags', tagNames.length > 0, 'No tagged feedback in the current view')) {
|
|
605
|
+
chart('ch-tags', barOption(tagNames, [
|
|
606
|
+
{ name: 'positive', type: 'bar', stack: 't', itemStyle: { color: css('--green') }, data: tagNames.map(function (t) { return agg.tags[t].positive; }) },
|
|
607
|
+
{ name: 'negative', type: 'bar', stack: 't', itemStyle: { color: css('--red') }, data: tagNames.map(function (t) { return agg.tags[t].negative; }) }
|
|
608
|
+
], true));
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function renderUtilityChart() {
|
|
613
|
+
var h = (DATA.utility && DATA.utility.histogram) || [];
|
|
614
|
+
if (!hasData('ch-utility', h.some(function (b) { return b.count > 0; }), 'No utility scores')) return;
|
|
615
|
+
chart('ch-utility', barOption(h.map(function (b) { return b.bucket; }),
|
|
616
|
+
[{ name: 'assets', type: 'bar', itemStyle: { color: css('--accent') }, data: h.map(function (b) { return b.count; }) }]));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function renderLlm() {
|
|
620
|
+
var useRows = LLM.length > 0;
|
|
621
|
+
var agg = useRows ? aggregateLlm(LLM, { from: state.from, to: state.to }) : null;
|
|
622
|
+
$('llm-hint').textContent = useRows ? 'follows the date range only' : 'window-wide aggregate (no per-call rows)';
|
|
623
|
+
function fromAggregate(o) {
|
|
624
|
+
return Object.keys(o || {}).map(function (k) { return { name: k, tokens: o[k].totalTokens, calls: o[k].calls, failures: o[k].failures }; })
|
|
625
|
+
.sort(function (a, b) { return b.tokens - a.tokens; });
|
|
626
|
+
}
|
|
627
|
+
var dims = [
|
|
628
|
+
['ch-llm-stage', useRows ? agg.byStage : fromAggregate(DATA.llm && DATA.llm.byStage)],
|
|
629
|
+
['ch-llm-process', useRows ? agg.byProcess : fromAggregate(DATA.llm && DATA.llm.byProcess)],
|
|
630
|
+
['ch-llm-engine', useRows ? agg.byEngine : fromAggregate(DATA.llm && DATA.llm.byEngine)]
|
|
631
|
+
];
|
|
632
|
+
dims.forEach(function (d) {
|
|
633
|
+
var list = d[1].slice(0, 15);
|
|
634
|
+
if (!hasData(d[0], list.length > 0, 'No LLM calls recorded')) return;
|
|
635
|
+
chart(d[0], Object.assign(barOption(list.map(function (x) { return x.name; }),
|
|
636
|
+
[{ name: 'tokens', type: 'bar', itemStyle: { color: css('--purple') }, data: list.map(function (x) { return x.tokens; }) }], true), {}));
|
|
637
|
+
});
|
|
638
|
+
if (hasData('ch-llm-time', useRows && agg.daily.length > 0, 'No per-call rows in this report')) {
|
|
639
|
+
var days = agg.daily.map(function (d) { return d.day; });
|
|
640
|
+
chart('ch-llm-time', {
|
|
641
|
+
legend: { top: 0, textStyle: { color: css('--muted') } },
|
|
642
|
+
grid: { left: 48, right: 56, top: 32, bottom: 36 },
|
|
643
|
+
xAxis: Object.assign({ type: 'category', data: days }, axisStyle()),
|
|
644
|
+
yAxis: [Object.assign({ type: 'value', name: 'calls', minInterval: 1 }, axisStyle()),
|
|
645
|
+
Object.assign({ type: 'value', name: 'ms' }, axisStyle(), { splitLine: { show: false } })],
|
|
646
|
+
series: [
|
|
647
|
+
{ name: 'calls', type: 'bar', itemStyle: { color: css('--accent') }, data: agg.daily.map(function (d) { return d.calls; }) },
|
|
648
|
+
{ name: 'mean latency (ms)', type: 'line', yAxisIndex: 1, itemStyle: { color: css('--yellow') }, data: agg.daily.map(function (d) { return d.avgMs; }) }
|
|
649
|
+
]
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return agg;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ── Tables ───────────────────────────────────────────────────────────────
|
|
656
|
+
var LIMIT = 50;
|
|
657
|
+
function renderTable(name, el, columns, rows, opts) {
|
|
658
|
+
opts = opts || {};
|
|
659
|
+
var st = state.sorts[name] || (state.sorts[name] = { key: opts.sortKey || columns[0].key, dir: opts.sortDir || 'desc' });
|
|
660
|
+
var sorted = sortRows(rows, st.key, st.dir);
|
|
661
|
+
tables[name] = { columns: columns, rows: sorted, file: opts.file || name };
|
|
662
|
+
if (sorted.length === 0) { el.innerHTML = '<div class="empty">' + esc(opts.empty || 'No data in the current view') + '</div>'; return; }
|
|
663
|
+
var shown = sorted.slice(0, opts.limit || LIMIT);
|
|
664
|
+
var head = columns.map(function (c) {
|
|
665
|
+
var active = c.key === st.key;
|
|
666
|
+
return '<th scope="col" tabindex="0" data-sort="' + esc(c.key) + '" class="' + (c.num ? 'num' : '') + '" aria-sort="' +
|
|
667
|
+
(active ? (st.dir === 'asc' ? 'ascending' : 'descending') : 'none') + '">' + esc(c.label) +
|
|
668
|
+
(active ? (st.dir === 'asc' ? ' ▲' : ' ▼') : '') + '</th>';
|
|
669
|
+
}).join('');
|
|
670
|
+
var body = shown.map(function (r) {
|
|
671
|
+
var attrs = opts.clickRef && r.ref ? ' class="clickable" tabindex="0" data-ref="' + esc(r.ref) + '"' : '';
|
|
672
|
+
return '<tr' + attrs + '>' + columns.map(function (c) {
|
|
673
|
+
var cell = c.html ? c.html(r) : esc(c.fmt ? c.fmt(r[c.key], r) : r[c.key]);
|
|
674
|
+
return '<td class="' + (c.num ? 'num ' : '') + (c.cls ? c.cls(r) : '') + '">' + cell + '</td>';
|
|
675
|
+
}).join('') + '</tr>';
|
|
676
|
+
}).join('');
|
|
677
|
+
el.innerHTML = '<table><thead><tr>' + head + '</tr></thead><tbody>' + body + '</tbody></table>' +
|
|
678
|
+
(sorted.length > shown.length ? '<div class="more">Showing ' + shown.length + ' of ' + sorted.length + '. Download CSV for all rows.</div>' : '');
|
|
679
|
+
localize(el);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
var refCell = function (r) { return '<code>' + esc(r.ref) + '</code>'; };
|
|
683
|
+
var COLS = {
|
|
684
|
+
assets: [
|
|
685
|
+
{ key: 'ref', label: 'Asset', html: refCell },
|
|
686
|
+
{ key: 'shows', label: 'Shows', num: true, fmt: fmtN },
|
|
687
|
+
{ key: 'searchHits', label: 'Search hits', num: true, fmt: fmtN },
|
|
688
|
+
{ key: 'selects', label: 'Selects', num: true, fmt: fmtN },
|
|
689
|
+
{ key: 'selectRate', label: 'Select rate', num: true, fmt: fmtPct },
|
|
690
|
+
{ key: 'positive', label: 'Feedback +', num: true, fmt: fmtN, cls: function (r) { return r.positive ? 'pos' : ''; } },
|
|
691
|
+
{ key: 'negative', label: 'Feedback −', num: true, fmt: fmtN, cls: function (r) { return r.negative ? 'neg' : ''; } },
|
|
692
|
+
{ key: 'utility', label: 'Utility', num: true, fmt: fmtNum2 },
|
|
693
|
+
{ key: 'lastUsedAt', label: 'Last used', html: function (r) { return timeTag(r.lastUsedAt); } }
|
|
694
|
+
],
|
|
695
|
+
queries: [
|
|
696
|
+
{ key: 'query', label: 'Query' },
|
|
697
|
+
{ key: 'count', label: 'Count', num: true, fmt: fmtN },
|
|
698
|
+
{ key: 'avgResults', label: 'Avg results', num: true, fmt: function (v) { return v == null ? '—' : v.toFixed(1); } },
|
|
699
|
+
{ key: 'lastAt', label: 'Last', html: function (r) { return timeTag(r.lastAt); } }
|
|
700
|
+
],
|
|
701
|
+
negatives: [
|
|
702
|
+
{ key: 'at', label: 'When', html: function (r) { return timeTag(r.at); } },
|
|
703
|
+
{ key: 'ref', label: 'Asset', html: refCell },
|
|
704
|
+
{ key: 'reason', label: 'Reason' },
|
|
705
|
+
{ key: 'tags', label: 'Tags', html: function (r) { return (r.tags || []).map(function (t) { return '<span class="tag">' + esc(t) + '</span>'; }).join(''); } }
|
|
706
|
+
],
|
|
707
|
+
utility: [
|
|
708
|
+
{ key: 'ref', label: 'Asset', html: refCell },
|
|
709
|
+
{ key: 'utility', label: 'Utility', num: true, fmt: fmtNum2 },
|
|
710
|
+
{ key: 'showCount', label: 'Shows', num: true, fmt: fmtN },
|
|
711
|
+
{ key: 'searchCount', label: 'Searches', num: true, fmt: fmtN },
|
|
712
|
+
{ key: 'selectRate', label: 'Select rate', num: true, fmt: fmtPct }
|
|
713
|
+
]
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
function assetRows(agg) {
|
|
717
|
+
return agg.assets.map(function (a) {
|
|
718
|
+
var selects = selectsByRef[a.ref];
|
|
719
|
+
var u = utilityByRef[a.ref];
|
|
720
|
+
return Object.assign({}, a, {
|
|
721
|
+
selects: selects == null ? null : selects,
|
|
722
|
+
selectRate: selects != null && a.searchHits > 0 ? selects / a.searchHits : null,
|
|
723
|
+
utility: u ? u.utility : null
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
729
|
+
function fallbackAggregate() {
|
|
730
|
+
var u = DATA.usage || {};
|
|
731
|
+
var q = function (x) { return { query: x.query, count: x.count, avgResults: x.avgResults, lastAt: x.lastAt }; };
|
|
732
|
+
return {
|
|
733
|
+
totals: { searches: u.totals.searches, shows: u.totals.shows, curates: u.totals.curates, feedback: DATA.feedback.totals.positive + DATA.feedback.totals.negative,
|
|
734
|
+
positive: DATA.feedback.totals.positive, negative: DATA.feedback.totals.negative, zeroResult: u.totals.zeroResultSearches },
|
|
735
|
+
daily: (u.daily || []).map(function (d) { return Object.assign({ positive: 0, negative: 0 }, d); }),
|
|
736
|
+
assets: (u.topAssets || []).map(function (a) { return { ref: a.ref, shows: a.shows, searchHits: a.searchHits, positive: a.positive, negative: a.negative, lastUsedAt: a.lastUsedAt }; }),
|
|
737
|
+
queries: (u.topQueries || []).map(q), zeroQueries: (u.zeroResultQueries || []).map(q),
|
|
738
|
+
tags: DATA.feedback.byTag || {}, sources: u.bySource || {},
|
|
739
|
+
negatives: (DATA.feedback.recentNegative || []).map(function (n) { return { ref: n.ref, at: n.at, reason: n.reason || '', tags: n.tags || [] }; })
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderKpis(agg, llm) {
|
|
744
|
+
var t = agg.totals;
|
|
745
|
+
var tokens = llm ? llm.totals.tokens : DATA.llm.totalTokens;
|
|
746
|
+
var calls = llm ? llm.totals.calls : DATA.llm.calls;
|
|
747
|
+
var tasks = DATA.tasks || {};
|
|
748
|
+
var cards = [
|
|
749
|
+
['Searches', fmtN(t.searches), fmtN(t.curates) + ' curates', ''],
|
|
750
|
+
['Shows', fmtN(t.shows), '', ''],
|
|
751
|
+
['Select rate', fmtPct(DATA.usage.selectRate), 'window-wide', ''],
|
|
752
|
+
['Zero-result rate', fmtPct(t.searches ? t.zeroResult / t.searches : null), fmtN(t.zeroResult) + ' searches', t.zeroResult ? 'yellow' : ''],
|
|
753
|
+
['Feedback', '<span class="pos">+' + fmtN(t.positive) + '</span> / <span class="neg">−' + fmtN(t.negative) + '</span>',
|
|
754
|
+
'net ' + (t.positive - t.negative >= 0 ? '+' : '−') + Math.abs(t.positive - t.negative), ''],
|
|
755
|
+
['LLM tokens', fmtN(tokens), fmtN(calls) + ' calls', ''],
|
|
756
|
+
['Task fail rate', fmtPct(tasks.failRate), fmtN(tasks.failed) + ' of ' + fmtN(tasks.runs) + ' runs, window-wide', tasks.failed ? 'red' : '']
|
|
757
|
+
];
|
|
758
|
+
$('kpis').innerHTML = cards.map(function (c) {
|
|
759
|
+
return '<div class="kpi-card ' + c[3] + '"><div class="label">' + esc(c[0]) + '</div><div class="value">' + c[1] + '</div><div class="sub">' + esc(c[2]) + '</div></div>';
|
|
760
|
+
}).join('');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function renderCards() {
|
|
764
|
+
function dl(pairs) {
|
|
765
|
+
return '<dl>' + pairs.map(function (p) { return '<dt>' + esc(p[0]) + '</dt><dd>' + esc(p[1]) + '</dd>'; }).join('') + '</dl>';
|
|
766
|
+
}
|
|
767
|
+
function kv(o, fmt) {
|
|
768
|
+
var keys = Object.keys(o || {}).sort();
|
|
769
|
+
return keys.length ? dl(keys.map(function (k) { return [k, (fmt || fmtN)(o[k])]; })) : '<div class="empty">none</div>';
|
|
770
|
+
}
|
|
771
|
+
var tk = DATA.tasks || {}, pr = DATA.proposals || {}, wf = DATA.workflows || {}, ix = DATA.index || {};
|
|
772
|
+
var html = '';
|
|
773
|
+
html += '<div class="panel"><h3>Tasks</h3>' + dl([['Runs', fmtN(tk.runs)], ['Failed', fmtN(tk.failed)], ['Fail rate', fmtPct(tk.failRate)]]) +
|
|
774
|
+
'<div class="table-wrap" id="tb-tasks" style="margin-top:10px"></div></div>';
|
|
775
|
+
html += '<div class="panel"><h3>Proposals</h3>' + kv(pr.byStatus) +
|
|
776
|
+
'<div class="table-wrap" id="tb-accept" style="margin-top:10px"></div></div>';
|
|
777
|
+
html += '<div class="panel"><h3>Workflows</h3>' + dl([['Runs', fmtN(wf.runs)], ['Tokens', fmtN(wf.tokens)]]) +
|
|
778
|
+
'<h3 style="margin-top:10px">By status</h3>' + kv(wf.byStatus) + '<h3 style="margin-top:10px">Tokens by model</h3>' + kv(wf.byModel) + '</div>';
|
|
779
|
+
html += '<div class="panel"><h3>Index runs</h3>' + dl([['Runs', fmtN(ix.runs)], ['Median time', fmtMs(ix.medianMs)]]) + '</div>';
|
|
780
|
+
$('cards').innerHTML = html;
|
|
781
|
+
renderTable('tasks', $('tb-tasks'), [
|
|
782
|
+
{ key: 'taskId', label: 'Task' }, { key: 'runs', label: 'Runs', num: true, fmt: fmtN },
|
|
783
|
+
{ key: 'failed', label: 'Failed', num: true, fmt: fmtN }, { key: 'medianMs', label: 'Median', num: true, fmt: fmtMs }
|
|
784
|
+
], tk.byTask || [], { sortKey: 'runs', file: 'tasks' });
|
|
785
|
+
renderTable('accept', $('tb-accept'), [
|
|
786
|
+
{ key: 'source', label: 'Source' }, { key: 'total', label: 'Total', num: true, fmt: fmtN },
|
|
787
|
+
{ key: 'accepted', label: 'Accepted', num: true, fmt: fmtN }, { key: 'rejected', label: 'Rejected', num: true, fmt: fmtN },
|
|
788
|
+
{ key: 'acceptRate', label: 'Accept rate', num: true, fmt: fmtPct }
|
|
789
|
+
], pr.acceptRateBySource || [], { sortKey: 'total', file: 'proposals' });
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function render(opts) {
|
|
793
|
+
opts = opts || {};
|
|
794
|
+
var agg = HAS_ROWS ? aggregate(filterRows(ALL, activeFilter(true)), { hitMode: hitMode() }) : fallbackAggregate();
|
|
795
|
+
var llm = renderLlm();
|
|
796
|
+
renderKpis(agg, llm);
|
|
797
|
+
if (!opts.skipTimeline) renderTimeline();
|
|
798
|
+
renderTable('assets', $('tb-assets'), COLS.assets, assetRows(agg), { sortKey: 'shows', clickRef: true, file: 'top-assets' });
|
|
799
|
+
renderTable('queries', $('tb-queries'), COLS.queries, agg.queries, { sortKey: 'count', file: 'queries' });
|
|
800
|
+
renderTable('zero', $('tb-zero'), COLS.queries.slice(0, 2).concat(COLS.queries.slice(3)), agg.zeroQueries, { sortKey: 'count', file: 'zero-result-queries', empty: 'No zero-result searches in the current view' });
|
|
801
|
+
renderTable('negatives', $('tb-negatives'), COLS.negatives, agg.negatives, { sortKey: 'at', clickRef: true, file: 'negative-feedback', empty: 'No negative feedback in the current view' });
|
|
802
|
+
renderFeedbackCharts(agg);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function renderStatic() {
|
|
806
|
+
var u = DATA.utility || {};
|
|
807
|
+
renderUtilityChart();
|
|
808
|
+
$('utility-never').textContent = fmtN(u.neverUsed) + ' indexed assets have never been used; ' + fmtN(u.count) + ' have a utility score.';
|
|
809
|
+
renderTable('lowest', $('tb-lowest'), COLS.utility, u.lowest || [], { sortKey: 'utility', sortDir: 'asc', clickRef: true, file: 'utility-lowest', empty: 'No utility scores' });
|
|
810
|
+
renderTable('highest', $('tb-highest'), COLS.utility, u.highest || [], { sortKey: 'utility', sortDir: 'desc', clickRef: true, file: 'utility-highest', empty: 'No utility scores' });
|
|
811
|
+
renderCards();
|
|
812
|
+
var notes = (DATA.notes || []).slice();
|
|
813
|
+
if (!HAS_ROWS) notes.push('This report carries no raw rows, so filters and drill-down are unavailable and the aggregates below are the command’s own.');
|
|
814
|
+
$('notes').innerHTML = notes.length ? '<ul>' + notes.map(function (n) { return '<li>' + esc(n) + '</li>'; }).join('') + '</ul>' : '';
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Asset drawer ─────────────────────────────────────────────────────────
|
|
818
|
+
var lastFocus = null;
|
|
819
|
+
function openDrawer(ref) {
|
|
820
|
+
lastFocus = document.activeElement;
|
|
821
|
+
var rows = ALL.filter(function (r) { return r.ref === ref; });
|
|
822
|
+
var agg = aggregate(rows, { hitMode: true });
|
|
823
|
+
var a = agg.assets[0] || { shows: 0, searchHits: 0, positive: 0, negative: 0 };
|
|
824
|
+
var u = utilityByRef[ref], o = outcomeByRef[ref];
|
|
825
|
+
var queries = {};
|
|
826
|
+
rows.forEach(function (r) { if (r.query) queries[r.query] = (queries[r.query] || 0) + 1; });
|
|
827
|
+
var qList = Object.keys(queries).sort(function (x, y) { return queries[y] - queries[x] || (x < y ? -1 : 1); }).slice(0, 15);
|
|
828
|
+
var reasons = rows.filter(function (r) { return r.eventType === 'feedback' && (r.reason || (r.tags && r.tags.length)); })
|
|
829
|
+
.sort(function (x, y) { return x.at < y.at ? 1 : -1; }).slice(0, 15);
|
|
830
|
+
var facts = [
|
|
831
|
+
['Shows', fmtN(a.shows)], ['Search hits', fmtN(a.searchHits)], ['Selects (window-wide)', fmtN(selectsByRef[ref])],
|
|
832
|
+
['Feedback +/−', a.positive + ' / ' + a.negative], ['Valence', valenceByRef[ref] == null ? '—' : fmtNum2(valenceByRef[ref])]
|
|
833
|
+
];
|
|
834
|
+
if (u) facts.push(['Utility', fmtNum2(u.utility)], ['Utility select rate', fmtPct(u.selectRate)]);
|
|
835
|
+
if (o) facts.push(['Outcome score', fmtNum2(o.outcomeScore)], ['Retrievals', fmtN(o.retrievalCount)], ['Accepted changes', fmtN(o.acceptedChangeCount)]);
|
|
836
|
+
$('drawer').innerHTML =
|
|
837
|
+
'<h2><span id="drawer-title"><code>' + esc(ref) + '</code></span><button type="button" class="close" id="drawer-close">Close</button></h2>' +
|
|
838
|
+
'<div class="cards" style="grid-template-columns:1fr;margin-bottom:14px"><div class="panel"><dl>' +
|
|
839
|
+
facts.map(function (f) { return '<dt>' + esc(f[0]) + '</dt><dd>' + esc(f[1]) + '</dd>'; }).join('') + '</dl></div></div>' +
|
|
840
|
+
'<h3>Activity</h3><div class="ec" id="ch-drawer" style="height:200px"></div>' +
|
|
841
|
+
'<h3>Queries that surfaced it</h3>' + (qList.length ? '<ul>' + qList.map(function (q) { return '<li>' + esc(q) + ' <span class="hint">×' + queries[q] + '</span></li>'; }).join('') + '</ul>' : '<div class="empty">None recorded</div>') +
|
|
842
|
+
'<h3>Feedback</h3>' + (reasons.length ? '<ul>' + reasons.map(function (r) {
|
|
843
|
+
return '<li><span class="' + (r.signal === 'negative' ? 'neg' : 'pos') + '">' + (r.signal === 'negative' ? '−' : '+') + '</span> ' + timeTag(r.at) + ' ' + esc(r.reason || '') +
|
|
844
|
+
' ' + (r.tags || []).map(function (t) { return '<span class="tag">' + esc(t) + '</span>'; }).join('') + '</li>';
|
|
845
|
+
}).join('') + '</ul>' : '<div class="empty">No feedback with a reason</div>');
|
|
846
|
+
$('drawer').hidden = false;
|
|
847
|
+
$('scrim').hidden = false;
|
|
848
|
+
localize($('drawer'));
|
|
849
|
+
var days = agg.daily.length ? dayRange(agg.daily[0].day, agg.daily[agg.daily.length - 1].day) : [];
|
|
850
|
+
var colors = TYPE_COLORS();
|
|
851
|
+
if (days.length && window.echarts) {
|
|
852
|
+
var el = $('ch-drawer');
|
|
853
|
+
var c = echarts.init(el, null, { renderer: 'canvas' });
|
|
854
|
+
charts['ch-drawer'] = c;
|
|
855
|
+
c.setOption({
|
|
856
|
+
backgroundColor: 'transparent', textStyle: { color: css('--muted') },
|
|
857
|
+
tooltip: { trigger: 'axis', formatter: tipFormat, backgroundColor: css('--surface'), borderColor: css('--border'), textStyle: { color: css('--text') } },
|
|
858
|
+
legend: { top: 0, textStyle: { color: css('--muted') } },
|
|
859
|
+
grid: { left: 36, right: 12, top: 28, bottom: 24 },
|
|
860
|
+
xAxis: Object.assign({ type: 'category', data: days }, axisStyle()),
|
|
861
|
+
yAxis: Object.assign({ type: 'value', minInterval: 1 }, axisStyle()),
|
|
862
|
+
series: ['show', 'search', 'feedback'].map(function (t) {
|
|
863
|
+
return { name: t, type: 'bar', stack: 'a', itemStyle: { color: colors[t] },
|
|
864
|
+
data: days.map(function (d) { return rowsOnDay(rows, d, t); }) };
|
|
865
|
+
})
|
|
866
|
+
});
|
|
867
|
+
} else if (days.length) {
|
|
868
|
+
$('ch-drawer').innerHTML = '<div class="ec-empty">Charts need the ECharts library from the CDN.</div>';
|
|
869
|
+
} else {
|
|
870
|
+
$('ch-drawer').innerHTML = '<div class="ec-empty">No activity</div>';
|
|
871
|
+
}
|
|
872
|
+
$('drawer-close').focus();
|
|
873
|
+
}
|
|
874
|
+
// Per-hit search rows carry the ref, so the drawer counts rows directly
|
|
875
|
+
// instead of using the daily series (which counts search summary rows).
|
|
876
|
+
function rowsOnDay(rows, day, type) {
|
|
877
|
+
return rows.filter(function (r) { return r.eventType === type && dayOf(r.at) === day; }).length;
|
|
878
|
+
}
|
|
879
|
+
function closeDrawer() {
|
|
880
|
+
if (charts['ch-drawer']) { charts['ch-drawer'].dispose(); delete charts['ch-drawer']; }
|
|
881
|
+
$('drawer').hidden = true;
|
|
882
|
+
$('scrim').hidden = true;
|
|
883
|
+
if (lastFocus && lastFocus.focus) lastFocus.focus();
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// ── Filter bar ───────────────────────────────────────────────────────────
|
|
887
|
+
function buildChips(containerId, values, set, label) {
|
|
888
|
+
var box = $(containerId);
|
|
889
|
+
values.forEach(function (v) {
|
|
890
|
+
var l = document.createElement('label');
|
|
891
|
+
l.className = 'chip';
|
|
892
|
+
var cb = document.createElement('input');
|
|
893
|
+
cb.type = 'checkbox';
|
|
894
|
+
cb.checked = true;
|
|
895
|
+
cb.addEventListener('change', function () {
|
|
896
|
+
if (cb.checked) set.add(v); else set.delete(v);
|
|
897
|
+
render();
|
|
898
|
+
});
|
|
899
|
+
l.appendChild(cb);
|
|
900
|
+
l.appendChild(document.createTextNode(label ? label(v) : v));
|
|
901
|
+
box.appendChild(l);
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
function resetFilters() {
|
|
905
|
+
state.from = ''; state.to = ''; state.text = '';
|
|
906
|
+
state.bundles = new Set(universe.bundles);
|
|
907
|
+
state.sources = new Set(universe.sources);
|
|
908
|
+
state.types = new Set(EVENT_TYPES);
|
|
909
|
+
$('f-from').value = ''; $('f-to').value = ''; $('f-text').value = '';
|
|
910
|
+
document.querySelectorAll('#filterbar input[type=checkbox]').forEach(function (cb) { cb.checked = true; });
|
|
911
|
+
render();
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function download(name) {
|
|
915
|
+
var t = tables[name];
|
|
916
|
+
if (!t) return;
|
|
917
|
+
var blob = new Blob([toCsv(t.columns, t.rows) + '\n'], { type: 'text/csv;charset=utf-8' });
|
|
918
|
+
var url = URL.createObjectURL(blob);
|
|
919
|
+
var a = document.createElement('a');
|
|
920
|
+
a.href = url;
|
|
921
|
+
a.download = 'akm-metrics-' + t.file + '.csv';
|
|
922
|
+
document.body.appendChild(a);
|
|
923
|
+
a.click();
|
|
924
|
+
a.remove();
|
|
925
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 0);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function init() {
|
|
929
|
+
buildChips('f-bundles', universe.bundles, state.bundles, function (b) { return b || '(no asset)'; });
|
|
930
|
+
buildChips('f-sources', universe.sources, state.sources);
|
|
931
|
+
buildChips('f-types', EVENT_TYPES, state.types);
|
|
932
|
+
if (!HAS_ROWS) $('filterbar').setAttribute('data-disabled', '');
|
|
933
|
+
$('f-from').addEventListener('change', function (e) { state.from = e.target.value; render(); });
|
|
934
|
+
$('f-to').addEventListener('change', function (e) { state.to = e.target.value; render(); });
|
|
935
|
+
$('f-text').addEventListener('input', function (e) { state.text = e.target.value; render({ skipTimeline: false }); });
|
|
936
|
+
$('f-reset').addEventListener('click', resetFilters);
|
|
937
|
+
$('scrim').addEventListener('click', closeDrawer);
|
|
938
|
+
document.addEventListener('keydown', function (e) {
|
|
939
|
+
if (e.key === 'Escape' && !$('drawer').hidden) closeDrawer();
|
|
940
|
+
if ((e.key === 'Enter' || e.key === ' ') && e.target.matches && e.target.matches('tr.clickable, th[data-sort]')) { e.preventDefault(); e.target.click(); }
|
|
941
|
+
});
|
|
942
|
+
document.addEventListener('click', function (e) {
|
|
943
|
+
var t = e.target;
|
|
944
|
+
if (!t.closest) return;
|
|
945
|
+
var exp = t.closest('[data-export]');
|
|
946
|
+
if (exp) { download(exp.getAttribute('data-export')); return; }
|
|
947
|
+
if (t.closest('#drawer-close')) { closeDrawer(); return; }
|
|
948
|
+
var th = t.closest('th[data-sort]');
|
|
949
|
+
if (th) {
|
|
950
|
+
var host = th.closest('.table-wrap');
|
|
951
|
+
var name = Object.keys(tables).find(function (k) { return $('tb-' + k) === host; });
|
|
952
|
+
if (name) {
|
|
953
|
+
var st = state.sorts[name], key = th.getAttribute('data-sort');
|
|
954
|
+
state.sorts[name] = { key: key, dir: st.key === key && st.dir === 'desc' ? 'asc' : 'desc' };
|
|
955
|
+
render({ skipTimeline: true });
|
|
956
|
+
renderStatic();
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
var row = t.closest('tr[data-ref]');
|
|
961
|
+
if (row && HAS_ROWS) openDrawer(row.getAttribute('data-ref'));
|
|
962
|
+
});
|
|
963
|
+
window.addEventListener('resize', function () { Object.keys(charts).forEach(function (k) { charts[k].resize(); }); });
|
|
964
|
+
if (window.matchMedia) {
|
|
965
|
+
var mq = window.matchMedia('(prefers-color-scheme: light)');
|
|
966
|
+
var again = function () { Object.keys(charts).forEach(function (k) { charts[k].dispose(); delete charts[k]; }); render(); renderStatic(); };
|
|
967
|
+
if (mq.addEventListener) mq.addEventListener('change', again);
|
|
968
|
+
}
|
|
969
|
+
renderStatic();
|
|
970
|
+
render();
|
|
971
|
+
localize(document);
|
|
972
|
+
}
|
|
973
|
+
init();
|
|
974
|
+
})();
|
|
975
|
+
</script>
|
|
976
|
+
</body>
|
|
977
|
+
</html>
|