@automagik/genie-brain 0.260404.8

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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Brain Lab — Common Utilities
3
+ *
4
+ * Shared utility library for the Brain Lab dashboard.
5
+ * API wrapper, formatters, DOM helpers, toast notifications.
6
+ */
7
+
8
+ // ─── API ──────────────────────────────────────────────────
9
+
10
+ const API_BASE = "";
11
+
12
+ /**
13
+ * Fetch JSON from the Brain Lab API.
14
+ * @param {string} method - HTTP method
15
+ * @param {string} path - API path (e.g., '/api/leaderboard')
16
+ * @param {object} [body] - Request body for POST/PATCH
17
+ * @returns {Promise<any>}
18
+ */
19
+ async function api(method, path, body) {
20
+ const opts = {
21
+ method,
22
+ headers: { "Content-Type": "application/json" },
23
+ };
24
+ if (body) opts.body = JSON.stringify(body);
25
+
26
+ const res = await fetch(`${API_BASE}${path}`, opts);
27
+ if (!res.ok) {
28
+ const err = await res.json().catch(() => ({ error: res.statusText }));
29
+ throw new Error(err.error || `API error: ${res.status}`);
30
+ }
31
+ return res.json();
32
+ }
33
+
34
+ // ─── Formatters ───────────────────────────────────────────
35
+
36
+ function formatNumber(n, decimals = 2) {
37
+ if (n == null || Number.isNaN(n)) return "—";
38
+ return Number(n).toFixed(decimals);
39
+ }
40
+
41
+ function formatPercent(n, decimals = 1) {
42
+ if (n == null || Number.isNaN(n)) return "—";
43
+ return `${Number(n).toFixed(decimals)}%`;
44
+ }
45
+
46
+ function formatDuration(ms) {
47
+ if (ms == null || Number.isNaN(ms)) return "—";
48
+ if (ms < 1000) return `${Math.round(ms)}ms`;
49
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
50
+ return `${(ms / 60000).toFixed(1)}m`;
51
+ }
52
+
53
+ function formatCost(cents) {
54
+ if (cents == null || Number.isNaN(cents)) return "—";
55
+ if (cents >= 100) return `$${(cents / 100).toFixed(2)}`;
56
+ return `${Number(cents).toFixed(1)}c`;
57
+ }
58
+
59
+ function timeAgo(dateStr) {
60
+ if (!dateStr) return "—";
61
+ const date = new Date(dateStr);
62
+ const now = Date.now();
63
+ const diff = now - date.getTime();
64
+ const mins = Math.floor(diff / 60000);
65
+ if (mins < 1) return "just now";
66
+ if (mins < 60) return `${mins}m ago`;
67
+ const hours = Math.floor(mins / 60);
68
+ if (hours < 24) return `${hours}h ago`;
69
+ const days = Math.floor(hours / 24);
70
+ return `${days}d ago`;
71
+ }
72
+
73
+ // ─── Scoring Helpers ──────────────────────────────────────
74
+
75
+ function getScoreClass(score) {
76
+ if (score >= 80) return "excellent";
77
+ if (score >= 60) return "good";
78
+ if (score >= 40) return "fair";
79
+ return "poor";
80
+ }
81
+
82
+ function getMetricClass(value, thresholds) {
83
+ const { excellent, good, fair } = thresholds || {
84
+ excellent: 0.8,
85
+ good: 0.6,
86
+ fair: 0.4,
87
+ };
88
+ if (value >= excellent) return "metric-excellent";
89
+ if (value >= good) return "metric-good";
90
+ if (value >= fair) return "metric-fair";
91
+ return "metric-poor";
92
+ }
93
+
94
+ function getMedalHtml(rank) {
95
+ if (rank === 1)
96
+ return '<span class="medal" title="1st Place">&#x1F947;</span>';
97
+ if (rank === 2)
98
+ return '<span class="medal" title="2nd Place">&#x1F948;</span>';
99
+ if (rank === 3)
100
+ return '<span class="medal" title="3rd Place">&#x1F949;</span>';
101
+ return `<span style="display:inline-block;min-width:24px;text-align:center;color:var(--text-muted)">${rank}</span>`;
102
+ }
103
+
104
+ function getScoreBarHtml(score, maxWidth = 80) {
105
+ const cls = getScoreClass(score);
106
+ const width = Math.max(4, (score / 100) * maxWidth);
107
+ return `<div class="score-bar">
108
+ <div class="score-bar-fill ${cls}" style="width:${width}px"></div>
109
+ <span class="score-value">${formatNumber(score, 1)}</span>
110
+ </div>`;
111
+ }
112
+
113
+ function getParetoHtml(isParetoOptimal) {
114
+ if (!isParetoOptimal) return "";
115
+ return '<span class="pareto-star" title="Pareto optimal — no other strategy dominates in all dimensions">&#9733;</span>';
116
+ }
117
+
118
+ // ─── DOM Helpers ──────────────────────────────────────────
119
+
120
+ function $(selector, parent) {
121
+ return (parent || document).querySelector(selector);
122
+ }
123
+
124
+ function $$(selector, parent) {
125
+ return Array.from((parent || document).querySelectorAll(selector));
126
+ }
127
+
128
+ function createElement(tag, attrs, children) {
129
+ const el = document.createElement(tag);
130
+ if (attrs) {
131
+ for (const [k, v] of Object.entries(attrs)) {
132
+ if (k === "className") el.className = v;
133
+ else if (k === "textContent") el.textContent = v;
134
+ else if (k === "innerHTML") el.innerHTML = v;
135
+ else if (k.startsWith("on"))
136
+ el.addEventListener(k.slice(2).toLowerCase(), v);
137
+ else el.setAttribute(k, v);
138
+ }
139
+ }
140
+ if (children) {
141
+ for (const child of Array.isArray(children) ? children : [children]) {
142
+ if (typeof child === "string")
143
+ el.appendChild(document.createTextNode(child));
144
+ else if (child) el.appendChild(child);
145
+ }
146
+ }
147
+ return el;
148
+ }
149
+
150
+ function escapeHtml(str) {
151
+ if (!str) return "";
152
+ return String(str)
153
+ .replace(/&/g, "&amp;")
154
+ .replace(/</g, "&lt;")
155
+ .replace(/>/g, "&gt;")
156
+ .replace(/"/g, "&quot;");
157
+ }
158
+
159
+ // ─── URL State ────────────────────────────────────────────
160
+
161
+ function getUrlParam(key) {
162
+ return new URLSearchParams(window.location.search).get(key);
163
+ }
164
+
165
+ function setUrlParams(params) {
166
+ const url = new URL(window.location);
167
+ for (const [k, v] of Object.entries(params)) {
168
+ if (v == null) url.searchParams.delete(k);
169
+ else url.searchParams.set(k, v);
170
+ }
171
+ window.history.replaceState({}, "", url);
172
+ }
173
+
174
+ // ─── Toast Notifications ──────────────────────────────────
175
+
176
+ function showToast(message, type = "info", duration = 3000) {
177
+ let container = $(".toast-container");
178
+ if (!container) {
179
+ container = createElement("div", { className: "toast-container" });
180
+ document.body.appendChild(container);
181
+ }
182
+ const toast = createElement("div", {
183
+ className: `toast ${type}`,
184
+ textContent: message,
185
+ });
186
+ container.appendChild(toast);
187
+ setTimeout(() => {
188
+ toast.style.opacity = "0";
189
+ setTimeout(() => toast.remove(), 200);
190
+ }, duration);
191
+ }
192
+
193
+ // ─── Sorting ──────────────────────────────────────────────
194
+
195
+ function sortData(data, column, direction) {
196
+ return [...data].sort((a, b) => {
197
+ const aVal = a[column];
198
+ const bVal = b[column];
199
+ if (aVal == null && bVal == null) return 0;
200
+ if (aVal == null) return 1;
201
+ if (bVal == null) return -1;
202
+ const cmp =
203
+ typeof aVal === "string" ? aVal.localeCompare(bVal) : aVal - bVal;
204
+ return direction === "asc" ? cmp : -cmp;
205
+ });
206
+ }
207
+
208
+ // ─── Chart Colors ─────────────────────────────────────────
209
+
210
+ const CHART_COLORS = [
211
+ "#58a6ff",
212
+ "#3fb950",
213
+ "#d29922",
214
+ "#f85149",
215
+ "#a371f7",
216
+ "#79c0ff",
217
+ "#56d364",
218
+ "#e3b341",
219
+ "#ff7b72",
220
+ "#bc8cff",
221
+ "#39d2c0",
222
+ "#ff6eb4",
223
+ "#ffc857",
224
+ "#6cb4ee",
225
+ "#b392f0",
226
+ ];
227
+
228
+ function getChartColor(index) {
229
+ return CHART_COLORS[index % CHART_COLORS.length];
230
+ }
231
+
232
+ function hexToRgba(hex, alpha = 1) {
233
+ const r = Number.parseInt(hex.slice(1, 3), 16);
234
+ const g = Number.parseInt(hex.slice(3, 5), 16);
235
+ const b = Number.parseInt(hex.slice(5, 7), 16);
236
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
237
+ }
238
+
239
+ // ─── Export as globals ────────────────────────────────────
240
+
241
+ window.BrainLab = {
242
+ api,
243
+ formatNumber,
244
+ formatPercent,
245
+ formatDuration,
246
+ formatCost,
247
+ timeAgo,
248
+ getScoreClass,
249
+ getMetricClass,
250
+ getMedalHtml,
251
+ getScoreBarHtml,
252
+ getParetoHtml,
253
+ $,
254
+ $$,
255
+ createElement,
256
+ escapeHtml,
257
+ getUrlParam,
258
+ setUrlParams,
259
+ showToast,
260
+ sortData,
261
+ CHART_COLORS,
262
+ getChartColor,
263
+ hexToRgba,
264
+ };
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Brain Lab — Compare View
3
+ *
4
+ * Side-by-side multi-strategy comparison with MemScore triple display.
5
+ * Inspired by memorybench's compare page pattern.
6
+ */
7
+
8
+ (() => {
9
+ const {
10
+ api,
11
+ formatNumber,
12
+ formatPercent,
13
+ formatDuration,
14
+ formatCost,
15
+ $,
16
+ $$,
17
+ escapeHtml,
18
+ showToast,
19
+ getChartColor,
20
+ } = window.BrainLab;
21
+
22
+ const selectedStrategies = new Set();
23
+ let leaderboardData = [];
24
+
25
+ // ─── Load ─────────────────────────────────────────────
26
+
27
+ async function loadCompare() {
28
+ const container = $("#compare-content");
29
+ if (!container) return;
30
+
31
+ try {
32
+ leaderboardData = window.BrainLabLeaderboard
33
+ ? window.BrainLabLeaderboard.getData()
34
+ : await api("GET", "/api/leaderboard");
35
+
36
+ if (!leaderboardData || leaderboardData.length === 0) {
37
+ container.innerHTML = `
38
+ <div class="empty-state">
39
+ <div class="empty-state-icon">&#x2696;</div>
40
+ <div class="empty-state-title">Nothing to compare</div>
41
+ <div class="empty-state-text">Run benchmarks first to compare strategies.</div>
42
+ </div>`;
43
+ return;
44
+ }
45
+
46
+ renderSelector();
47
+ } catch (err) {
48
+ container.innerHTML = `<div class="empty-state"><div class="empty-state-icon">&#x26A0;</div><div class="empty-state-text">${escapeHtml(err.message)}</div></div>`;
49
+ }
50
+ }
51
+
52
+ // ─── Strategy Selector ────────────────────────────────
53
+
54
+ function renderSelector() {
55
+ const container = $("#compare-content");
56
+ if (!container) return;
57
+
58
+ let html = `
59
+ <div class="card">
60
+ <div class="card-header">
61
+ <div class="card-title">Select Strategies to Compare</div>
62
+ <div class="card-subtitle">Choose 2 or more strategies for side-by-side comparison</div>
63
+ </div>
64
+ <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px">`;
65
+
66
+ for (const d of leaderboardData) {
67
+ const checked = selectedStrategies.has(d.strategy) ? "checked" : "";
68
+ html += `
69
+ <label class="checkbox-label">
70
+ <input type="checkbox" value="${escapeHtml(d.strategy)}" ${checked} class="compare-check">
71
+ ${escapeHtml(d.strategy)}
72
+ </label>`;
73
+ }
74
+
75
+ html += `</div>
76
+ <button class="btn btn-primary" id="compare-btn" ${selectedStrategies.size < 2 ? "disabled" : ""}>
77
+ Compare Selected (${selectedStrategies.size})
78
+ </button>
79
+ </div>
80
+ <div id="compare-results"></div>`;
81
+
82
+ container.innerHTML = html;
83
+
84
+ // Bind checkbox changes
85
+ $$(".compare-check", container).forEach((cb) => {
86
+ cb.addEventListener("change", () => {
87
+ if (cb.checked) {
88
+ selectedStrategies.add(cb.value);
89
+ } else {
90
+ selectedStrategies.delete(cb.value);
91
+ }
92
+ const btn = $("#compare-btn");
93
+ if (btn) {
94
+ btn.textContent = `Compare Selected (${selectedStrategies.size})`;
95
+ btn.disabled = selectedStrategies.size < 2;
96
+ }
97
+ });
98
+ });
99
+
100
+ // Bind compare button
101
+ const btn = $("#compare-btn");
102
+ if (btn) {
103
+ btn.addEventListener("click", renderComparison);
104
+ }
105
+
106
+ // Auto-render if strategies already selected
107
+ if (selectedStrategies.size >= 2) {
108
+ renderComparison();
109
+ }
110
+ }
111
+
112
+ // ─── Comparison View ──────────────────────────────────
113
+
114
+ function renderComparison() {
115
+ const resultsDiv = $("#compare-results");
116
+ if (!resultsDiv) return;
117
+
118
+ const selected = leaderboardData.filter((d) =>
119
+ selectedStrategies.has(d.strategy),
120
+ );
121
+ if (selected.length < 2) {
122
+ showToast("Select at least 2 strategies", "error");
123
+ return;
124
+ }
125
+
126
+ // Find best values for delta highlighting
127
+ const best = {
128
+ brainScore: Math.max(...selected.map((d) => d.brainScore || 0)),
129
+ avgScore: Math.max(...selected.map((d) => d.avgScore || 0)),
130
+ mrr: Math.max(...selected.map((d) => d.mrr || 0)),
131
+ ndcg: Math.max(...selected.map((d) => d.ndcg || 0)),
132
+ f1AtK: Math.max(...selected.map((d) => d.f1AtK || 0)),
133
+ hitRatePct: Math.max(...selected.map((d) => d.hitRatePct || 0)),
134
+ avgLatencyMs: Math.min(
135
+ ...selected.map((d) => d.avgLatencyMs || Number.POSITIVE_INFINITY),
136
+ ),
137
+ totalCostCents: Math.min(
138
+ ...selected.map((d) => d.totalCostCents || Number.POSITIVE_INFINITY),
139
+ ),
140
+ };
141
+
142
+ let html = '<div class="compare-grid">';
143
+
144
+ for (let i = 0; i < selected.length; i++) {
145
+ const d = selected[i];
146
+ const isBest = d.brainScore === best.brainScore;
147
+
148
+ html += `
149
+ <div class="compare-card ${isBest ? "selected" : ""}">
150
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
151
+ <h4>${escapeHtml(d.strategy)}</h4>
152
+ ${d.isParetoOptimal ? '<span class="pareto-star" title="Pareto optimal">&#9733;</span>' : ""}
153
+ </div>
154
+
155
+ <div class="compare-memscore">${escapeHtml(d.memScore || "—")}</div>
156
+
157
+ <table style="width:100%;margin-top:12px">
158
+ <tbody>
159
+ ${compareRow("BrainScore", d.brainScore, best.brainScore, formatNumber, true)}
160
+ ${compareRow("Quality", d.avgScore, best.avgScore, (v) => formatNumber(v, 3), true)}
161
+ ${compareRow("MRR", d.mrr, best.mrr, (v) => formatNumber(v, 3), true)}
162
+ ${compareRow("NDCG", d.ndcg, best.ndcg, (v) => formatNumber(v, 3), true)}
163
+ ${compareRow("F1@K", d.f1AtK, best.f1AtK, (v) => formatNumber(v, 3), true)}
164
+ ${compareRow("Hit Rate", d.hitRatePct, best.hitRatePct, (v) => formatPercent(v), true)}
165
+ ${compareRow("Avg Latency", d.avgLatencyMs, best.avgLatencyMs, (v) => formatDuration(v), false)}
166
+ ${compareRow("Total Cost", d.totalCostCents, best.totalCostCents, (v) => formatCost(v), false)}
167
+ <tr><td style="color:var(--text-muted)">Evaluations</td><td class="numeric">${d.totalEvaluations || 0}</td></tr>
168
+ </tbody>
169
+ </table>
170
+ </div>`;
171
+ }
172
+
173
+ html += "</div>";
174
+
175
+ // Comparison table
176
+ html += `
177
+ <div class="card" style="margin-top:16px">
178
+ <div class="card-header">
179
+ <div class="card-title">Metric Comparison Table</div>
180
+ </div>
181
+ <div class="table-wrapper">
182
+ <table>
183
+ <thead>
184
+ <tr>
185
+ <th>Metric</th>
186
+ ${selected.map((d) => `<th>${escapeHtml(d.strategy)}</th>`).join("")}
187
+ </tr>
188
+ </thead>
189
+ <tbody>
190
+ ${metricRow("BrainScore", selected, "brainScore", (v) => formatNumber(v, 1), true)}
191
+ ${metricRow("Quality", selected, "avgScore", (v) => formatNumber(v, 3), true)}
192
+ ${metricRow("MRR", selected, "mrr", (v) => formatNumber(v, 3), true)}
193
+ ${metricRow("NDCG", selected, "ndcg", (v) => formatNumber(v, 3), true)}
194
+ ${metricRow("F1@K", selected, "f1AtK", (v) => formatNumber(v, 3), true)}
195
+ ${metricRow("Hit Rate", selected, "hitRatePct", (v) => formatPercent(v), true)}
196
+ ${metricRow("Avg Latency", selected, "avgLatencyMs", (v) => formatDuration(v), false)}
197
+ ${metricRow("p95 Latency", selected, "p95LatencyMs", (v) => formatDuration(v), false)}
198
+ ${metricRow("Cost", selected, "totalCostCents", (v) => formatCost(v), false)}
199
+ </tbody>
200
+ </table>
201
+ </div>
202
+ </div>`;
203
+
204
+ resultsDiv.innerHTML = html;
205
+ }
206
+
207
+ // ─── Helpers ──────────────────────────────────────────
208
+
209
+ function compareRow(label, value, bestValue, formatter, higherIsBetter) {
210
+ const isBest = higherIsBetter ? value >= bestValue : value <= bestValue;
211
+ const cls = isBest ? "delta-positive" : "";
212
+ return `<tr>
213
+ <td style="color:var(--text-muted)">${label}</td>
214
+ <td class="numeric ${cls}">${formatter(value)}</td>
215
+ </tr>`;
216
+ }
217
+
218
+ function metricRow(label, entries, key, formatter, higherIsBetter) {
219
+ const values = entries.map((d) => d[key] || 0);
220
+ const bestVal = higherIsBetter ? Math.max(...values) : Math.min(...values);
221
+
222
+ const cells = entries
223
+ .map((d) => {
224
+ const val = d[key] || 0;
225
+ const isBest = val === bestVal;
226
+ return `<td class="numeric ${isBest ? "delta-positive" : ""}" style="${isBest ? "font-weight:600" : ""}">${formatter(val)}</td>`;
227
+ })
228
+ .join("");
229
+
230
+ return `<tr><td style="color:var(--text-muted)">${label}</td>${cells}</tr>`;
231
+ }
232
+
233
+ // ─── Public API ───────────────────────────────────────
234
+
235
+ window.BrainLabCompare = {
236
+ load: loadCompare,
237
+ getSelected: () => [...selectedStrategies],
238
+ };
239
+ })();