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; }, }; }