PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/tourAutoplay.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 16:59:07 UTC 4,699 bytes, 133 lines download raw bytes

1// tourAutoplay.js — auto-fires the matching `/tour-<x>` slash command the
2// first time the user opens a tool modal. One-shot per modal: dismissed or
3// not, the marker is set so reopens never auto-trigger again.
4//
5// Pairs with the existing tourHints.js (which shows a single global "drag
6// title bar to snap" hint). Tours are richer per-feature walkthroughs.
7//
8// Mobile is excluded — tours position halos by rect math that doesn't fit
9// the bottom-sheet layout cleanly.
10
11import { handleSlashCommand } from './slashCommands.js';
12
13// Modal id → slash command to fire (without the leading "/"). Add to this
14// map when a new feature picks up a `tour-*` command.
15const TOUR_FOR_MODAL = {
16  'doclib-modal':           'tour-library',
17  'cookbook-modal':         'tour-cookbook',
18  'research-overlay':       'tour-research',
19  'compare-model-overlay':  'tour-compare',
20  'theme-modal':            'tour-theme',
21  'settings-modal':         'tour-settings',
22  'gallery-modal':          'tour-gallery',
23};
24
25const SEEN_KEY = (tour) => `odysseus-tour-autoplay-seen-${tour}`;
26
27let _initialized = false;
28// Suppress re-fire if a tour is already active or another modal opens while
29// we're mid-tour. The slash command itself adds `body.tour-active` for the
30// duration of its halos.
31function _tourActive() {
32  return document.body.classList.contains('tour-active');
33}
34
35function _isVisible(el) {
36  if (!el || el.classList.contains('hidden')) return false;
37  if (el.style.display === 'none') return false;
38  const r = el.getBoundingClientRect();
39  return r.width > 0 && r.height > 0;
40}
41
42async function _maybeFire(modal) {
43  const id = modal.id;
44  const tour = TOUR_FOR_MODAL[id];
45  if (!tour) return;
46  if (_tourActive()) {
47    try { window.cancelActiveTour?.('modal-opened'); } catch (_) {}
48    return;
49  }
50  let seen = false;
51  try { seen = localStorage.getItem(SEEN_KEY(tour)) === '1'; } catch (_) {}
52  if (seen) return;
53  // Mark immediately so a quick double-trigger (e.g. modal-class observer
54  // fires twice during animation) can't queue two tours.
55  try { localStorage.setItem(SEEN_KEY(tour), '1'); } catch (_) {}
56  // Let the modal's own enter-animation settle before halos try to position
57  // off the title bar / first card / etc. ~400ms matches tourHints.
58  setTimeout(() => {
59    if (_tourActive()) return;
60    try {
61      handleSlashCommand('/' + tour);
62    } catch (e) {
63      // If firing fails we don't unmark — re-attempting on every modal open
64      // would be more annoying than a missed tour. User can run `/tour-x`
65      // manually from the chat input.
66      // eslint-disable-next-line no-console
67      console.warn(`Tour autoplay failed for ${id}:`, e);
68    }
69  }, 400);
70}
71
72function _watchModals() {
73  if (typeof MutationObserver === 'undefined') return;
74  const observer = new MutationObserver((muts) => {
75    for (const m of muts) {
76      if (m.attributeName !== 'class' && m.attributeName !== 'style') continue;
77      const el = m.target;
78      if (!(el instanceof HTMLElement)) continue;
79      if (!(el.id in TOUR_FOR_MODAL)) continue;
80      const wasHidden = !m.oldValue
81        || /\bhidden\b/.test(m.oldValue)
82        || /display:\s*none/.test(m.oldValue);
83      if (wasHidden && _isVisible(el)) _maybeFire(el);
84    }
85  });
86  // Observe each known target if it exists at boot…
87  Object.keys(TOUR_FOR_MODAL).forEach(id => {
88    const el = document.getElementById(id);
89    if (el) {
90      observer.observe(el, {
91        attributes: true,
92        attributeOldValue: true,
93        attributeFilter: ['class', 'style'],
94      });
95    }
96  });
97  // …and also for any matching modal added later (research overlay is
98  // appended on demand, for example).
99  const docObserver = new MutationObserver((muts) => {
100    for (const m of muts) {
101      m.addedNodes.forEach(node => {
102        if (!(node instanceof HTMLElement)) return;
103        if (node.id in TOUR_FOR_MODAL) {
104          observer.observe(node, {
105            attributes: true,
106            attributeOldValue: true,
107            attributeFilter: ['class', 'style'],
108          });
109          if (_isVisible(node)) _maybeFire(node);
110        }
111      });
112    }
113  });
114  docObserver.observe(document.body, { childList: true, subtree: false });
115}
116
117export function init() {
118  if (_initialized) return;
119  _initialized = true;
120  // Disabled for v1 stability: opening ordinary app windows must never
121  // auto-spawn tour overlays or interfere with close/backdrop behavior.
122  // Manual slash tours still work through slashCommands.js.
123}
124
125if (typeof window !== 'undefined') {
126  if (document.readyState === 'loading') {
127    document.addEventListener('DOMContentLoaded', init);
128  } else {
129    init();
130  }
131}
132
133export default { init };

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.