/** * 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 = `
`; 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; }, }; }