Spaces:
Running
Running
Linkify PR references in the message board
#2
by thomwolf HF Staff - opened
- 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>
|
| 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
|
| 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">×</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
|
| 1174 |
-
// the dataset as a fallback); Dataset is the loadable knowledge base.
|
| 1175 |
const wikiLink = document.getElementById('wikiLink');
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 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)}
|
| 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
|
| 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] : []
|
| 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
|
| 2557 |
const inboxed = (delivered || []).map(h => `@${h}`).join(', ');
|
| 2558 |
-
composerNoticeHtml =
|
| 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 =
|
| 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
|
| 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
|
| 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<
|
| 3238 |
-
<div style="
|
| 3239 |
-
<div style="
|
| 3240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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="
|
| 3268 |
-
<th>Agent</th><th>Harness</th><th>Model</th>
|
| 3269 |
-
<th class="num">Tokens</th><th class="num">Tools</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 |
-
|
| 3279 |
-
|
| 3280 |
-
const
|
| 3281 |
-
|
| 3282 |
-
|
| 3283 |
-
|
| 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 |
-
|
| 3300 |
const items = (data && data.items) || [];
|
| 3301 |
wikiMergedCount = (data && data.count) || items.length;
|
| 3302 |
-
renderTopSubtext(); // keep the header's "
|
| 3303 |
-
|
| 3304 |
-
|
| 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
|
| 3387 |
-
// epoch ms -> "MM-DD HH:MM" (UTC) for the time
|
| 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 |
-
|
| 3393 |
-
|
| 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
|
| 3411 |
-
|
| 3412 |
-
|
| 3413 |
-
|
| 3414 |
-
|
| 3415 |
-
|
| 3416 |
-
|
| 3417 |
-
|
| 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 |
-
|
| 3434 |
type: 'line',
|
| 3435 |
-
data: { datasets
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3436 |
options: {
|
| 3437 |
responsive: true, maintainAspectRatio: false, animation: false,
|
| 3438 |
-
|
| 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:
|
| 3446 |
-
y: { beginAtZero: true, grace: '
|
| 3447 |
-
ticks: { precision: 0, font: { size: 9 } } },
|
| 3448 |
},
|
| 3449 |
},
|
| 3450 |
});
|
| 3451 |
}
|
| 3452 |
-
|
| 3453 |
-
|
| 3454 |
-
|
| 3455 |
-
|
| 3456 |
-
const
|
| 3457 |
-
|
| 3458 |
-
|
| 3459 |
-
|
| 3460 |
-
|
| 3461 |
-
|
| 3462 |
-
|
| 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 |
-
|
| 3482 |
const items = (data && data.items) || [];
|
| 3483 |
-
|
| 3484 |
-
|
| 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">×</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>
|