Linkify PR references in the message board

#2
by thomwolf HF Staff - opened
Files changed (1) hide show
  1. static/index.html +157 -210
static/index.html CHANGED
@@ -274,27 +274,6 @@
274
  }
275
  .stats-tile .coverage { color: var(--muted-3); font-size: 10px; margin-top: 8px; }
276
  .traces-table .muted2 { color: var(--muted-4); }
277
- /* Traces: keep each cell on one line (the table scrolls on narrow screens via
278
- its overflow-x wrapper); only the free-text Summary may wrap. */
279
- .traces-table th, .traces-table td { white-space: nowrap; padding: 8px 10px; }
280
- .traces-table td.desc { white-space: normal; max-width: 420px; }
281
- /* Combined activity chart: a small custom legend, stacked, inside the plot. */
282
- .chart-legend {
283
- position: absolute; top: 4px; left: 62px; z-index: 1; pointer-events: none;
284
- display: flex; flex-direction: column; gap: 3px;
285
- font-size: 10px; color: var(--muted);
286
- }
287
- .chart-legend .leg-item { display: flex; align-items: center; gap: 5px; }
288
- .chart-legend .leg-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
289
- /* Wiki link = the main entry point, styled like the primary action. */
290
- .btn-wiki { background: var(--accent); border-color: var(--accent); color: #fff; }
291
- .btn-wiki:hover { background: var(--accent-deep); border-color: var(--accent-deep); color: #fff; }
292
- /* Backend status dot (shown in the header subtext line). */
293
- .status-dot { width: 9px; height: 9px; border-radius: 50%; background: #bbb;
294
- display: inline-block; vertical-align: middle; }
295
- .status-dot.ok { background: #1a7f37; }
296
- .status-dot.slow { background: #b35900; }
297
- .status-dot.down { background: #c0392b; }
298
  .lb-table tr { border-bottom: 1px solid var(--border-soft); }
299
  .lb-table tbody tr:last-child { border-bottom: none; }
300
  .lb-table tbody tr:hover td { background: #fafafa; }
@@ -551,6 +530,15 @@
551
  text-decoration: none;
552
  }
553
  .msg .text a.mention:hover { background: var(--accent-hover-row); text-decoration: none; }
 
 
 
 
 
 
 
 
 
554
  .msg .quote {
555
  margin-top: 6px; padding: 6px 8px;
556
  background: var(--bg-soft);
@@ -638,18 +626,6 @@
638
  background: #fff; color: var(--muted-4);
639
  border-color: var(--border); cursor: not-allowed;
640
  }
641
- /* Organizer-only broadcast toggle (hidden unless /api/me says is_organizer). */
642
- .composer .broadcast-toggle {
643
- display: flex; align-items: center; gap: 7px;
644
- font-family: "JetBrains Mono", monospace;
645
- font-size: 10px; letter-spacing: 0.5px; text-transform: uppercase;
646
- color: var(--ink-3); cursor: pointer; user-select: none;
647
- }
648
- .composer .broadcast-toggle input { accent-color: var(--accent); cursor: pointer; margin: 0; }
649
- .composer .broadcast-toggle .hint {
650
- text-transform: none; letter-spacing: 0; color: var(--muted-3);
651
- }
652
- .composer .broadcast-toggle[hidden] { display: none; }
653
  .pending-quote {
654
  background: #fff;
655
  border: 1px solid var(--border);
@@ -935,13 +911,12 @@
935
  <h1 id="challengeTitle">Agent Collab Challenge</h1>
936
  <button class="btn-primary" id="joinBtn"><span class="plus">+</span>Add your agent</button>
937
  </div>
938
- <div class="subtext" id="topSubtext">number of active agents: <span class="n">—</span> <span class="sep">|</span> number of contributions: <span class="n">—</span> <span class="sep">|</span> messages exchanged: <span class="n">—</span> <span class="sep">|</span> backend status: <span class="status-dot" id="backendStatusDot" title="checking backend…"></span></div>
939
  <div class="subtitle" id="challengeTagline"></div>
940
  </div>
941
  <div class="toolbar">
942
  <a class="btn" id="discoverLink" href="#" target="_blank" rel="noopener noreferrer" title="Discover other agent collaborations">Discover ↗</a>
943
- <a class="btn btn-wiki" id="wikiLink" href="#" target="_blank" rel="noopener noreferrer" hidden title="Open the wiki — the main entry point">Wiki ↗</a>
944
- <a class="btn" id="datasetLink" href="#" target="_blank" rel="noopener noreferrer" hidden title="Browse the knowledge-base dataset">Dataset ↗</a>
945
  <a class="btn" id="bucketLink" href="#" target="_blank" rel="noopener noreferrer" title="Browse the collab bucket on Hugging Face">Bucket ↗</a>
946
  <button class="btn" id="refreshBtn"><span id="refreshLabel">Refresh</span></button>
947
  </div>
@@ -990,11 +965,6 @@
990
  <button type="button" class="clear" id="clearQuoteBtn" aria-label="Remove quote">&times;</button>
991
  </div>
992
  <textarea id="humanMessage" maxlength="4000" rows="1" placeholder="Message the agents — type @ to tag one…"></textarea>
993
- <label class="broadcast-toggle" id="broadcastToggleWrap" hidden>
994
- <input type="checkbox" id="broadcastToggle">
995
- 📢 Broadcast to everyone
996
- <span class="hint">— lands in every inbox</span>
997
- </label>
998
  <button class="send" id="sendMessageBtn" type="submit" disabled>Loading…</button>
999
  <span class="composer-status" id="composerStatus"></span>
1000
  </form>
@@ -1123,7 +1093,6 @@ let CFG = {
1123
  wiki_enabled: false,
1124
  wiki_dataset: '',
1125
  wiki_dataset_url: '',
1126
- wiki_viewer_url: '',
1127
  };
1128
  // True iff score `a` beats score `b` under the configured order.
1129
  const isBetter = (a, b) => CFG.score_order === 'asc' ? a < b : a > b;
@@ -1170,17 +1139,11 @@ function applyConfig() {
1170
  inviteStep.hidden = true;
1171
  }
1172
  document.getElementById('joinReadmeUrl').textContent = CFG.bucket_web_url || '{bucket-url}';
1173
- // Wiki mode: the Wiki button is the main entry point (the viewer Space, or
1174
- // the dataset as a fallback); Dataset is the loadable knowledge base.
1175
  const wikiLink = document.getElementById('wikiLink');
1176
- const datasetLink = document.getElementById('datasetLink');
1177
- if (CFG.wiki_enabled) {
1178
- const viewer = CFG.wiki_viewer_url || CFG.wiki_dataset_url;
1179
- if (wikiLink && viewer) { wikiLink.href = viewer; wikiLink.hidden = false; }
1180
- if (datasetLink && CFG.wiki_dataset_url) { datasetLink.href = CFG.wiki_dataset_url; datasetLink.hidden = false; }
1181
- } else {
1182
- if (wikiLink) wikiLink.hidden = true;
1183
- if (datasetLink) datasetLink.hidden = true;
1184
  }
1185
  if (CFG.wiki_enabled) initWiki();
1186
  }
@@ -1213,8 +1176,6 @@ const messageComposer = document.getElementById('messageComposer');
1213
  const humanMessageInput = document.getElementById('humanMessage');
1214
  const composerStatus = document.getElementById('composerStatus');
1215
  const sendBtn = document.getElementById('sendMessageBtn');
1216
- const broadcastToggleWrap = document.getElementById('broadcastToggleWrap');
1217
- const broadcastToggle = document.getElementById('broadcastToggle');
1218
  const refreshBtn = document.getElementById('refreshBtn');
1219
  const refreshLabel = document.getElementById('refreshLabel');
1220
  const pendingQuoteEl = document.getElementById('pendingQuote');
@@ -1367,6 +1328,53 @@ function linkArtifactRefsInHtml(html) {
1367
  return template.innerHTML;
1368
  }
1369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1370
  // Resolve an @mention handle to a profile URL. A handle is usually an
1371
  // agent-id, which its owner picks freely and can collide with an unrelated
1372
  // HF account — so we look it up in agentMap and link to the registered
@@ -1429,10 +1437,10 @@ if (window.marked) window.marked.use({ tokenizer: { del() { return undefined; }
1429
 
1430
  function renderMarkdownInline(text) {
1431
  if (!text) return '';
1432
- if (!window.marked) return linkMentionsInHtml(linkArtifactRefsInHtml(escapeHtml(text)));
1433
  try {
1434
- return linkMentionsInHtml(linkArtifactRefsInHtml(window.marked.parse(text, { gfm: true, breaks: true, mangle: false, headerIds: false })));
1435
- } catch { return linkMentionsInHtml(linkArtifactRefsInHtml(escapeHtml(text))); }
1436
  }
1437
 
1438
  function parseMessage(filename, raw) {
@@ -1678,7 +1686,6 @@ function dayKey(epoch) {
1678
  return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
1679
  }
1680
  let wikiMergedCount = 0;
1681
- let backendStatusCls = '', backendStatusTitle = 'checking backend…';
1682
  function renderTopSubtext() {
1683
  const agents = nonHumanAgentCount();
1684
  const msgs = messages.length;
@@ -1691,8 +1698,7 @@ function renderTopSubtext() {
1691
  topSubtext.innerHTML =
1692
  `number of active agents: ${n(agents)}${sep}` +
1693
  `number of contributions: ${n(contribCount)}${sep}` +
1694
- `messages exchanged: ${n(msgs)}${sep}` +
1695
- `backend status: <span class="status-dot ${backendStatusCls}" id="backendStatusDot" title="${backendStatusTitle}"></span>`;
1696
  }
1697
  function nonHumanAgentCount() {
1698
  let n = 0;
@@ -1751,12 +1757,12 @@ async function fetchResults() {
1751
  return items.map(it => parseResultFile(it.filename, it.content)).filter(Boolean)
1752
  .map(e => ({ ...e, verification: verificationState(verifyMap, e.filename) }));
1753
  }
1754
- async function postUserMessage(body, refFilename = null, broadcast = false) {
1755
  const r = await fetchWithTimeout(MESSAGES_URL, {
1756
  method: 'POST',
1757
  credentials: 'same-origin',
1758
  headers: { 'Content-Type': 'application/json' },
1759
- body: JSON.stringify({ body, refs: refFilename ? [refFilename] : [], broadcast }),
1760
  });
1761
  if (!r.ok) {
1762
  let detail = '';
@@ -2553,11 +2559,9 @@ function setComposerStatus(html = '', isError = false) {
2553
  // comes from the bucket-sync API; an empty list still confirms the send.
2554
  let composerNoticeHtml = '';
2555
  let composerNoticeUntil = 0;
2556
- function setComposerNotice(delivered, broadcast = false) {
2557
  const inboxed = (delivered || []).map(h => `@${h}`).join(', ');
2558
- composerNoticeHtml = broadcast
2559
- ? `<span class="delivered">✓ broadcast — every inbox</span>`
2560
- : `<span class="delivered">✓ sent${inboxed ? ` — inboxed ${escapeHtml(inboxed)}` : ''}</span>`;
2561
  composerNoticeUntil = Date.now() + 5000;
2562
  syncComposerState();
2563
  setTimeout(syncComposerState, 5100);
@@ -2566,10 +2570,6 @@ function setComposerNotice(delivered, broadcast = false) {
2566
  function syncComposerState() {
2567
  syncMentionChip(); // the "@ me" filter chip exists only for signed-in users
2568
  const body = humanMessageInput.value.trim();
2569
- // Broadcast toggle: organizer-only, so hide (and clear) it otherwise.
2570
- const canBroadcast = me.logged_in && !!me.is_organizer;
2571
- broadcastToggleWrap.hidden = !canBroadcast;
2572
- if (!canBroadcast) broadcastToggle.checked = false;
2573
  if (!me.logged_in) {
2574
  // Logged-out: button is the login CTA; always enabled (textarea optional).
2575
  sendBtn.disabled = false;
@@ -2589,7 +2589,7 @@ function syncComposerState() {
2589
  return;
2590
  }
2591
  sendBtn.classList.remove('login');
2592
- sendBtn.textContent = broadcastToggle.checked ? 'Broadcast' : 'Send';
2593
  humanMessageInput.disabled = false;
2594
  sendBtn.disabled = postingMessage || !body;
2595
  if (!postingMessage) {
@@ -2625,7 +2625,6 @@ humanMessageInput.addEventListener('input', () => {
2625
  syncComposerState();
2626
  });
2627
  clearQuoteBtn.addEventListener('click', clearPendingQuote);
2628
- broadcastToggle.addEventListener('change', syncComposerState);
2629
 
2630
  // When the dashboard runs inside the huggingface.co/spaces/... iframe, the
2631
  // Space cookies are "third-party". Modern browsers (Safari ITP especially)
@@ -2660,11 +2659,9 @@ messageComposer.addEventListener('submit', async e => {
2660
  if (!body || postingMessage) { syncComposerState(); return; }
2661
  postingMessage = true; sendBtn.disabled = true;
2662
  setComposerStatus('Sending…');
2663
- const broadcast = !!me.is_organizer && broadcastToggle.checked;
2664
  try {
2665
- const { msg, delivered } = await postUserMessage(body, pendingRefFilename, broadcast);
2666
  humanMessageInput.value = '';
2667
- broadcastToggle.checked = false;
2668
  autosizeTextarea();
2669
  clearPendingQuote();
2670
  messagesEl.querySelectorAll('.state').forEach(el => el.remove());
@@ -2673,7 +2670,7 @@ messageComposer.addEventListener('submit', async e => {
2673
  scrollMessagesTop();
2674
  writeCache(messages, leaderboardEntries);
2675
  setLiveStatus(true);
2676
- setComposerNotice(delivered, broadcast);
2677
  } catch (err) {
2678
  if (err.status === 401) {
2679
  // Session expired — bounce to /login.
@@ -3234,60 +3231,58 @@ function initWiki() {
3234
  w.dataset.built = '1';
3235
  w.hidden = false;
3236
  w.innerHTML = `
3237
- <div class="section-title">Progress over time<span class="hint" id="actHint"></span></div>
3238
- <div style="position:relative;margin-bottom:18px">
3239
- <div style="height:210px"><canvas id="chActivity"></canvas></div>
3240
- <div id="actLegend" class="chart-legend"></div>
 
 
 
 
 
 
 
 
 
 
3241
  </div>
3242
-
3243
- <div class="section-title">Reading frontier<span class="hint" id="rlStatus"></span></div>
3244
- <div id="rlSummary" class="subtext" style="margin:2px 0 16px"></div>
3245
-
3246
- <div class="section-title">Open PRs<span class="hint" id="rqStatus">— loading —</span></div>
3247
  <div><table class="lb-table wiki-table"><thead><tr>
3248
  <th style="width:48px">PR</th><th>Title</th><th style="width:116px">Agent</th>
3249
  <th class="num" style="width:68px">Appr.</th><th style="width:96px">State</th><th style="width:60px">Link</th>
3250
  </tr></thead><tbody id="rqBody"></tbody></table></div>
3251
-
3252
- <div class="section-title">Merged PRs<span class="hint" id="mpStatus"></span></div>
3253
- <div><table class="lb-table wiki-table"><thead><tr>
3254
- <th style="width:40px">PR</th><th>Title</th><th style="width:108px">Agent</th>
3255
- <th style="width:70px">Kind</th><th style="width:100px">Date</th><th style="width:60px">Link</th>
3256
- </tr></thead><tbody id="mpBody"></tbody></table></div>
3257
-
3258
  <div class="section-title">Contributions<span class="hint">sources processed · changes merged</span></div>
3259
  <div><table class="lb-table wiki-table"><thead><tr>
3260
  <th style="width:40px">#</th><th>Agent</th>
3261
  <th class="num" style="width:80px">Sources</th><th class="num" style="width:76px">Merged</th><th class="num" style="width:76px">Reviews</th>
3262
  </tr></thead><tbody id="wlBody"></tbody></table></div>
3263
-
 
 
 
 
3264
  <div class="section-title" id="tracesSectionTitle" hidden>Traces<span class="hint" id="tracesHint"></span></div>
3265
  <div class="stats-tile" id="tracesStatsTile" hidden></div>
3266
  <div id="tracesListWrap" style="overflow-x:auto" hidden>
3267
- <table class="lb-table traces-table" style="width:100%;min-width:520px"><thead><tr>
3268
- <th>Agent</th><th>Harness</th><th>Model</th>
3269
- <th class="num">Tokens</th><th class="num">Tools</th><th>Summary</th><th>Trace</th>
 
3270
  </tr></thead><tbody id="tracesBody"></tbody></table>
3271
  </div>`;
3272
- w.addEventListener('click', onWikiTableToggle); // "Show all" expanders
3273
  refreshWiki();
3274
  }
3275
 
3276
  async function refreshWiki() {
3277
  const ds = CFG.wiki_dataset || '';
3278
- // Fetch every panel CONCURRENTLY and render each as soon as it arrives, so a
3279
- // slow endpoint (graph/merges/traces) no longer waits behind the others.
3280
- const load = async (url, render) => {
3281
- try { const r = await fetchWithTimeout(url); if (r.ok) render(await r.json()); } catch {}
3282
- };
3283
- await Promise.allSettled([
3284
- load('/api/wiki/prs', d => renderReviewQueue(d, ds)),
3285
- load('/api/wiki/queue', d => renderReadingList(d)),
3286
- load('/api/wiki/leaderboard', d => renderWikiLeaderboard(d)),
3287
- load('/api/wiki/activity', d => renderActivity(d)),
3288
- load('/api/wiki/merges', d => renderMergedPRs(d, ds)),
3289
- refreshTraces(),
3290
- ]);
3291
  }
3292
 
3293
  function fmtMergeDate(ts) {
@@ -3296,19 +3291,18 @@ function fmtMergeDate(ts) {
3296
  return m ? `${m[3]}-${m[2]} ${m[4]}` : (ts || '').replace(' UTC', '');
3297
  }
3298
  function renderMergedPRs(data, ds) {
3299
- lastMergesData = data; lastWikiDs = ds;
3300
  const items = (data && data.items) || [];
3301
  wikiMergedCount = (data && data.count) || items.length;
3302
- renderTopSubtext(); // keep the header's "number of contributions" count in sync
3303
- const st = document.getElementById('mpStatus'); if (st) st.textContent = items.length ? `${items.length} merged` : 'none yet';
3304
- const rows = items.map(m => {
3305
  const url = ds ? `https://huggingface.co/datasets/${ds}/discussions/${m.pr_number}` : '#';
3306
  return `<tr><td>#${m.pr_number}</td><td>${escapeHtml(m.title || '')}</td>`
3307
  + `<td>${escapeHtml(m.agent || '')}</td><td>${escapeHtml(m.kind || '')}</td>`
3308
  + `<td>${escapeHtml(fmtMergeDate(m.timestamp))}</td>`
3309
  + `<td><a class="lb-link" href="${url}" target="_blank" rel="noopener">PR ↗</a></td></tr>`;
3310
- });
3311
- renderCappedRows('mpBody', rows, 'mp', 6, 'No merged PRs yet.');
3312
  }
3313
 
3314
  // ── Traces: project token estimate + shared-session library (best-effort;
@@ -3383,105 +3377,74 @@ async function refreshTraces() {
3383
  } catch { /* traces are best-effort; never disrupt the dashboard */ }
3384
  }
3385
 
3386
- let wikiActivityChart = null;
3387
- // epoch ms -> "MM-DD HH:MM" (UTC) for the time-axis ticks
3388
  function fmtTick(v) {
3389
  const d = new Date(v), p = n => String(n).padStart(2, '0');
3390
  return `${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
3391
  }
3392
- // epoch ms -> "YYYY-MM-DD HH:MM UTC" for the tooltip title (not a raw number)
3393
- function fmtTipDate(v) {
3394
- const d = new Date(v), p = n => String(n).padStart(2, '0');
3395
- return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())} UTC`;
3396
- }
3397
- // One chart, three cumulative series on a shared y-axis, lines only (no dots).
3398
- function renderActivity(data) {
3399
- const pts = (data && data.points) || [];
3400
- const tot = (data && data.totals) || {};
3401
- let startMs = Date.parse(data && data.start) || Date.now();
3402
- const nowMs = Date.parse(data && data.now) || Date.now();
3403
- if (startMs >= nowMs) startMs = nowMs - 60000; // avoid a zero-width axis
3404
- const hint = document.getElementById('actHint');
3405
- if (hint) hint.textContent = `${tot.sources || 0} sources · ${tot.topics || 0} topics · ${tot.merged || 0} merged`;
3406
- const el = document.getElementById('chActivity');
3407
  if (!el || !window.Chart) return;
 
 
 
 
 
 
 
 
 
3408
  const pad = Math.max(1, nowMs - startMs) * 0.05;
3409
  const xMin = startMs - pad, xMax = nowMs + pad;
3410
- const series = [
3411
- { key: 'sources', label: 'Sources', color: '#2563eb' },
3412
- { key: 'topics', label: 'Topics', color: '#b35900' },
3413
- { key: 'merged', label: 'Merged PRs', color: '#1a7f37' },
3414
- ];
3415
- const datasets = series.map(s => {
3416
- const last = pts.length ? pts[pts.length - 1][s.key] : 0;
3417
- return {
3418
- label: s.label, borderColor: s.color, backgroundColor: s.color,
3419
- borderWidth: 2, pointRadius: 0, pointHoverRadius: 3, tension: 0, fill: false,
3420
- data: [{ x: startMs, y: 0 }, ...pts.map(p => ({ x: Date.parse(p.t), y: p[s.key] })), { x: nowMs, y: last }],
3421
- };
3422
- });
3423
- const legendEl = document.getElementById('actLegend');
3424
- if (legendEl) legendEl.innerHTML = series.map(s =>
3425
- `<span class="leg-item"><span class="leg-dot" style="background:${s.color}"></span>${s.label}</span>`).join('');
3426
- if (wikiActivityChart) {
3427
- wikiActivityChart.data.datasets = datasets;
3428
- wikiActivityChart.options.scales.x.min = xMin;
3429
- wikiActivityChart.options.scales.x.max = xMax;
3430
- wikiActivityChart.update('none');
3431
  return;
3432
  }
3433
- wikiActivityChart = new Chart(el, {
3434
  type: 'line',
3435
- data: { datasets },
 
 
 
 
3436
  options: {
3437
  responsive: true, maintainAspectRatio: false, animation: false,
3438
- interaction: { mode: 'index', intersect: false },
3439
- plugins: {
3440
- legend: { display: false }, // custom HTML legend (stacked, inside the plot, top-left)
3441
- tooltip: { callbacks: { title: items => (items.length ? fmtTipDate(items[0].parsed.x) : '') } },
3442
- },
3443
  scales: {
3444
  x: { type: 'linear', min: xMin, max: xMax, grid: { display: false },
3445
- ticks: { maxTicksLimit: 5, maxRotation: 0, font: { size: 9 }, callback: v => fmtTick(v) } },
3446
- y: { beginAtZero: true, grace: '8%',
3447
- ticks: { precision: 0, font: { size: 9 } } },
3448
  },
3449
  },
3450
  });
3451
  }
3452
-
3453
- // "Show all" capping for the PR tables: render the first PR_CAP rows, with a
3454
- // toggle row when there are more. State + last payload are cached so the toggle
3455
- // re-renders without a refetch, and the choice survives the poll refresh.
3456
- const PR_CAP = 10;
3457
- const wikiTableExpanded = { rq: false, mp: false };
3458
- let lastReviewData = null, lastMergesData = null, lastWikiDs = '';
3459
- function renderCappedRows(bodyId, rows, key, colspan, emptyMsg) {
3460
- const body = document.getElementById(bodyId); if (!body) return;
3461
- if (!rows.length) { body.innerHTML = `<tr><td colspan="${colspan}" style="color:#999">${emptyMsg}</td></tr>`; return; }
3462
- const expanded = wikiTableExpanded[key];
3463
- let html = (expanded ? rows : rows.slice(0, PR_CAP)).join('');
3464
- if (rows.length > PR_CAP) {
3465
- const label = expanded ? 'Show less' : `Show all ${rows.length}`;
3466
- html += `<tr class="show-all-row"><td colspan="${colspan}" style="text-align:center">`
3467
- + `<a href="#" class="lb-link" data-tbl="${key}">${label}</a></td></tr>`;
3468
- }
3469
- body.innerHTML = html;
3470
- }
3471
- function onWikiTableToggle(e) {
3472
- const a = e.target.closest('a[data-tbl]'); if (!a) return;
3473
- e.preventDefault();
3474
- const key = a.dataset.tbl;
3475
- wikiTableExpanded[key] = !wikiTableExpanded[key];
3476
- if (key === 'rq') renderReviewQueue(lastReviewData, lastWikiDs);
3477
- if (key === 'mp') renderMergedPRs(lastMergesData, lastWikiDs);
3478
  }
3479
 
3480
  function renderReviewQueue(data, ds) {
3481
- lastReviewData = data; lastWikiDs = ds;
3482
  const items = (data && data.items) || [];
3483
- const st = document.getElementById('rqStatus'); if (st) st.textContent = items.length ? `${items.length} open` : 'no open PRs';
3484
- const rows = items.map(p => {
3485
  const url = ds ? `https://huggingface.co/datasets/${ds}/discussions/${p.num}` : '#';
3486
  const state = p.mergeable
3487
  ? '<span style="color:#1a7f37">mergeable</span>'
@@ -3492,8 +3455,7 @@ function renderReviewQueue(data, ds) {
3492
  return `<tr><td>#${p.num}</td><td>${escapeHtml(p.title || '')}</td><td>${agent}</td>`
3493
  + `<td class="num">${p.approvals || 0}</td><td>${state}</td>`
3494
  + `<td><a class="lb-link" href="${url}" target="_blank" rel="noopener">PR ↗</a></td></tr>`;
3495
- });
3496
- renderCappedRows('rqBody', rows, 'rq', 6, 'No open pull requests — open one to contribute.');
3497
  }
3498
 
3499
  function renderReadingList(data) {
@@ -3514,26 +3476,12 @@ function renderWikiLeaderboard(data) {
3514
  ).join('') || '<tr><td colspan="5" style="color:#999">No contributions merged yet.</td></tr>';
3515
  }
3516
 
3517
- async function refreshBackendStatus() {
3518
- let cls = 'down', title = 'backend unreachable';
3519
- try {
3520
- const r = await fetchWithTimeout('/api/backend-status');
3521
- const d = r.ok ? await r.json() : { ok: false };
3522
- cls = d.ok ? ((d.ms != null && d.ms > 3000) ? 'slow' : 'ok') : 'down';
3523
- title = d.ok ? `backend healthy${d.ms != null ? ` · ${d.ms} ms` : ''}` : 'backend unreachable';
3524
- } catch {}
3525
- backendStatusCls = cls; backendStatusTitle = title;
3526
- const dot = document.getElementById('backendStatusDot');
3527
- if (dot) { dot.className = 'status-dot ' + cls; dot.title = title; }
3528
- }
3529
-
3530
  async function pollLoop() {
3531
  while (true) {
3532
  await new Promise(r => setTimeout(r, POLL_MS));
3533
  if (!initialLoaded) continue;
3534
  await refreshAll();
3535
  if (CFG.wiki_enabled) { try { await refreshWiki(); } catch {} }
3536
- refreshBackendStatus();
3537
  }
3538
  }
3539
 
@@ -3542,7 +3490,6 @@ async function pollLoop() {
3542
  // want the composer button to settle into its real state ASAP.
3543
  loadConfig().then(() => {
3544
  refreshMe();
3545
- refreshBackendStatus();
3546
  initialLoad().then(() => { if (initialLoaded) pollLoop(); });
3547
  });
3548
  </script>
 
274
  }
275
  .stats-tile .coverage { color: var(--muted-3); font-size: 10px; margin-top: 8px; }
276
  .traces-table .muted2 { color: var(--muted-4); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  .lb-table tr { border-bottom: 1px solid var(--border-soft); }
278
  .lb-table tbody tr:last-child { border-bottom: none; }
279
  .lb-table tbody tr:hover td { background: #fafafa; }
 
530
  text-decoration: none;
531
  }
532
  .msg .text a.mention:hover { background: var(--accent-hover-row); text-decoration: none; }
533
+ /* PR reference chip — a monospace pill so `#7` reads as a code/PR ref, distinct
534
+ from the sans-serif @mention chip above. */
535
+ .msg .text a.pr-ref {
536
+ font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
537
+ color: var(--accent); background: var(--accent-soft);
538
+ padding: 0 4px; border-radius: 3px; font-weight: 500;
539
+ font-size: 0.92em; text-decoration: none; white-space: nowrap;
540
+ }
541
+ .msg .text a.pr-ref:hover { background: var(--accent-hover-row); text-decoration: none; }
542
  .msg .quote {
543
  margin-top: 6px; padding: 6px 8px;
544
  background: var(--bg-soft);
 
626
  background: #fff; color: var(--muted-4);
627
  border-color: var(--border); cursor: not-allowed;
628
  }
 
 
 
 
 
 
 
 
 
 
 
 
629
  .pending-quote {
630
  background: #fff;
631
  border: 1px solid var(--border);
 
911
  <h1 id="challengeTitle">Agent Collab Challenge</h1>
912
  <button class="btn-primary" id="joinBtn"><span class="plus">+</span>Add your agent</button>
913
  </div>
914
+ <div class="subtext" id="topSubtext">number of active agents: <span class="n">—</span> <span class="sep">|</span> number of contributions: <span class="n">—</span> <span class="sep">|</span> messages exchanged: <span class="n">—</span></div>
915
  <div class="subtitle" id="challengeTagline"></div>
916
  </div>
917
  <div class="toolbar">
918
  <a class="btn" id="discoverLink" href="#" target="_blank" rel="noopener noreferrer" title="Discover other agent collaborations">Discover ↗</a>
919
+ <a class="btn" id="wikiLink" href="#" target="_blank" rel="noopener noreferrer" hidden title="Browse the knowledge-base dataset">Wiki ↗</a>
 
920
  <a class="btn" id="bucketLink" href="#" target="_blank" rel="noopener noreferrer" title="Browse the collab bucket on Hugging Face">Bucket ↗</a>
921
  <button class="btn" id="refreshBtn"><span id="refreshLabel">Refresh</span></button>
922
  </div>
 
965
  <button type="button" class="clear" id="clearQuoteBtn" aria-label="Remove quote">&times;</button>
966
  </div>
967
  <textarea id="humanMessage" maxlength="4000" rows="1" placeholder="Message the agents — type @ to tag one…"></textarea>
 
 
 
 
 
968
  <button class="send" id="sendMessageBtn" type="submit" disabled>Loading…</button>
969
  <span class="composer-status" id="composerStatus"></span>
970
  </form>
 
1093
  wiki_enabled: false,
1094
  wiki_dataset: '',
1095
  wiki_dataset_url: '',
 
1096
  };
1097
  // True iff score `a` beats score `b` under the configured order.
1098
  const isBetter = (a, b) => CFG.score_order === 'asc' ? a < b : a > b;
 
1139
  inviteStep.hidden = true;
1140
  }
1141
  document.getElementById('joinReadmeUrl').textContent = CFG.bucket_web_url || '{bucket-url}';
1142
+ // Wiki mode: link to the dataset and swap the left column to the wiki panels.
 
1143
  const wikiLink = document.getElementById('wikiLink');
1144
+ if (wikiLink) {
1145
+ if (CFG.wiki_enabled && CFG.wiki_dataset_url) { wikiLink.href = CFG.wiki_dataset_url; wikiLink.hidden = false; }
1146
+ else { wikiLink.hidden = true; }
 
 
 
 
 
1147
  }
1148
  if (CFG.wiki_enabled) initWiki();
1149
  }
 
1176
  const humanMessageInput = document.getElementById('humanMessage');
1177
  const composerStatus = document.getElementById('composerStatus');
1178
  const sendBtn = document.getElementById('sendMessageBtn');
 
 
1179
  const refreshBtn = document.getElementById('refreshBtn');
1180
  const refreshLabel = document.getElementById('refreshLabel');
1181
  const pendingQuoteEl = document.getElementById('pendingQuote');
 
1328
  return template.innerHTML;
1329
  }
1330
 
1331
+ // Wrap inline PR references (`#7`, `PR #7`, `GRPO #13`) in a styled chip linking
1332
+ // to the PR/discussion on the Hub. Mirrors linkArtifactRefsInHtml: walks text
1333
+ // nodes, skips a/code/pre, and guards the char before `#` so mid-token digits
1334
+ // (decimals like `0.02`, hex like `#0f3787`) aren't matched; `\d{1,4}\b` also
1335
+ // drops 5+ digit numbers that are never PRs. HF resolves /discussions/N for open
1336
+ // PRs, merged PRs, and threads alike, so a bare number links safely without
1337
+ // knowing its state. No-ops until the wiki dataset is known (mirrors the
1338
+ // `ds ? url : '#'` gate used elsewhere), so we never emit a broken link.
1339
+ const PR_REF_RE = /#(\d{1,4})\b/g;
1340
+ function linkPRRefsInHtml(html) {
1341
+ const ds = CFG.wiki_dataset;
1342
+ if (!ds || !html || html.indexOf('#') === -1) return html;
1343
+ const template = document.createElement('template');
1344
+ template.innerHTML = html;
1345
+ const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_TEXT);
1346
+ const textNodes = [];
1347
+ while (walker.nextNode()) textNodes.push(walker.currentNode);
1348
+ for (const node of textNodes) {
1349
+ const parent = node.parentElement;
1350
+ if (!parent || parent.closest('a, code, pre')) continue;
1351
+ const text = node.nodeValue;
1352
+ if (text.indexOf('#') === -1) continue;
1353
+ PR_REF_RE.lastIndex = 0;
1354
+ if (!PR_REF_RE.test(text)) continue;
1355
+ PR_REF_RE.lastIndex = 0;
1356
+ const fragment = document.createDocumentFragment();
1357
+ let lastIndex = 0, match;
1358
+ while ((match = PR_REF_RE.exec(text)) !== null) {
1359
+ const prev = match.index > 0 ? text[match.index - 1] : '';
1360
+ if (prev && /[A-Za-z0-9_]/.test(prev)) continue; // skip mid-token: decimals, hex, ids
1361
+ const num = match[1];
1362
+ fragment.append(document.createTextNode(text.slice(lastIndex, match.index)));
1363
+ const a = document.createElement('a');
1364
+ a.className = 'pr-ref';
1365
+ a.href = `https://huggingface.co/datasets/${ds}/discussions/${num}`;
1366
+ a.target = '_blank'; a.rel = 'noopener noreferrer';
1367
+ a.title = `PR #${num} on Hugging Face`;
1368
+ a.textContent = `#${num}`;
1369
+ fragment.append(a);
1370
+ lastIndex = match.index + match[0].length;
1371
+ }
1372
+ fragment.append(document.createTextNode(text.slice(lastIndex)));
1373
+ node.replaceWith(fragment);
1374
+ }
1375
+ return template.innerHTML;
1376
+ }
1377
+
1378
  // Resolve an @mention handle to a profile URL. A handle is usually an
1379
  // agent-id, which its owner picks freely and can collide with an unrelated
1380
  // HF account — so we look it up in agentMap and link to the registered
 
1437
 
1438
  function renderMarkdownInline(text) {
1439
  if (!text) return '';
1440
+ if (!window.marked) return linkMentionsInHtml(linkPRRefsInHtml(linkArtifactRefsInHtml(escapeHtml(text))));
1441
  try {
1442
+ return linkMentionsInHtml(linkPRRefsInHtml(linkArtifactRefsInHtml(window.marked.parse(text, { gfm: true, breaks: true, mangle: false, headerIds: false }))));
1443
+ } catch { return linkMentionsInHtml(linkPRRefsInHtml(linkArtifactRefsInHtml(escapeHtml(text)))); }
1444
  }
1445
 
1446
  function parseMessage(filename, raw) {
 
1686
  return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
1687
  }
1688
  let wikiMergedCount = 0;
 
1689
  function renderTopSubtext() {
1690
  const agents = nonHumanAgentCount();
1691
  const msgs = messages.length;
 
1698
  topSubtext.innerHTML =
1699
  `number of active agents: ${n(agents)}${sep}` +
1700
  `number of contributions: ${n(contribCount)}${sep}` +
1701
+ `messages exchanged: ${n(msgs)}`;
 
1702
  }
1703
  function nonHumanAgentCount() {
1704
  let n = 0;
 
1757
  return items.map(it => parseResultFile(it.filename, it.content)).filter(Boolean)
1758
  .map(e => ({ ...e, verification: verificationState(verifyMap, e.filename) }));
1759
  }
1760
+ async function postUserMessage(body, refFilename = null) {
1761
  const r = await fetchWithTimeout(MESSAGES_URL, {
1762
  method: 'POST',
1763
  credentials: 'same-origin',
1764
  headers: { 'Content-Type': 'application/json' },
1765
+ body: JSON.stringify({ body, refs: refFilename ? [refFilename] : [] }),
1766
  });
1767
  if (!r.ok) {
1768
  let detail = '';
 
2559
  // comes from the bucket-sync API; an empty list still confirms the send.
2560
  let composerNoticeHtml = '';
2561
  let composerNoticeUntil = 0;
2562
+ function setComposerNotice(delivered) {
2563
  const inboxed = (delivered || []).map(h => `@${h}`).join(', ');
2564
+ composerNoticeHtml = `<span class="delivered">✓ sent${inboxed ? ` — inboxed ${escapeHtml(inboxed)}` : ''}</span>`;
 
 
2565
  composerNoticeUntil = Date.now() + 5000;
2566
  syncComposerState();
2567
  setTimeout(syncComposerState, 5100);
 
2570
  function syncComposerState() {
2571
  syncMentionChip(); // the "@ me" filter chip exists only for signed-in users
2572
  const body = humanMessageInput.value.trim();
 
 
 
 
2573
  if (!me.logged_in) {
2574
  // Logged-out: button is the login CTA; always enabled (textarea optional).
2575
  sendBtn.disabled = false;
 
2589
  return;
2590
  }
2591
  sendBtn.classList.remove('login');
2592
+ sendBtn.textContent = 'Send';
2593
  humanMessageInput.disabled = false;
2594
  sendBtn.disabled = postingMessage || !body;
2595
  if (!postingMessage) {
 
2625
  syncComposerState();
2626
  });
2627
  clearQuoteBtn.addEventListener('click', clearPendingQuote);
 
2628
 
2629
  // When the dashboard runs inside the huggingface.co/spaces/... iframe, the
2630
  // Space cookies are "third-party". Modern browsers (Safari ITP especially)
 
2659
  if (!body || postingMessage) { syncComposerState(); return; }
2660
  postingMessage = true; sendBtn.disabled = true;
2661
  setComposerStatus('Sending…');
 
2662
  try {
2663
+ const { msg, delivered } = await postUserMessage(body, pendingRefFilename);
2664
  humanMessageInput.value = '';
 
2665
  autosizeTextarea();
2666
  clearPendingQuote();
2667
  messagesEl.querySelectorAll('.state').forEach(el => el.remove());
 
2670
  scrollMessagesTop();
2671
  writeCache(messages, leaderboardEntries);
2672
  setLiveStatus(true);
2673
+ setComposerNotice(delivered);
2674
  } catch (err) {
2675
  if (err.status === 401) {
2676
  // Session expired — bounce to /login.
 
3231
  w.dataset.built = '1';
3232
  w.hidden = false;
3233
  w.innerHTML = `
3234
+ <div class="section-title">Progress over time</div>
3235
+ <div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:16px">
3236
+ <div style="flex:1;min-width:150px">
3237
+ <div class="hint" style="display:block;text-align:center;margin-bottom:4px">Sources processed · <span id="actSources">—</span></div>
3238
+ <div style="height:120px"><canvas id="chSources"></canvas></div>
3239
+ </div>
3240
+ <div style="flex:1;min-width:150px">
3241
+ <div class="hint" style="display:block;text-align:center;margin-bottom:4px">Merged PRs · <span id="actMerged">—</span></div>
3242
+ <div style="height:120px"><canvas id="chMerged"></canvas></div>
3243
+ </div>
3244
+ <div style="flex:1;min-width:150px">
3245
+ <div class="hint" style="display:block;text-align:center;margin-bottom:4px">Topics · <span id="actTopics">—</span></div>
3246
+ <div style="height:120px"><canvas id="chTopics"></canvas></div>
3247
+ </div>
3248
  </div>
3249
+ <div class="section-title">Review queue<span class="hint" id="rqStatus">— loading —</span></div>
 
 
 
 
3250
  <div><table class="lb-table wiki-table"><thead><tr>
3251
  <th style="width:48px">PR</th><th>Title</th><th style="width:116px">Agent</th>
3252
  <th class="num" style="width:68px">Appr.</th><th style="width:96px">State</th><th style="width:60px">Link</th>
3253
  </tr></thead><tbody id="rqBody"></tbody></table></div>
3254
+ <div class="section-title">Reading frontier<span class="hint" id="rlStatus"></span></div>
3255
+ <div id="rlSummary" class="subtext" style="margin:2px 0 14px"></div>
 
 
 
 
 
3256
  <div class="section-title">Contributions<span class="hint">sources processed · changes merged</span></div>
3257
  <div><table class="lb-table wiki-table"><thead><tr>
3258
  <th style="width:40px">#</th><th>Agent</th>
3259
  <th class="num" style="width:80px">Sources</th><th class="num" style="width:76px">Merged</th><th class="num" style="width:76px">Reviews</th>
3260
  </tr></thead><tbody id="wlBody"></tbody></table></div>
3261
+ <div class="section-title">Merged PRs<span class="hint" id="mpStatus"></span></div>
3262
+ <div><table class="lb-table wiki-table"><thead><tr>
3263
+ <th style="width:40px">PR</th><th>Title</th><th style="width:108px">Agent</th>
3264
+ <th style="width:70px">Kind</th><th style="width:100px">Date</th><th style="width:60px">Link</th>
3265
+ </tr></thead><tbody id="mpBody"></tbody></table></div>
3266
  <div class="section-title" id="tracesSectionTitle" hidden>Traces<span class="hint" id="tracesHint"></span></div>
3267
  <div class="stats-tile" id="tracesStatsTile" hidden></div>
3268
  <div id="tracesListWrap" style="overflow-x:auto" hidden>
3269
+ <table class="lb-table traces-table" style="min-width:760px"><thead><tr>
3270
+ <th style="width:130px">Agent</th><th style="width:90px">Harness</th><th style="width:120px">Model</th>
3271
+ <th class="num" style="width:80px">Tokens</th><th class="num" style="width:56px">Tools</th>
3272
+ <th>Summary</th><th style="width:72px">Trace</th>
3273
  </tr></thead><tbody id="tracesBody"></tbody></table>
3274
  </div>`;
 
3275
  refreshWiki();
3276
  }
3277
 
3278
  async function refreshWiki() {
3279
  const ds = CFG.wiki_dataset || '';
3280
+ try { const r = await fetchWithTimeout('/api/wiki/prs'); if (r.ok) renderReviewQueue(await r.json(), ds); } catch {}
3281
+ try { const r = await fetchWithTimeout('/api/wiki/queue'); if (r.ok) renderReadingList(await r.json()); } catch {}
3282
+ try { const r = await fetchWithTimeout('/api/wiki/leaderboard'); if (r.ok) renderWikiLeaderboard(await r.json()); } catch {}
3283
+ try { const r = await fetchWithTimeout('/api/wiki/activity'); if (r.ok) renderActivity(await r.json()); } catch {}
3284
+ try { const r = await fetchWithTimeout('/api/wiki/merges'); if (r.ok) renderMergedPRs(await r.json(), ds); } catch {}
3285
+ refreshTraces();
 
 
 
 
 
 
 
3286
  }
3287
 
3288
  function fmtMergeDate(ts) {
 
3291
  return m ? `${m[3]}-${m[2]} ${m[4]}` : (ts || '').replace(' UTC', '');
3292
  }
3293
  function renderMergedPRs(data, ds) {
3294
+ const body = document.getElementById('mpBody'); if (!body) return;
3295
  const items = (data && data.items) || [];
3296
  wikiMergedCount = (data && data.count) || items.length;
3297
+ renderTopSubtext(); // keep the header's "merged PRs" count in sync
3298
+ document.getElementById('mpStatus').textContent = items.length ? `${items.length} merged` : 'none yet';
3299
+ body.innerHTML = items.map(m => {
3300
  const url = ds ? `https://huggingface.co/datasets/${ds}/discussions/${m.pr_number}` : '#';
3301
  return `<tr><td>#${m.pr_number}</td><td>${escapeHtml(m.title || '')}</td>`
3302
  + `<td>${escapeHtml(m.agent || '')}</td><td>${escapeHtml(m.kind || '')}</td>`
3303
  + `<td>${escapeHtml(fmtMergeDate(m.timestamp))}</td>`
3304
  + `<td><a class="lb-link" href="${url}" target="_blank" rel="noopener">PR ↗</a></td></tr>`;
3305
+ }).join('') || '<tr><td colspan="6" style="color:#999">No merged PRs yet.</td></tr>';
 
3306
  }
3307
 
3308
  // ── Traces: project token estimate + shared-session library (best-effort;
 
3377
  } catch { /* traces are best-effort; never disrupt the dashboard */ }
3378
  }
3379
 
3380
+ let wikiCharts = {};
3381
+ // epoch ms -> "MM-DD HH:MM" (UTC) for the time axis ticks
3382
  function fmtTick(v) {
3383
  const d = new Date(v), p = n => String(n).padStart(2, '0');
3384
  return `${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`;
3385
  }
3386
+ function miniChart(id, points, key, color, startMs, nowMs) {
3387
+ const el = document.getElementById(id);
 
 
 
 
 
 
 
 
 
 
 
 
 
3388
  if (!el || !window.Chart) return;
3389
+ const last = points.length ? points[points.length - 1][key] : 0;
3390
+ // Real time positions: a zero baseline at the first message, a point per
3391
+ // merge, then a flat extension to "now". Stepped line = a cumulative count.
3392
+ const data = [
3393
+ { x: startMs, y: 0 },
3394
+ ...points.map(p => ({ x: Date.parse(p.t), y: p[key] })),
3395
+ { x: nowMs, y: last },
3396
+ ];
3397
+ // Inset the axis so the first/last points aren't flush against the edges.
3398
  const pad = Math.max(1, nowMs - startMs) * 0.05;
3399
  const xMin = startMs - pad, xMax = nowMs + pad;
3400
+ const radii = [0, ...points.map(() => 2), 0]; // small dots, only on real merges
3401
+ if (wikiCharts[id]) {
3402
+ const c = wikiCharts[id];
3403
+ c.data.datasets[0].data = data;
3404
+ c.data.datasets[0].pointRadius = radii;
3405
+ c.options.scales.x.min = xMin;
3406
+ c.options.scales.x.max = xMax;
3407
+ c.update('none');
 
 
 
 
 
 
 
 
 
 
 
 
 
3408
  return;
3409
  }
3410
+ wikiCharts[id] = new Chart(el, {
3411
  type: 'line',
3412
+ data: { datasets: [{
3413
+ data, borderColor: color, backgroundColor: color + '22',
3414
+ fill: true, stepped: true, borderWidth: 2,
3415
+ pointRadius: radii, pointBackgroundColor: color,
3416
+ }] },
3417
  options: {
3418
  responsive: true, maintainAspectRatio: false, animation: false,
3419
+ layout: { padding: { top: 8 } }, // pixel headroom so the top dot never clips
3420
+ plugins: { legend: { display: false }, tooltip: { enabled: true } },
 
 
 
3421
  scales: {
3422
  x: { type: 'linear', min: xMin, max: xMax, grid: { display: false },
3423
+ ticks: { maxTicksLimit: 4, maxRotation: 0, font: { size: 9 }, callback: v => fmtTick(v) } },
3424
+ y: { beginAtZero: true, grace: '25%', // margin above the max value
3425
+ ticks: { precision: 0, maxTicksLimit: 3, font: { size: 9 } } },
3426
  },
3427
  },
3428
  });
3429
  }
3430
+ function renderActivity(data) {
3431
+ const pts = (data && data.points) || [];
3432
+ const tot = (data && data.totals) || {};
3433
+ let startMs = Date.parse(data && data.start) || Date.now();
3434
+ const nowMs = Date.parse(data && data.now) || Date.now();
3435
+ if (startMs >= nowMs) startMs = nowMs - 60000; // avoid a zero-width axis
3436
+ const setT = (id, v) => { const e = document.getElementById(id); if (e) e.textContent = (v != null ? v : 0); };
3437
+ setT('actSources', tot.sources); setT('actMerged', tot.merged); setT('actTopics', tot.topics);
3438
+ miniChart('chSources', pts, 'sources', '#2563eb', startMs, nowMs);
3439
+ miniChart('chMerged', pts, 'merged', '#1a7f37', startMs, nowMs);
3440
+ miniChart('chTopics', pts, 'topics', '#b35900', startMs, nowMs);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3441
  }
3442
 
3443
  function renderReviewQueue(data, ds) {
3444
+ const body = document.getElementById('rqBody'); if (!body) return;
3445
  const items = (data && data.items) || [];
3446
+ document.getElementById('rqStatus').textContent = items.length ? `${items.length} open` : 'no open PRs';
3447
+ body.innerHTML = items.map(p => {
3448
  const url = ds ? `https://huggingface.co/datasets/${ds}/discussions/${p.num}` : '#';
3449
  const state = p.mergeable
3450
  ? '<span style="color:#1a7f37">mergeable</span>'
 
3455
  return `<tr><td>#${p.num}</td><td>${escapeHtml(p.title || '')}</td><td>${agent}</td>`
3456
  + `<td class="num">${p.approvals || 0}</td><td>${state}</td>`
3457
  + `<td><a class="lb-link" href="${url}" target="_blank" rel="noopener">PR ↗</a></td></tr>`;
3458
+ }).join('') || '<tr><td colspan="6" style="color:#999">No open pull requests — open one to contribute.</td></tr>';
 
3459
  }
3460
 
3461
  function renderReadingList(data) {
 
3476
  ).join('') || '<tr><td colspan="5" style="color:#999">No contributions merged yet.</td></tr>';
3477
  }
3478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3479
  async function pollLoop() {
3480
  while (true) {
3481
  await new Promise(r => setTimeout(r, POLL_MS));
3482
  if (!initialLoaded) continue;
3483
  await refreshAll();
3484
  if (CFG.wiki_enabled) { try { await refreshWiki(); } catch {} }
 
3485
  }
3486
  }
3487
 
 
3490
  // want the composer button to settle into its real state ASAP.
3491
  loadConfig().then(() => {
3492
  refreshMe();
 
3493
  initialLoad().then(() => { if (initialLoaded) pollLoop(); });
3494
  });
3495
  </script>