File size: 4,291 Bytes
1fab703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
 * 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;
    },
  };
}