Spaces:
Running
Running
| /** | |
| * 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; | |
| }, | |
| }; | |
| } | |