/* ========================================================================= dashboard.js — the user's DreamRouter account view • auth guard: Google session cookie only (no API-key sign-in exists) • loads YOUR usage (GET /api/me/billing) + model list (GET /v1/models) • renders usage stat cards, model grid, and a streaming playground • full state coverage: loading skeletons → populated → empty → error ========================================================================= */ (async function () { 'use strict'; // where the router lives (same origin when deployed to HF Space) var BASE = window.location.origin; var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ---- auth guard -------------------------------------------------------- */ // The ONLY way in is a Google session cookie. Probe it via /auth/me. // (No sessionStorage key path anymore — API-key login was removed.) var greet = document.getElementById('greet'); var ssoUser = null; try { var res = await fetch('/auth/me'); if (res.ok) { var me = await res.json(); ssoUser = me.username || me.email || null; // google display name } else { // not signed in → back to the Google-only login screen window.location.replace('login.html'); return; } } catch (e) { window.location.replace('login.html'); return; } // personalise the greeting if (ssoUser) { greet.textContent = 'welcome, ' + ssoUser; } /* ---- element refs ------------------------------------------------------ */ var statsRow = document.getElementById('stats-row'); var modelGrid = document.getElementById('model-grid'); var pgForm = document.getElementById('pg-form'); var pgModel = document.getElementById('pg-model'); var pgTokens = document.getElementById('pg-tokens'); var pgPrompt = document.getElementById('pg-prompt'); var pgOutput = document.getElementById('pg-output'); var pgSend = document.getElementById('pg-send'); var pgClear = document.getElementById('pg-clear'); var pgStatus = document.getElementById('pg-status'); var logout = document.getElementById('logout'); var endpointMini = document.getElementById('endpoint-mini'); // show the real endpoint in the header pill endpointMini.textContent = BASE + '/v1'; /* ---- logout ------------------------------------------------------------ */ logout.addEventListener('click', function () { // clear the Google session cookie server-side, then return to login fetch('/auth/logout', { method: 'POST' }).catch(function () {}).finally(function () { window.location.replace('login.html'); // back to sign-in }); }); /* ---- small svg icon set (inline, no external dep) --------------------- */ var ICON = { requests: '', tokens: '', alltime: '', models: '', down: '' }; function svgIcon(name, label) { return ''; } /* ---- stats: render skeletons first, then real data -------------------- */ function renderSkeletons() { statsRow.innerHTML = ( statCardSkeleton() + statCardSkeleton() + statCardSkeleton() + statCardSkeleton() ); } function statCardSkeleton() { return '
' + '
' + '
' + '
' + '
'; } // render the 4 USER-facing stat cards (no operator/routing-health metrics) function renderStats(usage, modelCount) { var today = (usage && usage.today) || {}; var total = (usage && usage.total) || {}; var reqToday = today.requests || 0; var tokToday = (today.tok_in || 0) + (today.tok_out || 0); var reqAll = total.requests || 0; statsRow.innerHTML = statCard('requests', reqToday.toLocaleString(), 'requests today', '') + statCard('tokens', tokToday.toLocaleString(), 'tokens today', '') + statCard('alltime', reqAll.toLocaleString(), 'all-time requests', '') + statCard('models', modelCount.toLocaleString(), 'models available', ''); } function statCard(icon, num, label, cls) { return '
' + svgIcon(icon) + '
' + num + '
' + '
' + label + '
' + '
'; } function renderStatsError() { statsRow.innerHTML = '
' + svgIcon('down', 'error') + '
Could not load your usage
' + '
Check your connection, then sign in again.
' + '
'; } /* ---- models: skeleton → grid ------------------------------------------ */ function renderModelSkeletons() { var html = ''; for (var i = 0; i < 12; i++) { html += '
'; } modelGrid.innerHTML = html; } function renderModels(list) { if (!list || !list.length) { // empty state: headline + explanation (state-coverage craft) modelGrid.innerHTML = '
' + '

No models loaded

' + '

The router is up but returned no models. Try again in a moment.

'; return; } var html = ''; for (var i = 0; i < list.length; i++) { var m = list[i]; var isAnthropic = m.id.indexOf('anthropic/') === 0; var isFree = m.id.indexOf('free:') === 0; // free-tier models (no credit cost) html += '
' + '' + escapeHtml(m.id) + '' + (isFree ? 'free' : '' + (m.targets || 0) + ' targets') + '
'; } modelGrid.innerHTML = html; // click a model chip → load it into the playground var chips = modelGrid.querySelectorAll('.model-chip'); for (var j = 0; j < chips.length; j++) { chips[j].addEventListener('click', function () { var id = this.getAttribute('data-model'); pgModel.value = id; // select it pgPrompt.focus(); // jump to the prompt if (!reduceMotion) pgPrompt.scrollIntoView({ behavior: 'smooth', block: 'center' }); }); } } /* ---- populate the playground model dropdown --------------------------- */ function fillModelDropdown(list) { var html = ''; for (var i = 0; i < list.length; i++) { html += ''; } pgModel.innerHTML = html; // default to a small fast free model if present var preferred = 'free:liquid/lfm-2.5-1.2b-instruct'; if ([].some.call(pgModel.options, function (o) { return o.value === preferred; })) { pgModel.value = preferred; } } /* ---- escape helpers (prevent HTML injection in user-visible strings) -- */ function escapeHtml(s) { return String(s).replace(/[&<>]/g, function (c) { return c === '&' ? '&' : c === '<' ? '<' : '>'; }); } function escapeAttr(s) { return String(s).replace(/"/g, '"'); } /* ---- load everything --------------------------------------------------- */ function load() { renderSkeletons(); renderModelSkeletons(); // fetch the user's own usage (cookie-authed); 401 → billing not provisioned yet var billingPromise = fetch(BASE + '/api/me/billing', { credentials: 'same-origin' }) .then(function (r) { return r.ok ? r.json() : null; }) .catch(function () { return null; }); // fetch model list (public, no auth) var modelsPromise = fetch(BASE + '/v1/models') .then(function (r) { return r.json(); }) .catch(function () { return { data: [] }; }); Promise.all([billingPromise, modelsPromise]).then(function (results) { var billing = results[0]; var modelsData = results[1]; var list = (modelsData && modelsData.data) || []; var modelCount = list.length; // usage stats (graceful zeros if billing isn't ready) renderStats(billing && billing.usage, modelCount); // populate the prominent wallet hero (ETH address + QR + key) if (billing && billing.deposit_address) { renderWallet(billing.deposit_address, billing.api_key); } // model dropdown + grid fillModelDropdown(list); renderModels(list); }).catch(function () { renderStatsError(); }); } /* ---- wallet hero: show the user's ETH address prominently + QR + key --- */ var walletHero = document.getElementById('wallet-hero'); var walletAddr = document.getElementById('wallet-addr'); var walletQr = document.getElementById('wallet-qr'); var walletCopy = document.getElementById('wallet-copy'); var walletCopyOk = document.getElementById('wallet-copyok'); var khKey = document.getElementById('kh-key'); var khCopy = document.getElementById('kh-copy'); /* build a crisp, themeable SVG QR for a string (returns true on success) */ function renderQR(text, container) { if (!window.qrcode) return false; // lib didn't load → degrade to text try { var qr = qrcode(0, 'M'); // type 0 = auto-size, error-correction M qr.addData(text); qr.make(); var count = qr.getModuleCount(); // grid width in modules var cell = 6, margin = 2; // px/module, quiet-zone modules var size = (count + margin * 2) * cell; var parts = ['']; parts.push(''); for (var r = 0; r < count; r++) { // row for (var c = 0; c < count; c++) { // col if (qr.isDark(r, c)) { // dark module → ink rect parts.push(''); } } } parts.push(''); container.innerHTML = parts.join(''); // inject the SVG return true; } catch (e) { // encode error → degrade return false; } } function renderWallet(depositAddress, apiKey) { if (!walletHero || !depositAddress) return; // hero not on this page walletAddr.textContent = depositAddress; // the full checksummed address renderQR(depositAddress, walletQr); // scannable QR (graceful if lib missing) if (apiKey && khKey) khKey.textContent = apiKey; // secondary: the sk- key walletHero.hidden = false; // reveal the hero // copy the ETH address (for deposits) walletCopy.addEventListener('click', function () { navigator.clipboard.writeText(depositAddress).then(function () { walletCopyOk.hidden = false; var orig = walletCopy.textContent; walletCopy.textContent = 'Copied!'; setTimeout(function () { walletCopy.textContent = orig; walletCopyOk.hidden = true; }, 1500); }).catch(function () { /* clipboard blocked — address still selectable */ }); }); // copy the API key (sk-addr) if (khCopy && apiKey) { khCopy.addEventListener('click', function () { navigator.clipboard.writeText(apiKey).then(function () { var orig = khCopy.textContent; khCopy.textContent = 'Copied!'; setTimeout(function () { khCopy.textContent = orig; }, 1500); }).catch(function () { /* blocked */ }); }); } } /* ---- playground: streaming chat (cookie-authed, no bearer key) -------- */ function setStatus(msg, kind) { pgStatus.textContent = msg; pgStatus.className = 'pg-status' + (kind ? ' is-' + kind : ''); } pgClear.addEventListener('click', function () { pgOutput.innerHTML = ''; pgPrompt.value = ''; setStatus('', ''); pgPrompt.focus(); }); pgForm.addEventListener('submit', function (e) { e.preventDefault(); var model = pgModel.value; var prompt = pgPrompt.value.trim(); if (!model) { setStatus('Pick a model first.', 'error'); return; } if (!prompt) { setStatus('Write a message to send.', 'error'); pgPrompt.focus(); return; } pgSend.classList.add('is-loading'); pgOutput.innerHTML = ''; // blinking caret while waiting setStatus('Routing…'); var body = JSON.stringify({ model: model, messages: [{ role: 'user', content: prompt }], max_tokens: parseInt(pgTokens.value, 10) || 256, stream: true }); // cookie-authed: the Google session cookie is sent automatically (same-origin) fetch(BASE + '/v1/chat/completions', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: body }).then(function (res) { if (res.status === 401 || res.status === 403) { throw new Error('Your session expired. Please sign in again.'); } if (res.status === 402) { throw new Error('OUT_OF_CREDITS'); } if (res.status === 404) { throw new Error('That model is not available. Pick another from the list.'); } if (res.status === 503) { throw new Error('All targets are cooling down. Try again in a minute.'); } if (!res.ok) { throw new Error('The router returned ' + res.status + '. Try again.'); } return res.body.getReader(); }).then(function (reader) { pgSend.classList.remove('is-loading'); setStatus('Streaming…', 'ok'); var decoder = new TextDecoder(); var acc = ''; var buf = ''; function pump() { reader.read().then(function (chunk) { if (chunk.done) { // stream ended — clear the caret, finalize pgOutput.innerHTML = escapeHtml(acc) || '(empty response)'; setStatus('Done.', 'ok'); return; } buf += decoder.decode(chunk.value, { stream: true }); // parse SSE: only lines starting with "data:" carry JSON var lines = buf.split('\n'); buf = lines.pop(); // keep the last partial line for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (line.indexOf('data:') !== 0) continue; // skip non-data lines var payload = line.slice(5).trim(); if (payload === '[DONE]') continue; // end-of-stream marker try { var json = JSON.parse(payload); var delta = json.choices && json.choices[0] && json.choices[0].delta; if (delta && delta.content) acc += delta.content; } catch (err) { /* partial JSON; ignore */ } } // live-render with a trailing caret pgOutput.innerHTML = escapeHtml(acc) + ''; pump(); }).catch(function (err) { setStatus(err.message, 'error'); }); } pump(); }).catch(function (err) { pgSend.classList.remove('is-loading'); pgOutput.innerHTML = ''; if (err.message === 'OUT_OF_CREDITS') { setStatus('No credits remaining. Send ETH to your deposit address to top up.', 'error'); // scroll to the billing section so the deposit address is visible var bs = document.getElementById('billing-section'); if (bs && !reduceMotion) bs.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } setStatus(err.message || 'Network error. Check your connection.', 'error'); if (/sign in again/i.test(err.message || '')) { setTimeout(function () { window.location.replace('login.html'); }, 1500); } }); }); // kick it off load(); })();