GGUFGuy's picture
Duplicate from GGUFGuy/hyperdex-trainer
2a49d9a
Raw History Blame Contribute Delete
6.82 kB
/* NanoDex — shared frontend helpers */
const NX = {
fmtTokens(n){ n = Number(n||0);
if(n>=1e9) return (n/1e9).toFixed(2)+'B';
if(n>=1e6) return (n/1e6).toFixed(n>=1e7?0:1).replace(/\.0$/,'')+'M';
if(n>=1e3) return Math.round(n/1e3)+'k';
return String(n); },
fmtParams(n){ n = Number(n||0);
return n>=1e6 ? (n/1e6).toFixed(2)+'M' : Math.round(n/1e3)+'k'; },
fmtTime(s){ s = Number(s||0); if(!isFinite(s)||s<0) return '—';
if(s<90) return Math.round(s)+'s';
if(s<5400) return Math.round(s/60)+' min';
return (s/3600).toFixed(1)+' h'; },
ago(ts){ if(!ts) return '—';
const d = Date.now()/1000 - ts;
if(d<60) return 'just now';
if(d<3600) return Math.floor(d/60)+'m ago';
if(d<86400) return Math.floor(d/3600)+'h ago';
return Math.floor(d/86400)+'d ago'; },
esc(s){ const d=document.createElement('div'); d.textContent=s==null?'':String(s);
return d.innerHTML; },
chip(status, extra){
const label = {queued:'queued', running:'training', done:'completed',
failed:'failed', cancelled:'cancelled'}[status] || status;
return `<span class="chip ${status}"><i></i>${label}${extra?' · '+NX.esc(extra):''}</span>`;
},
async get(url){
const r = await fetch(url, {headers:{'Accept':'application/json'}});
if(!r.ok) throw new Error((await r.json().catch(()=>({}))).error || r.statusText);
return r.json();
},
async post(url, body){
const r = await fetch(url, {method:'POST',
headers:{'Content-Type':'application/json'}, body: JSON.stringify(body||{})});
const j = await r.json().catch(()=>({}));
if(!r.ok) throw new Error(j.error || r.statusText);
return j;
},
toast(msg, kind){
let t = document.querySelector('.toast');
if(!t){ t = document.createElement('div'); t.className='toast';
document.body.appendChild(t); }
t.className = 'toast ' + (kind||'');
t.innerHTML = msg;
requestAnimationFrame(()=> t.classList.add('on'));
clearTimeout(t._h);
t._h = setTimeout(()=> t.classList.remove('on'), 5200);
},
runRow(j){
const pct = (j.progress*100).toFixed(1);
const bits = [];
if(j.status==='running'){
bits.push(`<span><b>${pct}%</b> of ${NX.fmtTokens(j.target_tokens)}</span>`);
if(j.loss) bits.push(`<span>loss <b>${j.loss.toFixed(4)}</b></span>`);
bits.push(`<span><b>${Math.round(j.tok_per_s/1e3)}k</b> tok/s</span>`);
if(j.eta_s) bits.push(`<span><b>${NX.fmtTime(j.eta_s)}</b> left</span>`);
} else if(j.status==='queued'){
bits.push(`<span>position <b>#${j.queue_position||'?'}</b></span>`);
bits.push(`<span><b>${NX.fmtTokens(j.target_tokens)}</b> tokens queued</span>`);
} else if(j.status==='done'){
bits.push(`<span><b>${NX.fmtTokens(j.tokens_seen)}</b> tokens</span>`);
if(j.loss) bits.push(`<span>final loss <b>${j.loss.toFixed(4)}</b></span>`);
bits.push(`<span>${j.pushed_repo ? 'published' : 'not published'}</span>`);
} else {
bits.push(`<span>${NX.esc(j.error||'—')}</span>`);
}
const showBar = j.status==='running';
return `<a class="run" href="/models/${j.id}">
<div class="run-top">
<h4>${NX.esc(j.name)}</h4>
${NX.chip(j.status)}
</div>
<div class="run-meta">
<span class="mono">${NX.esc(j.tier_label)}</span>
<span>${Number(j.n_params).toLocaleString()} params</span>
${bits.join('')}
<span class="muted">${NX.ago(j.created_at)}</span>
</div>
${showBar ? `<div class="bar"><i style="width:${pct}%"></i></div>` : ''}
</a>`;
},
/* Minimal dependency-free loss chart.
The viewBox is set to the element's real pixel size so that 1 user unit ==
1 CSS pixel: a fixed 760-wide viewBox squeezed into a 280px phone would
render the axis labels at ~4px. */
lossChart(el, pts){
if(!el) return;
el._pts = pts;
const rect = el.getBoundingClientRect();
const W = Math.max(280, Math.round(rect.width) || 760);
const H = Math.max(170, Math.round(rect.height) || 280);
const narrow = W < 460;
const PL = narrow ? 40 : 54, PR = narrow ? 8 : 14;
const PT = 16, PB = narrow ? 28 : 34;
const TICKS = narrow ? 3 : 4;
el.setAttribute('viewBox', `0 0 ${W} ${H}`);
if(!pts || pts.length < 2){
el.innerHTML = `<text x="${W/2}" y="${H/2}" class="ph"
text-anchor="middle">waiting for the first metrics…</text>`;
return;
}
const xs = pts.map(p=>p.tokens), ys = pts.map(p=>p.loss);
const x0=Math.min(...xs), x1=Math.max(...xs);
let y0=Math.min(...ys), y1=Math.max(...ys);
const padY=(y1-y0)*0.12 || 0.4; y0-=padY; y1+=padY;
const sx = v => PL + (v-x0)/((x1-x0)||1)*(W-PL-PR);
const sy = v => PT + (1-(v-y0)/((y1-y0)||1))*(H-PT-PB);
const d = pts.map((p,i)=> (i?'L':'M')+sx(p.tokens).toFixed(1)+' '+sy(p.loss).toFixed(1)).join(' ');
const area = d + ` L${sx(x1).toFixed(1)} ${H-PB} L${sx(x0).toFixed(1)} ${H-PB} Z`;
let g='';
for(let i=0;i<=TICKS;i++){
const v = y0+(y1-y0)*i/TICKS, y = sy(v);
g += `<line class="gl" x1="${PL}" y1="${y.toFixed(1)}" x2="${W-PR}" y2="${y.toFixed(1)}"/>`;
g += `<text class="ax" x="${PL-7}" y="${(y+3.5).toFixed(1)}" text-anchor="end">${v.toFixed(2)}</text>`;
}
for(let i=0;i<=TICKS;i++){
const v = x0+(x1-x0)*i/TICKS, x = sx(v);
const anchor = i===0 ? 'start' : (i===TICKS ? 'end' : 'middle');
g += `<text class="ax" x="${x.toFixed(1)}" y="${H-PB+18}" text-anchor="${anchor}">${NX.fmtTokens(v)}</text>`;
}
const last = pts[pts.length-1];
el.innerHTML = `<defs><linearGradient id="lg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2ee6a8" stop-opacity=".22"/>
<stop offset="100%" stop-color="#2ee6a8" stop-opacity="0"/>
</linearGradient></defs>
${g}
<path class="ar" d="${area}"/>
<path class="ln" d="${d}"/>
<circle cx="${sx(last.tokens).toFixed(1)}" cy="${sy(last.loss).toFixed(1)}"
r="4" fill="#2ee6a8"/>
<text class="ax" x="${W-PR}" y="${PT+2}" text-anchor="end"
style="fill:#2ee6a8;font-size:12px">loss ${last.loss.toFixed(4)}</text>`;
},
poll(fn, ms){
let stop=false;
const tick = async () => {
if(stop) return;
try { await fn(); } catch(e){ /* keep polling */ }
if(!stop) setTimeout(tick, ms);
};
tick();
return ()=>{ stop=true; };
}
};
window.NX = NX;
/* Charts use pixel-sized viewBoxes, so they must be redrawn when the box
changes — rotating a phone, or opening the keyboard. */
let _rsT;
addEventListener('resize', () => {
clearTimeout(_rsT);
_rsT = setTimeout(() => {
document.querySelectorAll('svg.chart').forEach(el => {
if(el._pts) NX.lossChart(el, el._pts);
});
}, 160);
});