File size: 3,650 Bytes
39371ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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;
    },
  };
}