pack-precision-map / progress.js
aleada's picture
pack precision map
1fab703 verified
Raw
History Blame Contribute Delete
4.29 kB
/**
* The progress bar the three Spaces share.
*
* Every one of them reads safetensors headers over ranged requests — two
* per shard, and a large model has a dozen shards. That is seconds of
* silence behind a line of static text, which reads as a page that does
* not work. The bar exists so waiting looks like waiting.
*
* One file, copied into each Space at deploy time rather than written
* three times: three copies of a progress bar drift into three different
* behaviours, and the one that matters here is subtle — a bar that goes
* backwards, or sits at 100% while work continues, is worse than no bar,
* because it is believed.
*
* Two modes, and which one is honest depends on what is known:
*
* start(label) indeterminate — something is happening, and
* nobody knows how much of it there is yet
* total(n) switches to determinate once the shard count
* is known
* step(label) one unit done
* done() / fail() release it
*/
const BAR_CSS = `
.pg { margin: 1.25rem 0 .5rem; }
.pg-label { font-size: .88rem; color: var(--muted); margin-bottom: .45rem;
display: flex; justify-content: space-between; gap: 1rem; }
.pg-count { font-family: var(--mono); font-size: .82rem; white-space: nowrap; }
.pg-track { height: .4rem; border-radius: .2rem; background: var(--line);
overflow: hidden; position: relative; }
.pg-fill { height: 100%; background: var(--accent); border-radius: .2rem;
width: 0; transition: width .18s ease-out; }
.pg-track.indeterminate .pg-fill {
width: 35%; animation: pg-slide 1.1s ease-in-out infinite;
}
@keyframes pg-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(300%); }
}
@media (prefers-reduced-motion: reduce) {
.pg-track.indeterminate .pg-fill { animation: none; width: 100%; opacity: .45; }
.pg-fill { transition: none; }
}`;
let styled = false;
function ensureStyle() {
if (styled) return;
const el = document.createElement('style');
el.textContent = BAR_CSS;
document.head.appendChild(el);
styled = true;
}
/**
* A progress bar mounted in `host`.
*
* It replaces the host's contents, so the caller renders the result into
* the same element when it finishes and the bar disappears with it.
*/
export function progress(host) {
ensureStyle();
let done = 0;
let total = 0;
host.innerHTML = `<div class="pg">
<div class="pg-label"><span class="pg-text"></span><span class="pg-count"></span></div>
<div class="pg-track indeterminate"><div class="pg-fill"></div></div>
</div>`;
const text = host.querySelector('.pg-text');
const count = host.querySelector('.pg-count');
const track = host.querySelector('.pg-track');
const fill = host.querySelector('.pg-fill');
const paint = () => {
if (!total) return;
// Never let the bar reach the end before the work does: a full bar
// with the page still frozen is exactly the impression this is here
// to remove. It closes only in done().
const pctDone = Math.min(99, Math.round((done / total) * 100));
fill.style.width = `${pctDone}%`;
count.textContent = `${done} / ${total}`;
};
return {
/** Say what is happening now, without claiming to know how long. */
start(label) {
text.textContent = label;
return this;
},
/**
* Begin a determinate phase of `n` units.
*
* Resets the count, because a second call means a second phase — the
* integrity checker reads the pack and then the source model, and
* without the reset the bar would show "12 / 5" and run past its own
* end.
*/
total(n) {
total = n;
done = 0;
track.classList.remove('indeterminate');
paint();
return this;
},
/** One unit finished. Safe to call before total() — it just counts. */
step(label) {
done += 1;
if (label) text.textContent = label;
paint();
return this;
},
/** Fill it before the caller replaces the host with the result. */
done() {
track.classList.remove('indeterminate');
fill.style.width = '100%';
if (total) count.textContent = `${total} / ${total}`;
return this;
},
};
}