diff --git a/app/main.py b/app/main.py index 07b74c0..a7751dc 100644 --- a/app/main.py +++ b/app/main.py @@ -122,6 +122,12 @@ async def api_usage(request: Request): data = await broker_get(identity, "/broker/usage") keys = await broker_get(identity, "/broker/keys") data["keys"] = keys.get("keys", []) + # Co-op-wide model popularity (aggregate, anonymized) for the leaderboard. + try: + board = await broker_get(identity, "/broker/model-leaderboard") + data["coop_models"] = board.get("models", []) + except HTTPException: + data["coop_models"] = [] # best-effort; never block the main view return data except HTTPException as e: return JSONResponse({"error": e.detail}, status_code=e.status_code) @@ -318,6 +324,24 @@ PAGE_HTML = """ .model-name { font-weight: 600; font-size: 13px; } .model-sub { color: var(--muted); font-size: 12px; } .model-spend { font-size: 13px; font-weight: 600; color: var(--ink); text-align: right; } + + /* Leaderboard */ + .board-row { + display: flex; align-items: flex-start; gap: 14px; + padding: 12px 0; border-top: 1px solid #f0ede6; + } + .board-rank { + width: 26px; height: 26px; border-radius: 50%; + background: #eaf3ea; color: #2f5c30; font-weight: 700; font-size: 13px; + display: flex; align-items: center; justify-content: center; flex-shrink: 0; + margin-top: 2px; + } + .board-row:first-child .board-rank { background: var(--green-deep); color: #fff; } + .board-main { flex: 1; min-width: 0; } + .board-name { font-weight: 600; font-size: 13px; margin-bottom: 5px; } + .board-bar { height: 6px; background: #eef0ea; border-radius: 4px; overflow: hidden; } + .board-fill { height: 100%; background: var(--green-mid); border-radius: 4px; } + .board-sub { color: var(--muted); font-size: 11px; margin-top: 4px; } .keys { margin-top: 8px; } .key-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; @@ -461,6 +485,12 @@ PAGE_HTML = """
+
+

Popular in the co-op

+

Which models members are using most, across the whole co-op. A starting point when choosing one.

+
+
+

API keys

Keys let you use the co-op's AI from your own tools. They draw from the same balance as chat.

@@ -538,6 +568,7 @@ async function load() { $('calls').textContent = calls.toLocaleString(); renderModels(d.models || []); + renderBoard(d.coop_models || []); renderKeys(d.keys || []); } catch (e) { flash('Could not load usage: ' + e.message, false); @@ -557,6 +588,27 @@ function renderModels(models) { }).join(''); } +function renderBoard(models) { + const el = $('coop-board'); + if (!models.length) { + el.innerHTML = '
Not enough usage data yet — check back soon.
'; + return; + } + // Rank bar widths are relative to the top model's request count. + const top = Math.max(...models.map(m => m.requests || 0), 1); + el.innerHTML = models.map((m, i) => { + const pct = Math.round(((m.requests || 0) / top) * 100); + return '
' + + '
' + (i + 1) + '
' + + '
' + + '
' + escapeHtml(m.model) + '
' + + '
' + + '
' + (m.requests || 0).toLocaleString() + ' requests by members
' + + '
' + + '
'; + }).join(''); +} + function renderKeys(keys) { const el = $('keys'); if (!keys.length) {