| 1 | import { callJsonApi } from "/js/api.js"; |
| 2 | import { isModalOpen } from "/js/modals.js"; |
| 3 | |
| 4 | const DEFAULT_SURFACE = "welcome"; |
| 5 | const STARTUP_DELAY_MS = 650; |
| 6 | const SUPPRESSION_PREFIX = "discovery_auto_modal_closed"; |
| 7 | |
| 8 | let initialized = false; |
| 9 | let lastOpened = null; |
| 10 | let checking = false; |
| 11 | |
| 12 | function cleanPath(path = "") { |
| 13 | return String(path || "").replace(/^\/+/, ""); |
| 14 | } |
| 15 | |
| 16 | function currentContextId(fallback = "") { |
| 17 | try { |
| 18 | return fallback || globalThis.getContext?.() || sessionStorage.getItem("lastSelectedChat") || "welcome"; |
| 19 | } catch { |
| 20 | return fallback || "welcome"; |
| 21 | } |
| 22 | } |
| 23 | |
| 24 | function bannerSupportsSurface(banner, surface) { |
| 25 | const surfaces = banner?.auto_modal_surfaces; |
| 26 | return !Array.isArray(surfaces) || surfaces.length === 0 || surfaces.includes(surface); |
| 27 | } |
| 28 | |
| 29 | function suppressionKey({ surface, ctxid, reason, path }) { |
| 30 | return `${SUPPRESSION_PREFIX}:${surface}:${ctxid || "none"}:${reason || "unknown"}:${cleanPath(path)}`; |
| 31 | } |
| 32 | |
| 33 | function isSuppressed(item) { |
| 34 | try { |
| 35 | return sessionStorage.getItem(suppressionKey(item)) === "1"; |
| 36 | } catch { |
| 37 | return false; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | function suppress(item) { |
| 42 | try { |
| 43 | sessionStorage.setItem(suppressionKey(item), "1"); |
| 44 | } catch { |
| 45 | // no-op |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | function modalAlreadyOpen(path) { |
| 50 | return isModalOpen(path); |
| 51 | } |
| 52 | |
| 53 | async function fetchAutoModalBanner(surface, ctxid) { |
| 54 | const response = await callJsonApi("/banners", { |
| 55 | banners: [], |
| 56 | context: { |
| 57 | is_welcome: surface === "welcome", |
| 58 | is_onboarding: document.body.dataset.mode === "onboarding", |
| 59 | surface, |
| 60 | ctxid, |
| 61 | }, |
| 62 | }); |
| 63 | |
| 64 | const banners = Array.isArray(response?.banners) ? response.banners : []; |
| 65 | return banners |
| 66 | .filter((banner) => banner?.auto_modal_path) |
| 67 | .filter((banner) => bannerSupportsSurface(banner, surface)) |
| 68 | .sort((left, right) => (Number(right.auto_modal_priority || 0) - Number(left.auto_modal_priority || 0)))[0] || null; |
| 69 | } |
| 70 | |
| 71 | async function maybeOpenAutoModal(surface = DEFAULT_SURFACE, detail = {}) { |
| 72 | if (checking) return; |
| 73 | checking = true; |
| 74 | const ctxid = currentContextId(detail.ctxid || ""); |
| 75 | |
| 76 | try { |
| 77 | const banner = await fetchAutoModalBanner(surface, ctxid); |
| 78 | if (!banner?.auto_modal_path) return; |
| 79 | |
| 80 | const item = { |
| 81 | surface, |
| 82 | ctxid, |
| 83 | path: banner.auto_modal_path, |
| 84 | reason: banner.auto_modal_reason || banner.id || "auto-modal", |
| 85 | }; |
| 86 | |
| 87 | if (isSuppressed(item) || modalAlreadyOpen(item.path)) return; |
| 88 | |
| 89 | const opener = globalThis.ensureModalOpen || globalThis.openModal; |
| 90 | if (!opener) return; |
| 91 | |
| 92 | lastOpened = item; |
| 93 | await opener(item.path); |
| 94 | } catch (error) { |
| 95 | console.error("Discovery auto-modal check failed:", error); |
| 96 | } finally { |
| 97 | checking = false; |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | function handleModalClosed(event) { |
| 102 | const closedPath = event?.detail?.modalPath || ""; |
| 103 | if (lastOpened && cleanPath(closedPath) === cleanPath(lastOpened.path)) { |
| 104 | suppress(lastOpened); |
| 105 | const surface = lastOpened.surface; |
| 106 | const ctxid = lastOpened.ctxid; |
| 107 | lastOpened = null; |
| 108 | window.setTimeout(() => { |
| 109 | void maybeOpenAutoModal(surface, { ctxid }); |
| 110 | }, 250); |
| 111 | return; |
| 112 | } |
| 113 | |
| 114 | window.setTimeout(() => { |
| 115 | void maybeOpenAutoModal(DEFAULT_SURFACE); |
| 116 | }, 250); |
| 117 | } |
| 118 | |
| 119 | function handleChatCreated(event) { |
| 120 | const ctxid = event?.detail?.ctxid || ""; |
| 121 | window.setTimeout(() => { |
| 122 | void maybeOpenAutoModal("chat-created", { ctxid }); |
| 123 | }, 300); |
| 124 | } |
| 125 | |
| 126 | export default function initDiscoveryAutoModal() { |
| 127 | if (initialized) return; |
| 128 | initialized = true; |
| 129 | |
| 130 | document.addEventListener("modal-closed", handleModalClosed); |
| 131 | document.addEventListener("chat-created", handleChatCreated); |
| 132 | |
| 133 | window.setTimeout(() => { |
| 134 | void maybeOpenAutoModal(DEFAULT_SURFACE); |
| 135 | }, STARTUP_DELAY_MS); |
| 136 | } |