main
js 136 lines 3.58 KB
Raw
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 }