PageSourceSearch

https://work.flyos.ai/static/apps/js/global.js?org_id=8be4f724-2f2d-4762-91d1-f148ae95a39a

js flyos.ai collected 2026-09-28 06:03:12 UTC 39,347 bytes, 845 lines download raw bytes

1(function () {
2    'use strict';
3
4    var orgId;
5    window.SaaraAI = window.SaaraAI || {};
6    window.SaaraAI.baseUrl = "https://work.flyos.ai";
7
8
9    // A simple, safe way to find our own script tag. This is more reliable
10    // than document.currentScript in all browsers.
11    var currentScript = document.currentScript || (function () {
12        var scripts = document.getElementsByTagName('script');
13        return scripts[scripts.length - 1];
14    })();
15
16    function triggerOnBrowserClose(callback) {
17        let triggered = false;
18
19        function fire() {
20            if (triggered) return;
21            triggered = true;
22            document.removeEventListener("mouseout", handleMouseOut);
23            document.removeEventListener("mousemove", handleMouseMove);
24            callback();
25        }
26
27        function handleMouseOut(e) {
28            if (e.clientY <= 0) fire();
29        }
30
31        function handleMouseMove(e) {
32            if (e.clientY < 10) fire();
33        }
34
35        document.addEventListener("mouseout", handleMouseOut);
36        document.addEventListener("mousemove", handleMouseMove);
37    }
38
39
40    function initialize() {
41        // --- 1. Extract the Organization ID from our own script's URL ---
42        var scriptSrc = currentScript.src;
43        try {
44            orgId = new URL(scriptSrc).searchParams.get('org_id');
45        } catch (e) {
46            console.error('SaaraAI Script Error: Could not parse script URL.', e);
47            return;
48        }
49
50        if (!orgId) {
51            console.error('SaaraAI Script Error: Organization ID (org_id) is missing from the script tag.');
52            return;
53        }
54
55        // --- 2. Create global namespace for shared variables ---
56        window.SaaraAI = window.SaaraAI || {};
57        window.SaaraAI.organizationId = orgId;
58
59        // --- Run parallel tasks (fingerprint + session + triggers) ---
60        runParallelTasks(orgId);
61
62        // --- 4. Load Dynamic Widgets from DB (e.g. newsletter popup, sales popup) ---
63        loadWidgets();
64
65        // --- 5. Load Global Merchant Apps (legacy script + CSS loaders) ---
66        var apiUrl = window.SaaraAI.baseUrl + '/global-script/config?org_id=' + orgId;
67
68        fetch(apiUrl)
69            .then(function (response) {
70                if (!response.ok) {
71                    throw new Error('Network response was not ok: ' + response.statusText);
72                }
73                return response.json();
74            })
75            .then(function (data) {
76                if (data && data.scripts) {
77                    data.scripts.forEach(function (app) {
78                        console.log('SaaraAI: Loading app -', app.name);
79
80                        if (app.css_url) {
81                            loadCss(app.css_url);
82                        }
83
84                        if (app.js_url) {
85                            loadScript(app);
86                        }
87                    });
88                }
89            })
90            .catch(function (error) {
91                console.error('SaaraAI Script Error: Failed to fetch or process configuration.', error);
92            });
93    }
94
95
96    /**
97     * Runs several independent, parallel tasks in the background.
98     * These tasks do not block the main script execution.
99     */
100    function runParallelTasks(orgId) {
101        startHeartbeat(orgId);
102        identifyUser(orgId);
103        getWidgetAction(orgId);
104        saveSessionData(orgId);
105    }
106
107    function startHeartbeat(orgId) {
108        var heartbeatUrl = window.SaaraAI.baseUrl + '/global-script/heartbeat';
109
110        function buildPayload() {
111            return {
112                org_id: orgId,
113                current_url: window.location.href,
114                origin: window.location.origin,
115                page_title: document.title || ''
116            };
117        }
118
119        function sendHeartbeat() {
120            var payload = JSON.stringify(buildPayload());
121
122            if (navigator.sendBeacon) {
123                navigator.sendBeacon(heartbeatUrl, new Blob([payload], { type: 'application/json' }));
124                return;
125            }
126
127            fetch(heartbeatUrl, {
128                method: 'POST',
129                headers: { 'Content-Type': 'application/json' },
130                body: payload,
131                keepalive: true
132            }).catch(function () {});
133        }
134
135        sendHeartbeat();
136        window.SaaraAI.heartbeatIntervalId = window.setInterval(sendHeartbeat, 60000);
137
138        document.addEventListener('visibilitychange', function () {
139            if (document.visibilityState === 'visible') {
140                sendHeartbeat();
141            }
142        });
143
144        window.addEventListener('pageshow', sendHeartbeat);
145        window.addEventListener('pagehide', sendHeartbeat);
146    }
147
148    /**
149     * Collects a device fingerprint for user identification.
150     */
151    function collectFingerprint() {
152        const nav = navigator;
153        const screenInfo = {
154            width: screen.width,
155            height: screen.height,
156            availWidth: screen.availWidth,
157            availHeight: screen.availHeight,
158            colorDepth: screen.colorDepth,
159            pixelRatio: window.devicePixelRatio
160        };
161
162        const data = {
163            ua: nav.userAgent,
164            platform: nav.platform,
165            vendor: nav.vendor,
166            languages: nav.languages,
167            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
168            language: nav.language,
169            hardwareConcurrency: nav.hardwareConcurrency || null,
170            deviceMemory: nav.deviceMemory || null,
171            maxTouchPoints: nav.maxTouchPoints || 0,
172            cookieEnabled: nav.cookieEnabled,
173            doNotTrack: nav.doNotTrack,
174            connection: nav.connection ? {
175                effectiveType: nav.connection.effectiveType,
176                downlink: nav.connection.downlink
177            } : null,
178            screen: screenInfo,
179            viewport: { w: window.innerWidth, h: window.innerHeight },
180            pluginsLength: (nav.plugins && nav.plugins.length) || 0,
181            time: Date.now()
182        };
183
184        // Canvas fingerprint (simple)
185        try {
186            const c = document.createElement('canvas');
187            c.width = 200; c.height = 50;
188            const ctx = c.getContext('2d');
189            ctx.textBaseline = 'top';
190            ctx.font = "14px 'Arial'";
191            ctx.fillStyle = '#f60';
192            ctx.fillRect(125, 1, 62, 20);
193            ctx.fillStyle = '#069';
194            ctx.fillText('fingerprint-test', 2, 15);
195            data.canvas = c.toDataURL();
196        } catch (e) { data.canvas = null; }
197
198        return data;
199    }
200
201    /**
202     * 1. Identifies the current user by collecting a device fingerprint.
203     * This function will contain more complex logic in the future before the API call.
204     */
205    function identifyUser(orgId) {
206        console.log('SaaraAI: Starting user identification process...');
207        var apiUrl = window.SaaraAI.baseUrl + '/widget/identify-user/';
208        var fingerprintData = collectFingerprint();
209
210        fetch(apiUrl, {
211            method: 'POST',
212            headers: { 'Content-Type': 'application/json' },
213            body: JSON.stringify({
214                merchantId: orgId,
215                fingerprint: fingerprintData,
216            }),
217        })
218            .then(function (response) {
219                if (!response.ok) {
220                    console.warn('SaaraAI: User identification request failed.');
221                    return null;
222                }
223                return response.json();
224            })
225            .then(function (data) {
226                if (data && data.user) {
227                    console.log('SaaraAI: User identified:', data.user);
228                    // Store user data globally if needed, e.g., window.SaaraAI.user = data.user;
229                }
230            })
231            .catch(function (error) {
232                console.error('SaaraAI Script Error: User identification failed.', error);
233            });
234    }
235
236    /**
237     * 2. Determines which dynamic action/widget to trigger.
238     */
239    function getWidgetAction(orgId) {
240        var apiUrl = window.SaaraAI.baseUrl + '/widget/trigger/';
241        fetch(apiUrl, {
242            method: 'POST',
243            headers: { 'Content-Type': 'application/json' },
244            body: JSON.stringify({
245                merchantId: orgId,
246                currentUrl: window.location.href,
247            }),
248        })
249            .then(function (response) {
250                if (!response.ok) {
251                    return null; // Fail silently if no widget to trigger
252                }
253                return response.json();
254            })
255            .then(function (data) {
256                if (data && data.widget) {
257                    var widget = data.widget;
258                    console.log('SaaraAI: Dynamically triggering widget:', widget.name);
259                    loadWidget(widget.config, widget.type, widget.name);
260                }
261            })
262            .catch(function (error) {
263                console.error('SaaraAI Script Error: Dynamic widget trigger failed.', error);
264            });
265    }
266
267    /**
268     * 3. Saves session data.
269     */
270    function saveSessionData(orgId) {
271        var apiUrl = window.SaaraAI.baseUrl + '/save-session';
272        // Dummy data for now
273        var sessionData = {
274            startTime: new Date().toISOString(),
275            pageUrl: window.location.href,
276            referrer: document.referrer,
277        };
278
279        fetch(apiUrl, {
280            method: 'POST',
281            headers: { 'Content-Type': 'application/json' },
282            body: JSON.stringify({
283                merchantId: orgId,
284                session: sessionData,
285            }),
286        })
287            .then(function (response) {
288                if (!response.ok) {
289                    console.warn('SaaraAI: Save session data request failed.');
290                } else {
291                    console.log('SaaraAI: Session data saved successfully.');
292                }
293            })
294            .catch(function (error) {
295                console.error('SaaraAI Script Error: Saving session data failed.', error);
296            });
297    }
298
299    /**
300     * Creates and injects a <script> tag into the document's <head>.
301     * @param {object} app - The script object from our API response.
302     */
303    function loadScript(app) {
304        var script = document.createElement('script');
305        script.src = app.js_url;
306        script.async = true;
307        // This is how we pass the specific config to the app script.
308        // The app script can then find its own tag and read this data.
309        script.dataset.saaraConfig = JSON.stringify(app.config);
310        script.dataset.saaraAppName = app.name;
311        document.head.appendChild(script);
312    }
313
314    /**
315     * Creates and injects a <link rel="stylesheet"> tag into the document's <head>.
316     * @param {string} href - The URL of the CSS file.
317     */
318    function loadCss(href) {
319        var link = document.createElement('link');
320        link.rel = 'stylesheet';
321        link.type = 'text/css';
322        link.href = href;
323        document.head.appendChild(link);
324    }
325
326    // --- Core Engine v2.0 Widget Loading Logic ---
327
328    /**
329     * Fetches and loads widget configurations based on targeting rules.
330     * This logic is merged from the original core-engine.js.
331     */
332    function loadWidgets() {
333        var currentUrl = window.location.href;
334        var urlParams = new URLSearchParams(window.location.search);
335        var ugcToken = urlParams.get('ugc_review_token');
336        var configApiUrl = window.SaaraAI.baseUrl + '/widget/config/merchant/';
337
338        fetch(configApiUrl, {
339            method: 'POST',
340            headers: { 'Content-Type': 'application/json' },
341            body: JSON.stringify({
342                merchantId: orgId,
343                ugcToken: ugcToken,
344                currentUrl: currentUrl,
345            }),
346        })
347            .then(function (response) {
348                if (!response.ok) {
349                    throw new Error('Configuration request failed with status ' + response.status);
350                }
351                return response.json();
352            })
353            .then(function (config) {
354                // Check UGC Agent widget - this should only be triggered if the ugc_review_token is present.
355                if (ugcToken && config.features && config.features.ugcAgent && config.features.ugcAgent.enabled) {
356                    var targetingRules = config.features.ugcAgent.targetingRules;
357                    if (matchesTargeting(targetingRules, currentUrl)) {
358                        console.log('SaaraAI: UGC Agent enabled and targeting rules matched. Loading widget...');
359                        // The widget name for a token-based trigger is always 'Collect UGC'.
360                        loadWidget(config.features.ugcAgent, 'ugcAgent', 'Collect UGC');
361                    } else {
362                        console.log('SaaraAI: UGC Agent enabled but targeting rules did not match current page.');
363                    }
364                }
365
366                // Check Sales Agent widget
367                if (config.features && config.features.salesAgent && config.features.salesAgent.enabled) {
368                    var salesAgentTargeting = config.features.salesAgent.targetingRules;
369                    if (matchesTargeting(salesAgentTargeting, currentUrl)) {
370                        console.log('SaaraAI: Sales Agent enabled and targeting rules matched. Loading widget...');
371                        loadWidget(config.features.salesAgent, 'salesAgent', 'Sales Widget');
372                    } else {
373                        console.log('SaaraAI: Sales Agent enabled but targeting rules did not match current page.');
374                    }
375                }
376
377                // --- Load AI-generated custom widgets dynamically ---
378                if (config.customWidgets && config.customWidgets.length > 0) {
379                    config.customWidgets.forEach(function (cw) {
380                        if (matchesTargeting(cw.targetingRules, currentUrl)) {
381                            var trigger = cw.targetingRules && cw.targetingRules.trigger;
382                            var delaySeconds = cw.targetingRules && cw.targetingRules.delaySeconds;
383                            var scrollPercent = cw.targetingRules && cw.targetingRules.scrollPercent;
384                            var inactivitySeconds = cw.targetingRules && cw.targetingRules.inactivitySeconds;
385
386                            function injectWidget() {
387                                console.log('SaaraAI: Loading custom widget: ' + cw.name);
388                                try {
389                                    // Inject CSS
390                                    if (cw.code && cw.code.css) {
391                                        var style = document.createElement('style');
392                                        style.setAttribute('data-saara-widget', cw.id);
393                                        style.textContent = cw.code.css;
394                                        document.head.appendChild(style);
395                                    }
396                                    // Inject HTML
397                                    if (cw.code && cw.code.html) {
398                                        var container = document.createElement('div');
399                                        container.setAttribute('data-saara-widget', cw.id);
400                                        container.innerHTML = cw.code.html;
401                                        document.body.appendChild(container);
402                                    }
403                                    // Execute JS with widgetConfig context
404                                    if (cw.code && cw.code.js) {
405                                        var script = document.createElement('script');
406                                        script.setAttribute('data-saara-widget', cw.id);
407                                        script.textContent = '(function(){var widgetConfig={merchantId:"' + orgId + '",baseUrl:"' + window.SaaraAI.baseUrl + '",widgetId:"' + cw.id + '"};' + cw.code.js + '})();';
408                                        document.head.appendChild(script);
409                                    }
410                                    trackWidgetLoad(cw.name, 'custom', cw.id);
411                                } catch (err) {
412                                    console.error('SaaraAI Error: Failed to load custom widget ' + cw.name, err);
413                                }
414                            }
415
416                            if (trigger === 'add_to_cart') {
417                                var atcInjected = false;
418
419                                function onAddToCart() {
420                                    if (atcInjected) return;
421                                    atcInjected = true;
422                                    console.log('SaaraAI: Add to cart detected, triggering widget "' + cw.name + '"');
423                                    injectWidget();
424                                }
425
426                                // --- Strategy 1: Click on button/link whose text or attributes match ---
427                                var atcPattern = /add[\s\-_]?to[\s\-_]?(cart|bag|basket|trolley)|buy[\s\-_]?now/i;
428
429                                document.addEventListener('click', function (e) {
430                                    if (atcInjected) return;
431                                    var el = e.target;
432                                    // Walk up to 3 levels to handle clicks on child elements (e.g. <span> inside <button>)
433                                    for (var i = 0; i < 3 && el; i++) {
434                                        var tag = el.tagName && el.tagName.toLowerCase();
435                                        if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'submit') {
436                                            var text = (el.textContent || el.value || '').trim();
437                                            var label = el.getAttribute('aria-label') || '';
438                                            var name = el.getAttribute('name') || '';
439                                            var id = el.id || '';
440                                            var cls = el.className || '';
441                                            if (
442                                                atcPattern.test(text) ||
443                                                atcPattern.test(label) ||
444                                                atcPattern.test(name) ||
445                                                atcPattern.test(id) ||
446                                                atcPattern.test(cls)
447                                            ) {
448                                                onAddToCart();
449                                                return;
450                                            }
451                                        }
452                                        el = el.parentElement;
453                                    }
454                                }, true);
455
456                                // --- Strategy 2: Shopify & common platform cart events ---
457                                document.addEventListener('cart:add', onAddToCart);
458                                document.addEventListener('theme:cart:add', onAddToCart);
459                                document.addEventListener('add-to-cart', onAddToCart);
460
461                                // --- Strategy 3: Intercept fetch/XHR POST to common cart endpoints ---
462                                var cartEndpointPattern = /\/cart\/(add|update)|\/api\/cart|CartAdd/i;
463
464                                var origFetch = window.fetch;
465                                window.fetch = function (input, init) {
466                                    var url = (typeof input === 'string' ? input : (input && input.url)) || '';
467                                    if (!atcInjected && (init && init.method || '').toUpperCase() === 'POST' && cartEndpointPattern.test(url)) {
468                                        onAddToCart();
469                                    }
470                                    return origFetch.apply(this, arguments);
471                                };
472
473                                var origOpen = XMLHttpRequest.prototype.open;
474                                var origSend = XMLHttpRequest.prototype.send;
475                                XMLHttpRequest.prototype.open = function (method, url) {
476                                    this._saaraMethod = method;
477                                    this._saaraUrl = url;
478                                    return origOpen.apply(this, arguments);
479                                };
480                                XMLHttpRequest.prototype.send = function () {
481                                    if (!atcInjected &&
482                                        (this._saaraMethod || '').toUpperCase() === 'POST' &&
483                                        cartEndpointPattern.test(this._saaraUrl || '')) {
484                                        onAddToCart();
485                                    }
486                                    return origSend.apply(this, arguments);
487                                };
488
489                            } else if (trigger === 'login_click') {
490                                var loginInjected = false;
491
492                                function onLoginClick() {
493                                    if (loginInjected) return;
494                                    loginInjected = true;
495                                    console.log('SaaraAI: Login/Signup click detected, triggering widget "' + cw.name + '"');
496                                    injectWidget();
497                                }
498
499                                // --- Pattern to match login/signup actions ---
500                                var loginPattern = /login|log[\s\-_]?in|sign[\s\-_]?in|signup|sign[\s\-_]?up|register|create[\s\-_]?account|join/i;
501
502                                // --- Strategy 1: Click detection ---
503                                document.addEventListener('click', function (e) {
504                                    if (loginInjected) return;
505                                    var el = e.target;
506
507                                    // Traverse up to handle nested elements (span inside button etc.)
508                                    for (var i = 0; i < 3 && el; i++) {
509                                        var tag = el.tagName && el.tagName.toLowerCase();
510
511                                        if (tag === 'button' || tag === 'a' || tag === 'input') {
512                                            var text = (el.textContent || el.value || '').trim();
513                                            var label = el.getAttribute('aria-label') || '';
514                                            var name = el.getAttribute('name') || '';
515                                            var id = el.id || '';
516                                            var cls = el.className || '';
517                                            var href = el.getAttribute('href') || '';
518
519                                            if (
520                                                loginPattern.test(text) ||
521                                                loginPattern.test(label) ||
522                                                loginPattern.test(name) ||
523                                                loginPattern.test(id) ||
524                                                loginPattern.test(cls) ||
525                                                loginPattern.test(href)
526                                            ) {
527                                                onLoginClick();
528                                                return;
529                                            }
530                                        }
531
532                                        el = el.parentElement;
533                                    }
534                                }, true);
535
536                                // --- Strategy 2: Detect navigation to auth routes ---
537                                var authRoutePattern = /login|signin|signup|register|account/i;
538
539                                window.addEventListener('popstate', function () {
540                                    if (!loginInjected && authRoutePattern.test(location.href)) {
541                                        onLoginClick();
542                                    }
543                                });
544
545                                // --- Strategy 3: Intercept API calls (login/signup requests) ---
546                                var authApiPattern = /login|signin|signup|register|auth|account/i;
547
548                                var origFetch = window.fetch;
549                                window.fetch = function (input, init) {
550                                    var url = (typeof input === 'string' ? input : (input && input.url)) || '';
551                                    if (
552                                        !loginInjected &&
553                                        (init && init.method || '').toUpperCase() === 'POST' &&
554                                        authApiPattern.test(url)
555                                    ) {
556                                        onLoginClick();
557                                    }
558                                    return origFetch.apply(this, arguments);
559                                };
560
561                                var origOpen = XMLHttpRequest.prototype.open;
562                                var origSend = XMLHttpRequest.prototype.send;
563
564                                XMLHttpRequest.prototype.open = function (method, url) {
565                                    this._saaraMethod = method;
566                                    this._saaraUrl = url;
567                                    return origOpen.apply(this, arguments);
568                                };
569
570                                XMLHttpRequest.prototype.send = function () {
571                                    if (
572                                        !loginInjected &&
573                                        (this._saaraMethod || '').toUpperCase() === 'POST' &&
574                                        authApiPattern.test(this._saaraUrl || '')
575                                    ) {
576                                        onLoginClick();
577                                    }
578                                    return origSend.apply(this, arguments);
579                                };
580                            }
581                            else if (trigger === 'inactivity') {
582                                var inactivityDelay = inactivitySeconds > 0 ? inactivitySeconds : 30;
583                                console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + inactivityDelay + 's of inactivity');
584                                var inactivityTimer = null;
585                                var inactivityInjected = false;
586                                var inactivityLastMove = 0;
587                                var inactivityEvents = ['mousemove', 'keydown', 'scroll', 'click', 'touchstart'];
588
589                                function resetInactivityTimer(e) {
590                                    if (inactivityInjected) return;
591                                    if (e && e.type === 'mousemove') {
592                                        var now = Date.now();
593                                        if (now - inactivityLastMove < 500) return;
594                                        inactivityLastMove = now;
595                                    }
596                                    clearTimeout(inactivityTimer);
597                                    inactivityTimer = setTimeout(function () {
598                                        if (inactivityInjected) return;
599                                        inactivityInjected = true;
600                                        inactivityEvents.forEach(function (ev) {
601                                            document.removeEventListener(ev, resetInactivityTimer);
602                                        });
603                                        injectWidget();
604                                    }, inactivityDelay * 1000);
605                                }
606
607                                inactivityEvents.forEach(function (ev) {
608                                    document.addEventListener(ev, resetInactivityTimer, { passive: true });
609                                });
610                                resetInactivityTimer(null);
611                            } else if (trigger === 'timed_delay' && delaySeconds > 0) {
612                                console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + delaySeconds + 's delay');
613                                setTimeout(injectWidget, delaySeconds * 1000);
614                            } else if (trigger === 'exit_intent') {
615                                console.log('SaaraAI: Widget "' + cw.name + '" will load on exit intent');
616                                triggerOnBrowserClose(injectWidget);
617                            } else if (trigger === 'scroll_depth' && scrollPercent > 0) {
618                                console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + scrollPercent + '% scroll depth');
619                                var injected = false;
620                                function onScroll() {
621                                    if (injected) return;
622                                    var scrolled = (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100;
623                                    if (scrolled >= scrollPercent) {
624                                        injected = true;
625                                        window.removeEventListener('scroll', onScroll);
626                                        injectWidget();
627                                    }
628                                }
629                                window.addEventListener('scroll', onScroll);
630                            } else if (trigger === 'abandoned_cart') {
631                                var hasTargetUrls = cw.targetingRules.targetUrls && cw.targetingRules.targetUrls.length > 0;
632                                var isCartPage = hasTargetUrls
633                                    ? true // already filtered by matchesTargeting() above
634                                    : /\/(cart|checkout|basket|bag|trolley)(\/|$|\?|#)/i.test(window.location.pathname)
635                                      || /[?&](cart|checkout)=/i.test(window.location.search);
636
637                                if (!isCartPage) {
638                                    console.log('SaaraAI: Widget "' + cw.name + '" skipped - not a cart page');
639                                    return;
640                                }
641
642                                var delay = inactivitySeconds > 0 ? inactivitySeconds : 30;
643                                console.log('SaaraAI: Widget "' + cw.name + '" will load after ' + delay + 's of cart inactivity');
644                                var cartTimer = null;
645                                var cartInjected = false;
646                                var lastMoveTime = 0;
647                                var activityEvents = ['mousemove', 'keydown', 'scroll', 'click', 'touchstart'];
648
649                                function cancelCartTimer() {
650                                    if (cartTimer) clearTimeout(cartTimer);
651                                }
652
653                                function startCartTimer() {
654                                    cancelCartTimer();
655                                    cartTimer = setTimeout(function () {
656                                        if (cartInjected) return;
657                                        cartInjected = true;
658                                        activityEvents.forEach(function (ev) {
659                                            document.removeEventListener(ev, resetCartTimer);
660                                        });
661                                        injectWidget();
662                                    }, delay * 1000);
663                                }
664
665                                function resetCartTimer(e) {
666                                    if (cartInjected) return;
667                                    // Throttle mousemove to at most once every 500ms
668                                    if (e && e.type === 'mousemove') {
669                                        var now = Date.now();
670                                        if (now - lastMoveTime < 500) return;
671                                        lastMoveTime = now;
672                                    }
673                                    startCartTimer();
674                                }
675
676                                activityEvents.forEach(function (ev) {
677                                    document.addEventListener(ev, resetCartTimer, { passive: true });
678                                });
679                                startCartTimer();
680                            } else {
681                                injectWidget();
682                            }
683                        }
684                    });
685                }
686            })
687            .catch(function (error) {
688                console.error("SaaraAI Error: Could not load widget configuration.", error);
689            });
690    }
691
692    /**
693     * Checks if the current URL matches any of the targeting rules.
694     */
695    function matchesTargeting(targetingRules, currentUrl) {
696        if (!targetingRules || !targetingRules.targetUrls || targetingRules.targetUrls.length === 0) {
697            return true; // No rules means show on all pages
698        }
699        var currentUrlNormalized = currentUrl.split('?')[0].split('#')[0];
700        return targetingRules.targetUrls.some(function (targetUrl) {
701            var targetUrlNormalized = targetUrl.split('?')[0].split('#')[0];
702            return currentUrlNormalized === targetUrlNormalized;
703        });
704    }
705
706    /**
707     * Loads a specific widget by either executing its rendered script or loading an external URL.
708     */
709
710    function loadWidget(widgetConfig, widgetType, widgetName) {
711        if (!widgetName) {
712            console.error('SaaraAI Error: widgetName is required for tracking.');
713            return;
714        }
715
716        const targetingRules = widgetConfig?.targetingRules || {};
717        const targetUrls = targetingRules?.targetUrls || [];
718        const exitIntent = targetingRules?.exit_intent || "on_load";
719
720        const currentUrl = window.location.href;
721
722        // ✅ URL NORMALIZER (removes query + hash + trailing slash)
723        function normalizeUrl(url) {
724            if (!url) return "";
725            return url
726                .split("?")[0]
727                .split("#")[0]
728                .replace(/\/$/, "")
729                .toLowerCase();
730        }
731
732        function isTargetUrlMatch() {
733            if (!targetUrls.length) return true; // No targeting = show everywhere
734
735            const currentNormalized = normalizeUrl(currentUrl);
736
737            return targetUrls.some(target => {
738                return normalizeUrl(target) === currentNormalized;
739            });
740        }
741
742        // ✅ STOP if URL does not match
743        if (!isTargetUrlMatch()) {
744            console.log("SaaraAI: Widget skipped - URL not matched");
745            return;
746        }
747
748        function executeWidget() {
749            try {
750                if (widgetConfig.renderedScript) {
751                    console.log('SaaraAI: Loading ' + widgetType + ' using database-driven template');
752                    executeRenderedScript(widgetConfig.renderedScript);
753                    trackWidgetLoad(widgetName);
754                } else if (widgetConfig.scriptUrl) {
755                    console.log('SaaraAI: Loading ' + widgetType + ' using legacy script file');
756                    loadExternalScript(widgetConfig.scriptUrl).then(function () {
757                        if (
758                            widgetType === 'ugcAgent' &&
759                            window.SaaraAIPlatformUgcAgent &&
760                            typeof window.SaaraAIPlatformUgcAgent.init === 'function'
761                        ) {
762                            var urlParams = new URLSearchParams(window.location.search);
763                            var ugcToken = urlParams.get('ugc_review_token');
764                            var initOptions = widgetConfig.options || {};
765                            initOptions.merchantId = orgId;
766                            initOptions.ugcToken = ugcToken;
767                            window.SaaraAIPlatformUgcAgent.init(initOptions);
768                        }
769                        trackWidgetLoad(widgetName);
770                    });
771                }
772            } catch (error) {
773                console.error('SaaraAI Error: Failed to load ' + widgetType + ' widget', error);
774            }
775        }
776
777        // ✅ EXIT INTENT HANDLING
778        if (exitIntent === "browser_close") {
779            console.log("SaaraAI: Widget will trigger on browser close");
780            triggerOnBrowserClose(executeWidget);
781        } else {
782            console.log("SaaraAI: Widget triggered on load");
783            executeWidget();
784        }
785    }
786
787
788    /**
789     * Tracks the loading of a widget for analytics.
790     */
791    function trackWidgetLoad(widgetName, widgetType, widgetId) {
792        var trackingUrl = window.SaaraAI.baseUrl + '/widget/track-load/';
793        var payload = {
794            merchantId: orgId,
795            widgetName: widgetName,
796        };
797        if (widgetType) payload.widgetType = widgetType;
798        if (widgetId) payload.widgetId = widgetId;
799        fetch(trackingUrl, {
800            method: 'POST',
801            headers: { 'Content-Type': 'application/json' },
802            body: JSON.stringify(payload),
803        }).catch(function (error) {
804            console.debug('SaaraAI: Widget load tracking failed', error);
805        });
806    }
807
808    /**
809     * Executes a script string by creating a new script tag.
810     */
811    function executeRenderedScript(scriptContent) {
812        try {
813            var script = document.createElement('script');
814            script.textContent = scriptContent;
815            document.head.appendChild(script);
816        } catch (error) {
817            console.error('SaaraAI Error: Failed to execute widget script', error);
818        }
819    }
820
821    /**
822     * Loads an external script file asynchronously.
823     */
824    function loadExternalScript(src) {
825        return new Promise(function (resolve, reject) {
826            var script = document.createElement('script');
827            script.src = src;
828            script.async = true;
829            script.onload = resolve;
830            script.onerror = function () {
831                reject(new Error('Failed to load script: ' + src));
832            };
833            document.head.appendChild(script);
834        });
835    }
836
837    // --- 5. Run the initialization logic ---
838    // We wait for the DOM to be fully loaded to ensure we don't block page rendering.
839    if (document.readyState === 'loading') {
840        document.addEventListener('DOMContentLoaded', initialize);
841    } else {
842        initialize();
843    }
844
845})();

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.