1const STYLE_ID = "pixlane-model-status-overlay-style"; 2const WARMUP_MAX_VISIBLE_MS = 9000; 3const REVEAL_DELAY_MS = 160; 4const MIN_VISIBLE_MS = 420; 5const CLOSE_HOLD_MS = 260; 6const CLOSE_FADE_MS = 220; 7const PROGRESS_LABEL_INTERVAL_MS = 180; 8const OPTIMIZATION_MESSAGE_INTERVAL_MS = 3200; 9const OPTIMIZATION_MESSAGE_FADE_MS = 190; 10 11const OPTIMIZATION_MESSAGES = [ 12 { 13 key: "tool.modelOptimizationAcceleration", 14 fallback: "Checking available browser acceleration", 15 }, 16 { 17 key: "tool.modelOptimizationPerformance", 18 fallback: "Optimizing performance for this device", 19 }, 20 { 21 key: "tool.modelOptimizationPreparing", 22 fallback: "Preparing the AI model for processing", 23 }, 24]; 25 26const PHASES = [ 27 { 28 prefix: "downloading-", 29 stage: 0, 30 detailKey: "tool.modelStageDownloadingDetail", 31 detailFallback: "Fetching the secure local files needed for on-device processing.", 32 }, 33 { 34 prefix: "decrypting-", 35 stage: 1, 36 detailKey: "tool.modelStageDecryptingDetail", 37 detailFallback: "Preparing protected local assets for this device.", 38 }, 39 { 40 prefix: "loading-", 41 stage: 1, 42 detailKey: "tool.modelStageLoadingDetail", 43 detailFallback: "Preparing the local runtime for this device.", 44 }, 45 { 46 prefix: "initializing-", 47 stage: 2, 48 detailKey: "tool.modelStageInitializingDetail", 49 detailFallback: "Optimizing the first execution path for this device.", 50 }, 51 { 52 prefix: "running-", 53 stage: 2, 54 detailKey: "tool.modelStageRunningDetail", 55 detailFallback: "Finishing the local operation and preparing the preview.", 56 }, 57]; 58 59const FALLBACK_PHASE = { 60 stage: 1, 61 detailKey: "tool.modelStageOptimizingDetail", 62 detailFallback: "Preparing the local runtime before processing begins on this device.", 63}; 64 65function clamp(value, min, max) { 66 return Math.min(max, Math.max(min, value)); 67} 68 69function resolvePhase(message) { 70 const normalized = String(message || "").trim().toLowerCase(); 71 if (!normalized) return FALLBACK_PHASE; 72 return PHASES.find((phase) => normalized.startsWith(phase.prefix)) || FALLBACK_PHASE; 73} 74 75function normalizeStatusPayload(nextValue) { 76 const payload = 77 nextValue && typeof nextValue === "object" && !Array.isArray(nextValue) 78 ? nextValue 79 : { message: nextValue }; 80 const currentFrame = Math.max(0, Number(payload.currentFrame) || 0); 81 const totalFrames = Math.max(0, Number(payload.totalFrames) || 0); 82 const hasExplicitProgress = 83 (payload.progressKind === "measured" || payload.progressMeasured === true) && 84 payload.progress !== null && 85 payload.progress !== undefined && 86 payload.progress !== ""; 87 const explicitProgress = hasExplicitProgress ? Number(payload.progress) : Number.NaN; 88 const frameProgress = 89 currentFrame > 0 && totalFrames > 0 90 ? (Math.min(currentFrame, totalFrames) / totalFrames) * 100 91 : null; 92 93 return { 94 message: String(payload.message || "").trim(), 95 progress: 96 frameProgress != null 97 ? clamp(frameProgress, 0, 100) 98 : Number.isFinite(explicitProgress) 99 ? clamp(explicitProgress, 0, 100) 100 : null, 101 detail: typeof payload.detail === "string" ? payload.detail.trim() : "", 102 phase: typeof payload.phase === "string" ? payload.phase.trim() : "", 103 currentFrame, 104 totalFrames, 105 }; 106} 107 108function ensureStyles() { 109 if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return; 110 const style = document.createElement("style"); 111 style.id = STYLE_ID; 112 style.textContent = ` 113.model-status{position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:24px;color:#eef6ff;opacity:1;transition:opacity ${CLOSE_FADE_MS}ms ease} 114.model-status[hidden]{display:none!important} 115.model-status.model-status--closing{opacity:0;pointer-events:none} 116.model-status__scrim{position:absolute;inset:0;background:radial-gradient(circle at 50% 40%,rgba(19,56,91,.2),transparent 45%),rgba(2,5,10,.68);backdrop-filter:blur(14px) saturate(.82);-webkit-backdrop-filter:blur(14px) saturate(.82)} 117.model-status__card{position:relative;z-index:1;isolation:isolate;display:grid;grid-template-columns:116px minmax(0,1fr);align-items:center;gap:26px;width:min(570px,calc(100vw - 48px));min-height:238px;padding:28px;border:1px solid rgba(152,206,255,.15);border-radius:28px;background:linear-gradient(145deg,rgba(17,28,43,.97),rgba(7,13,23,.98) 68%);box-shadow:0 34px 110px rgba(0,0,0,.54),0 0 0 1px rgba(255,255,255,.025) inset,0 1px rgba(255,255,255,.08) inset;overflow:hidden;transform:translateY(0) scale(1);animation:model-status-card-in .34s cubic-bezier(.2,.8,.2,1) both} 118.model-status__card::before{content:"";position:absolute;z-index:-2;inset:-45% 42% 18% -18%;border-radius:50%;background:radial-gradient(circle,rgba(66,174,255,.24),rgba(24,112,190,.09) 38%,transparent 70%);
118filter:blur(12px);pointer-events:none} 119.model-status__card::after{content:"";position:absolute;z-index:-1;inset:0;background:linear-gradient(105deg,transparent 0 44%,rgba(255,255,255,.035) 49%,transparent 54%);transform:translateX(-100%);animation:model-status-sheen 3.8s ease-in-out infinite;pointer-events:none} 120.model-status__visual{position:relative;width:108px;height:108px;display:grid;place-items:center;border-radius:32px;background:radial-gradient(circle at 50% 42%,rgba(74,185,255,.16),rgba(4,13,24,.2) 58%,rgba(4,10,18,.55));border:1px solid rgba(123,202,255,.13);box-shadow:0 18px 46px rgba(0,0,0,.28),inset 0 1px rgba(255,255,255,.055)} 121.model-status__orbit{position:absolute;border:1px solid rgba(116,199,255,.19);border-radius:50%;animation:model-status-orbit 5.8s linear infinite} 122.model-status__orbit::before{content:"";position:absolute;top:-3px;left:50%;width:6px;height:6px;border-radius:50%;background:#72cbff;box-shadow:0 0 13px rgba(93,194,255,.95)} 123.model-status__orbit--outer{inset:14px} 124.model-status__orbit--inner{inset:27px;border-style:dashed;opacity:.78;animation-direction:reverse;animation-duration:4.4s} 125.model-status__core{position:relative;display:grid;place-items:center;width:38px;height:38px;border-radius:13px;color:#dff4ff;background:linear-gradient(145deg,rgba(80,188,255,.3),rgba(28,104,180,.17));border:1px solid rgba(130,210,255,.29);box-shadow:0 0 28px rgba(51,164,245,.23),inset 0 1px rgba(255,255,255,.12)} 126.model-status__core svg{width:20px;height:20px} 127.model-status__core-pulse{position:absolute;inset:-1px;border-radius:14px;border:1px solid rgba(111,203,255,.3);animation:model-status-core-pulse 2s ease-out infinite} 128.model-status__body{display:grid;gap:14px;min-width:0} 129.model-status__meta{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:28px} 130.model-status__phase{display:inline-flex;align-items:center;gap:8px;min-width:0;font-family:var(--font-mono,ui-monospace,monospace);font-size:.68rem;font-weight:700;line-height:1.2;letter-spacing:.09em;text-transform:uppercase;color:#a9dbfa} 131.model-status__phase::before{content:"";width:7px;height:7px;flex:0 0 auto;border-radius:50%;background:#5fd19d;box-shadow:0 0 12px rgba(95,209,157,.58);animation:model-status-dot-breathe 1.7s ease-in-out infinite} 132.model-status__progress-value{font-family:var(--font-mono,ui-monospace,monospace);font-size:.76rem;font-weight:750;letter-spacing:.04em;color:#dff4ff;font-variant-numeric:tabular-nums} 133.model-status__progress-value[hidden]{display:none!important} 134.model-status__copy{display:grid;gap:7px;min-width:0} 135.model-status__headline{font-size:1.2rem;font-weight:720;line-height:1.25;letter-spacing:-.025em;color:#fff} 136.model-status__copy small{display:block;max-width:47ch;font-size:.84rem;line-height:1.55;color:rgba(225,237,248,.69)} 137.model-status__optimization{display:flex;align-items:center;gap:8px;min-height:20px;color:rgba(191,224,247,.64);font-family:var(--font-mono,ui-monospace,monospace);font-size:.68rem;line-height:1.3;letter-spacing:.015em} 138.model-status__optimization-icon{position:relative;width:17px;height:17px;display:grid;place-items:center;flex:0 0 auto;border-radius:6px;color:#8ad5ff;background:rgba(77,179,244,.09);border:1px solid rgba(113,201,255,.13)} 139.model-status__optimization-icon svg{width:10px;height:10px} 140.model-status__optimization-text{opacity:1;transform:translateY(0);transition:opacity ${OPTIMIZATION_MESSAGE_FADE_MS}ms ease,transform ${OPTIMIZATION_MESSAGE_FADE_MS}ms ease} 141.model-status__optimization-text.is-changing{opacity:0;transform:translateY(3px)} 142.model-status__meter{position:relative;height:6px;border-radius:999px;background:rgba(187,220,246,.095);box-shadow:inset 0 1px 2px rgba(0,0,0,.28);overflow:hidden} 143.model-status__meter-fill{position:absolute;inset:0 auto 0 0;width:0;border-radius:inherit;background:linear-gradient(90deg,#238fe4,#5ec7ff 68%,#a3e5ff);box-shadow:0 0 18px rgba(66,180,255,.34);transition:width .12s linear} 144.model-status__meter-runner{position:absolute;inset:0 auto 0 0;width:34%;border-radius:inherit;background:linear-gradient(90deg,transparent,rgba(65,177,250,.32),#78d2ff,rgba(65,177,250,.32),transparent);filter:drop-shadow(0 0 8px rgba(85,194,255,.4));animation:model-status-runner 1.65s cubic-bezier(.45,0,.55,1) infinite} 145.model-status__card--determinate .model-status__meter-runner{display:none} 146.model-status__timeline{display:grid;grid-template-columns:repeat(3,1fr);gap:7px;align-items:center} 147.model-status__step{height:2px;border-radius:999px;background:rgba(205,229,248,.1);transition:background-color .25s ease,box-shadow .25s ease} 148.model-status__step--done{background:rgba(85,190,255,.34)} 149.model-status__step--active{background:#5fc8ff;box-shadow:0 0 12px rgba(83,191,255,.5)} 150.model-status__footer{display:flex;align-items:center;justify-content:space-between;gap:14px;min-height:34px;padding-top:1px} 151.model-status__privacy{display:inline-flex;align-items:center;gap:7px;min-width:0;color:rgba(209,227,241,.52);font-size:.72rem;line-height:1.25} 152.model-status__privacy svg{width:14px;
152height:14px;flex:0 0 auto;color:#78c9f6} 153.model-status__actions{display:flex;align-items:center;justify-content:flex-end} 154.model-status__actions[hidden]{display:none!important} 155.model-status__action{min-height:42px;padding:0 15px;border-radius:11px;border:1px solid rgba(255,255,255,.13);background:rgba(255,255,255,.055);color:#e8f2fb;font-size:.78rem;font-weight:650;letter-spacing:-.005em;transition:background-color .16s ease,border-color .16s ease,opacity .16s ease} 156.model-status__action:hover:not(:disabled){background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.22)} 157.model-status__action:disabled{opacity:.52;cursor:not-allowed} 158.model-status__card--complete .model-status__core{color:#dffff0;background:linear-gradient(145deg,rgba(73,196,137,.3),rgba(28,125,82,.17));border-color:rgba(105,224,166,.32);box-shadow:0 0 28px rgba(63,207,139,.2)} 159.model-status__card--complete .model-status__core-pulse{border-color:rgba(95,209,157,.32)} 160.model-status__card--complete .model-status__phase::before{animation:none;background:#7be3ad} 161@keyframes model-status-card-in{from{opacity:0;transform:translateY(12px) scale(.985)}to{opacity:1;transform:translateY(0) scale(1)}} 162@keyframes model-status-sheen{0%,56%{transform:translateX(-115%)}82%,100%{transform:translateX(125%)}} 163@keyframes model-status-orbit{to{transform:rotate(360deg)}} 164@keyframes model-status-core-pulse{0%{opacity:.72;transform:scale(.9)}70%,100%{opacity:0;transform:scale(1.42)}} 165@keyframes model-status-dot-breathe{0%,100%{opacity:.55;transform:scale(.84)}50%{opacity:1;transform:scale(1)}} 166@keyframes model-status-runner{0%{transform:translateX(-115%)}55%{transform:translateX(185%)}100%{transform:translateX(310%)}} 167@media(max-width:640px){.model-status{padding:16px}.model-status__card{grid-template-columns:1fr;justify-items:center;width:min(360px,calc(100vw - 32px));min-height:0;gap:20px;padding:24px 20px 21px;text-align:center}.model-status__visual{width:96px;height:96px;border-radius:28px}.model-status__body{width:100%}.model-status__meta,.model-status__footer{justify-content:center;flex-wrap:wrap}.model-status__copy{justify-items:center}.model-status__optimization{justify-content:center}.model-status__privacy{justify-content:center}.model-status__actions{width:100%;justify-content:center}.model-status__action{width:100%}} 168@media(prefers-reduced-motion:reduce){.model-status,.model-status__card,.model-status__meter-fill,.model-status__step,.model-status__optimization-text{transition:none}.model-status__card,.model-status__card::after,.model-status__orbit,.model-status__core-pulse,.model-status__phase::before,.model-status__meter-runner{animation:none}.model-status__meter-runner{left:33%;opacity:.72}} 169`; 170 document.head.appendChild(style); 171} 172 173export function createModelStatusOverlay({ 174 container, 175 translate, 176 defaultHeadline = "Optimizing for your device...", 177}) { 178 const noop = { handle() {}, setAction() {}, destroy() {} }; 179 if (!container || typeof window === "undefined" || typeof document === "undefined") { 180 return noop; 181 } 182 183 ensureStyles(); 184 185 let refs = null; 186 let current = 0; 187 let target = 0; 188 let determinate = false; 189 let stageIndex = 0; 190 let lastFrame = 0; 191 let lastProgressLabelAt = 0; 192 let lastProgressLabel = -1; 193 let rafId = 0; 194 let revealTimer = 0; 195 let closeTimer = 0; 196 let teardownTimer = 0; 197 let warmupTimer = 0; 198 let optimizationTimer = 0; 199 let optimizationSwapTimer = 0; 200 let optimizationIndex = 0; 201 let visibleStartedAt = 0; 202 let message = ""; 203 let statusDetail = ""; 204 let statusPhase = ""; 205 let actionLabel = ""; 206 let actionDisabled = false; 207 let actionHandler = null; 208 let active = false; 209 210 const t = (key, fallback) => 211 typeof translate === "function" ? translate(key, fallback) : fallback; 212 213 const reducedMotion = () => 214 typeof window.matchMedia === "function" && 215 window.matchMedia("(prefers-reduced-motion: reduce)").matches; 216 217 function ensureDom() { 218 if (refs) return refs; 219 container.innerHTML = ` 220 <div class="model-status__scrim"></div> 221 <div class="model-status__card" role="status" aria-live="polite" aria-busy="true" data-stage="1"> 222 <div class="model-status__visual" aria-hidden="true">
223 <span class="model-status__orbit model-status__orbit--outer"></span> 224 <span class="model-status__orbit model-status__orbit--inner"></span> 225 <span class="model-status__core"> 226 <span class="model-status__core-pulse"></span> 227 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"> 228 <path d="M12 3v4M12 17v4M3 12h4M17 12h4"/> 229 <path d="m5.6 5.6 2.8 2.8m7.2 7.2 2.8 2.8m0-12.8-2.8 2.8m-7.2 7.2-2.8 2.8" opacity=".7"/> 230 <circle cx="12" cy="12" r="3.2"/> 231 </svg> 232 </span> 233 </div> 234 <div class="model-status__body"> 235 <div class="model-status__meta"> 236 <span class="model-status__phase"></span> 237 <span class="model-status__progress-value" aria-hidden="true" hidden></span> 238 </div> 239 <div class="model-status__copy"> 240 <div class="model-status__headline"></div> 241 <small></small> 242 </div> 243 <div class="model-status__optimization" aria-hidden="true"> 244 <span class="model-status__optimization-icon"> 245 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> 246 <path d="m13 2-8 12h7l-1 8 8-12h-7l1-8Z"/> 247 </svg> 248 </span> 249 <span class="model-status__optimization-text"></span> 250 </div> 251 <div class="model-status__meter" role="progressbar" aria-valuemin="0" aria-valuemax="100"> 252 <span class="model-status__meter-fill"></span> 253 <span class="model-status__meter-runner"></span> 254 </div> 255 <div class="model-status__timeline" aria-hidden="true"> 256 <span class="model-status__step"></span> 257 <span class="model-status__step"></span> 258 <span class="model-status__step"></span> 259 </div> 260 <div class="model-status__footer"> 261 <span class="model-status__privacy"> 262 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 263 <path d="M12 3 5 6v5c0 4.5 2.8 8.1 7 10 4.2-1.9 7-5.5 7-10V6l-7-3Z"/> 264 <path d="m9.4 12 1.7 1.7 3.7-4"/> 265 </svg> 266 <span></span> 267 </span> 268 <div class="model-status__actions" hidden></div> 269 </div> 270 </div> 271 </div> 272 `; 273 refs = { 274 card: container.querySelector(".model-status__card"), 275 headline: container.querySelector(".model-status__headline"), 276 detail: container.querySelector(".model-status__copy small"), 277 optimizationText: container.querySelector(".model-status__optimization-text"), 278 phase: container.querySelector(".model-status__phase"), 279 progressValue: container.querySelector(".model-status__progress-value"), 280 meter: container.querySelector(".model-status__meter"), 281 meterFill: container.querySelector(".model-status__meter-fill"), 282 steps: Array.from(container.querySelectorAll(".model-status__step")), 283 privacyText: container.querySelector(".model-status__privacy span"), 284 actions: container.querySelector(".model-status__actions"), 285 actionButton: null, 286 }; 287 return refs; 288 } 289 290 function setVisible(nextVisible) { 291 if (nextVisible) { 292 container.hidden = false; 293 container.classList.remove("model-status--closing"); 294 return; 295 } 296 container.hidden = true; 297 container.classList.remove("model-status--closing"); 298 } 299 300 function scheduleReveal() { 301 if (!active || !container.hidden || revealTimer) return; 302 revealTimer = window.setTimeout(() => { 303 revealTimer = 0; 304 if (!active) return; 305 visibleStartedAt = performance.now(); 306 setVisible(true); 307 startOptimizationRotation(true); 308 }, REVEAL_DELAY_MS); 309 } 310 311 function stopOptimizationRotation() { 312 if (optimizationTimer) { 313 window.clearInterval(optimizationTimer); 314 optimizationTimer = 0; 315 } 316 if (optimizationSwapTimer) { 317 window.clearTimeout(optimizationSwapTimer); 318 optimizationSwapTimer = 0; 319 } 320 refs?.optimizationText?.classList.remove("is-changing"); 321 } 322 323 function paintOptimizationMessage() { 324 const ui = ensureDom(); 325 const message = OPTIMIZATION_MESSAGES[optimizationIndex % OPTIMIZATION_MESSAGES.length]; 326 ui.optimizationText.textContent = t(message.key, message.fallback); 327 } 328 329 function startOptimizationRotation(reset = false) { 330 if (reset) { 331 stopOptimizationRotation(); 332 optimizationIndex = 0; 333 } 334 paintOptimizationMessage(); 335 if (reducedMotion() || optimizationTimer || OPTIMIZATION_MESSAGES.length < 2) return; 336 337 optimizationTimer = window.setInterval(() => { 338 if (!active || container.hidden || document.hidden || optimizationSwapTimer) return; 339 const ui = ensureDom(); 340 ui.optimizationText.classList.add("is-changing"); 341 optimizationSwapTimer = window.setTimeout(() => { 342 optimizationSwapTimer = 0; 343 if (!active || !refs) return; 344 optimizationIndex = (optimizationIndex + 1) % OPTIMIZATION_MESSAGES.length; 345 paintOptimizationMessage(); 346 ui.optimizationText.classList.remove("is-changing"); 347 }, OPTIMIZATION_MESSAGE_FADE_MS); 348 }, OPTIMIZATION_MESSAGE_INTERVAL_MS); 349 } 350 351 function paintAction() { 352 const ui = ensureDom(); 353 ui.actions.replaceChildren(); 354 ui.actionButton = null; 355 if (!actionLabel) { 356 ui.actions.hidden = true; 357 return; 358 } 359 360 const button = document.createElement("button"); 361 button.type = "button"; 362 button.className = "model-status__action"; 363 button.textContent = actionLabel; 364 button.disabled = !!actionDisabled; 365 button.addEventListener("click", () => { 366 if (actionDisabled || typeof actionHandler !== "function") return; 367 actionHandler(); 368 }); 369 ui.actions.appendChild(button); 370 ui.actions.hidden = false;
371 ui.actionButton = button; 372 } 373 374 function paintStage(stageIndex) { 375 const ui = ensureDom(); 376 const stage = clamp(Number(stageIndex) || 0, 0, ui.steps.length - 1); 377 ui.card.dataset.stage = String(stage); 378 ui.steps.forEach((step, index) => { 379 step.classList.toggle("model-status__step--done", index < stage); 380 step.classList.toggle("model-status__step--active", index === stage); 381 }); 382 } 383 384 function paintStatic() { 385 const phase = resolvePhase(message); 386 const ui = ensureDom(); 387 const headlineFallback = 388 !defaultHeadline || defaultHeadline === "Optimizing for your device..." 389 ? "Optimizing inference for your device" 390 : defaultHeadline; 391 ui.headline.textContent = t("tool.modelInferenceOptimizing", headlineFallback); 392 ui.detail.textContent = statusDetail || t(phase.detailKey, phase.detailFallback); 393 ui.phase.textContent = 394 statusPhase || t("tool.modelStagePhaseFallback", "On-device processing"); 395 ui.privacyText.textContent = t( 396 "tool.modelStatusPrivacy", 397 "Your file stays on this device" 398 ); 399 stageIndex = Math.max(stageIndex, phase.stage); 400 paintStage(stageIndex); 401 if (!ui.optimizationText.textContent) paintOptimizationMessage(); 402 paintAction(); 403 } 404 405 function paintProgress(now = performance.now(), forceLabel = false) { 406 const ui = ensureDom(); 407 ui.card.classList.toggle("model-status__card--determinate", determinate); 408 409 if (!determinate) { 410 ui.progressValue.hidden = true; 411 ui.progressValue.textContent = ""; 412 ui.meterFill.style.width = "0%"; 413 ui.meter.removeAttribute("aria-valuenow"); 414 ui.meter.setAttribute( 415 "aria-valuetext", 416 t("tool.modelStatusWorking", "Working locally") 417 ); 418 return; 419 } 420 421 const exact = clamp(current, 0, 100); 422 const rounded = Math.round(exact); 423 ui.progressValue.hidden = false; 424 ui.meterFill.style.width = `${exact.toFixed(2)}%`; 425 ui.meter.setAttribute("aria-valuenow", String(rounded)); 426 ui.meter.setAttribute("aria-valuetext", `${rounded}%`); 427 428 if ( 429 forceLabel || 430 rounded === 100 || 431 (rounded !== lastProgressLabel && now - lastProgressLabelAt >= PROGRESS_LABEL_INTERVAL_MS) 432 ) { 433 ui.progressValue.textContent = `${rounded}%`; 434 lastProgressLabel = rounded; 435 lastProgressLabelAt = now; 436 } 437 } 438 439 function stopAnimation() { 440 if (rafId) { 441 window.cancelAnimationFrame(rafId); 442 rafId = 0; 443 } 444 lastFrame = 0; 445 } 446 447 function tick(now) { 448 const dt = lastFrame ? Math.min(64, now - lastFrame) : 16; 449 lastFrame = now; 450 451 if (determinate && current < target) { 452 const distance = target - current; 453 const easing = target >= 100 ? 0.16 : 0.095; 454 const minimumStep = target >= 100 ? 0.72 : 0.16; 455 current = Math.min( 456 target, 457 current + Math.max(minimumStep, distance * easing) * (dt / 16) 458 ); 459 paintProgress(now); 460 } 461 462 if (determinate && target - current > 0.08) { 463 rafId = window.requestAnimationFrame(tick); 464 return; 465 } 466 if (determinate) { 467 current = target; 468 paintProgress(now, true); 469 } 470 rafId = 0; 471 lastFrame = 0; 472 } 473 474 function startAnimation() { 475 if (!determinate || rafId) return; 476 if (reducedMotion()) { 477 current = target; 478 paintProgress(performance.now(), true); 479 return; 480 } 481 rafId = window.requestAnimationFrame(tick); 482 } 483 484 function clearCompletionTimers() { 485 if (closeTimer) { 486 window.clearTimeout(closeTimer); 487 closeTimer = 0; 488 } 489 if (teardownTimer) { 490 window.clearTimeout(teardownTimer); 491 teardownTimer = 0; 492 } 493 if (warmupTimer) { 494 window.clearTimeout(warmupTimer); 495 warmupTimer = 0; 496 } 497 } 498 function clearTimers() { 499 clearCompletionTimers(); 500 if (revealTimer) { 501 window.clearTimeout(revealTimer); 502 revealTimer = 0; 503 } 504 } 505 506 function teardown() { 507 clearTimers(); 508 stopOptimizationRotation(); 509 stopAnimation(); 510 refs = null; 511 active = false; 512 determinate = false; 513 stageIndex = 0; 514 current = 0; 515 target = 0; 516 lastProgressLabelAt = 0; 517 lastProgressLabel = -1; 518 optimizationIndex = 0; 519 visibleStartedAt = 0; 520 message = ""; 521 statusDetail = ""; 522 statusPhase = ""; 523 actionLabel = ""; 524 actionDisabled = false;
525 actionHandler = null; 526 container.innerHTML = ""; 527 setVisible(false); 528 } 529 530 function beginClose() { 531 if (!active) { 532 teardown(); 533 return; 534 } 535 536 if (revealTimer) { 537 window.clearTimeout(revealTimer); 538 revealTimer = 0; 539 teardown(); 540 return; 541 } 542 if (container.hidden) { 543 teardown(); 544 return; 545 } 546 if (warmupTimer) { 547 window.clearTimeout(warmupTimer); 548 warmupTimer = 0; 549 } 550 stopOptimizationRotation(); 551 if (closeTimer || teardownTimer) return; 552 553 const elapsed = performance.now() - visibleStartedAt; 554 const waitForMinimum = Math.max(0, MIN_VISIBLE_MS - elapsed); 555 closeTimer = window.setTimeout(() => { 556 closeTimer = 0; 557 const ui = ensureDom(); 558 ui.card.classList.add("model-status__card--complete"); 559 ui.card.setAttribute("aria-busy", "false"); 560 if (determinate) { 561 target = 100; 562 startAnimation(); 563 } 564 565 closeTimer = window.setTimeout(() => { 566 closeTimer = 0; 567 if (determinate) { 568 current = 100; 569 target = 100; 570 paintProgress(performance.now(), true); 571 } 572 container.classList.add("model-status--closing"); 573 teardownTimer = window.setTimeout(teardown, CLOSE_FADE_MS); 574 }, CLOSE_HOLD_MS); 575 }, waitForMinimum); 576 } 577 578 return { 579 handle(nextStatus) { 580 const payload = normalizeStatusPayload(nextStatus); 581 if (!payload.message) { 582 beginClose(); 583 return; 584 } 585 586 clearCompletionTimers(); 587 if (!active) { 588 active = true; 589 visibleStartedAt = 0; 590 current = 0; 591 target = 0; 592 determinate = false; 593 stageIndex = 0; 594 lastProgressLabelAt = 0; 595 lastProgressLabel = -1; 596 } 597 message = payload.message; 598 statusDetail = payload.detail; 599 statusPhase = payload.phase; 600 601 if (payload.progress != null) { 602 determinate = true; 603 target = Math.max(current, target, payload.progress); 604 } 605 606 const ui = ensureDom(); 607 ui.card.classList.remove("model-status__card--complete"); 608 ui.card.setAttribute("aria-busy", "true"); 609 paintStatic(); 610 paintProgress(performance.now(), true); 611 startAnimation(); 612 scheduleReveal(); 613 if (!container.hidden && !optimizationTimer) startOptimizationRotation(); 614 615 if (payload.message.toLowerCase().includes("warmup")) { 616 warmupTimer = window.setTimeout(() => { 617 warmupTimer = 0; 618 beginClose(); 619 }, WARMUP_MAX_VISIBLE_MS); 620 } 621 }, 622 setAction(nextAction) { 623 if ( 624 nextAction && 625 typeof nextAction === "object" && 626 typeof nextAction.label === "string" && 627 nextAction.label.trim() 628 ) { 629 actionLabel = nextAction.label.trim(); 630 actionDisabled = !!nextAction.disabled; 631 actionHandler = 632 typeof nextAction.onClick === "function" ? nextAction.onClick : null; 633 } else { 634 actionLabel = ""; 635 actionDisabled = false; 636 actionHandler = null; 637 } 638 if (refs) paintAction(); 639 }, 640 destroy() { 641 teardown(); 642 }, 643 }; 644}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.