MiniCPM5-2B-WebGPU-Pi-HTTP / app /src /load-dialog.mjs
Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw History Blame Contribute Delete
3.65 kB
import { playCatSound } from './cat-sound.mjs';
// Native modal dialog provides focus trapping, background inertness and Escape.
export function createLoadDialog(dialog, { onClose }) {
const find = selector => dialog.querySelector(selector);
const confirm = find('#model-confirm'), cancel = find('#model-cancel');
const error = find('#model-error');
const cat = find('.setup-cat img');
const figure = cat.parentElement, tail = find('.setup-cat-tail');
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
let tailReaction;
function stopCatReaction() { tailReaction?.cancel(); tailReaction = undefined; }
figure.onpointerdown = event => event.preventDefault();
figure.onmousedown = event => event.preventDefault();
figure.onclick = () => {
playCatSound();
if (!dialog.open || document.hidden || reducedMotion.matches || !figure.classList.contains('is-ready')) return;
const pose = getComputedStyle(tail).transform;
stopCatReaction();
// Restart the idle rest period so it meets the click animation at neutral.
const idle = tail.getAnimations().find(animation => animation.animationName === 'setup-tail-swish');
if (idle) idle.currentTime = 0;
tailReaction = tail.animate([
{ transform: pose },
{ transform: 'rotate(-7deg)', offset: .3 },
{ transform: 'rotate(4deg)', offset: .6 },
{ transform: 'rotate(-1deg)', offset: .82 },
{ transform: 'rotate(0deg)' },
], { id: 'cat-tail-reaction', duration: 1200, easing: 'ease-in-out' });
tailReaction.onfinish = () => { tailReaction = undefined; };
};
dialog.addEventListener('close', stopCatReaction);
// The HTML preloads the sprite; decode it now, including on a warm cache hit.
// A slow or failed image must never hold up model setup.
void cat.decode().then(() => {
// Both layers reuse the decoded, preloaded sprite; no second asset download.
cat.parentElement.style.setProperty('--cat-sprite', `url("${cat.currentSrc || cat.src}")`);
cat.parentElement.classList.add('is-ready');
}).catch(() => {});
const syncCatMotion = () => {
dialog.style.setProperty('--cat-play-state', document.hidden ? 'paused' : 'running');
if (document.hidden || reducedMotion.matches) stopCatReaction();
};
document.addEventListener('visibilitychange', syncCatMotion);
reducedMotion.addEventListener('change', syncCatMotion);
syncCatMotion();
let resolveRequest;
function finish(send) {
if (!resolveRequest) return;
stopCatReaction();
const resolve = resolveRequest; resolveRequest = undefined;
dialog.close(); onClose(); resolve(send);
}
cancel.onclick = () => finish(false);
dialog.addEventListener('cancel', event => { event.preventDefault(); finish(false); });
confirm.onclick = () => finish(true);
return {
request(text, { errorMessage } = {}) {
if (resolveRequest) throw Error('Finish the current model setup before sending another message.');
confirm.textContent = errorMessage ? 'Try again' : 'Load model & send';
find('#model-title').textContent = errorMessage ? 'Load model' : 'Download model weights';
find('#model-preview').textContent = text;
error.textContent = errorMessage ?? ''; error.hidden = !errorMessage;
const storageLink = document.querySelector('#open-tab');
find('#model-open-tab').hidden = !errorMessage || storageLink.hidden;
find('#model-open-tab').href = storageLink.href;
const request = new Promise(resolve => { resolveRequest = resolve; });
dialog.showModal(); find('.setup-body').scrollTop = 0; confirm.focus();
return request;
},
};
}