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.