PageSourceSearch

https://scopicsoftware.com/scopic-ai/js/scopic-ai.js

js scopicsoftware.com collected 2026-09-25 04:14:47 UTC 38,064 bytes, 1,356 lines download raw bytes

1
2function getScopicAIConfig() {
3  const script = document.getElementById('scopic-ai-standalone') || document.currentScript;
4
5  return {
6    API_ENDPOINT: (script && script.dataset.chatEndpoint) || "",
7    GEOLOCATION_ENDPOINT: (script && script.dataset.geoEndpoint) || "",
8    WEBSOCKET_ENDPOINT: (script && script.dataset.wsEndpoint) || "",
9    HOST_PATH: (script && script.dataset.hostPath) || "/scopic-ai",
10    API_KEY: (script && script.dataset.apiKey) || "",
11  };
12}
13
14var scopicAI = getScopicAIConfig();
15
16jQuery(document).ready(function ($) {
17
18  const API_BASE_URL = scopicAI.API_ENDPOINT;
19  const INIT_SESSION_ENDPOINT = `${API_BASE_URL}init_chat_session`;
20  const WEBSOCKET_ENDPOINT = scopicAI.WEBSOCKET_ENDPOINT
21  const KEY_NAME_STORAGE = "session_ai_chat_id";
22  const KEY_CHAT_HISTORY_STORAGE = "scopic_ai_chat_history";
23  const KEY_SEO_VISIT_DATA = "scopic_ai_seo_visit_data";
24  const KEY_SESSION_START = "scopic_ai_session_start";
25  const KEY_PAGES_VISITED = "scopic_ai_pages_visited";
26  const KEY_PAGE_REVISITS = "scopic_ai_page_revisits";
27  const KEY_LOCATION_INFO = "scopic_ai_location_info";
28  const KEY_LINGER_START = "scopic_ai_linger_start";
29  const KEY_LINGER_TRIGGERED = "scopic_ai_linger_triggered";
30  const KEY_WELCOME_SHOWN = "scopic_ai_welcome_shown";
31  const MAX_PROACTIVE_MESSAGES_PER_PAGE = 2;
32
33  const chatbot_icon_path = `${scopicAI.HOST_PATH}/images/chat-vector.svg`;
34  const chatbot_audio_path = `${scopicAI.HOST_PATH}/audio/pop.mp3`;
35
36  const chatbot_icon = $("#scopic_chatbot_icon");
37  const chatbot_intro_screen = $("#scopic_chatbot_splash_intro");
38  const chatbot_intro_close_btn = $("#scopic_chatbot_intro_close_btn");
39  const intro_screen_chat_with_me_btn = $("#scopic_chatbot_splash_cta");
40  const chatroom = $("#scopic_ai_chatroom");
41  const chatroom_close_btn = $("#scopic_chatbot_close_btn");
42  const chat_thread_window = $("#scopic_chatbot_chatbox");
43  const chat_history_panel = $("#scopic_chatbot_chat_history_panel");
44  const chat_input_form = $("#scopic_chatbot_chat_form");
45  const chatbot_typing = $("#scopic_chatbot_typing");
46  const chatbot_bubble = $("#scopic_chatbot_bubble");
47  const chatbot_bubble_close_btn = $("#scopic_chatbot_bubble_close_btn")
48  const chatbot_session_closed = $("#scopic_chatbot_session_closed");
49  const chatbot_session_closed_close_btn = $("#scopic_chatbot_session_closed_close_btn");
50  const chatbot_session_closed_reason = $("#scopic_chatbot_session_closed_reason");
51  const chatbot_disclaimer_text = $("#scopic_chatbot_disclaimer_text");
52
53
54  let sessionTimeoutTimer = null;
55  let bubbleTimeouts = [];
56  let bubbleLoopInternal = null;
57  let bubbleRunId = 0;
58  let bubbleState = "hidden";
59  let messageIndex = 0;
60  let lastHeight = chat_thread_window[0].scrollHeight;
61  let intervalInactiveTime = null;
62  let userActionsData = null;
63  let initMessage = "Welcome to Scopic! With our expertise in custom software development, we empower businesses to overcome their challenges and achieve strategic success. How can I help you today?";
64  let chat_history_data = JSON.parse(getLocalStorage(KEY_CHAT_HISTORY_STORAGE));
65  let sessionClosedReason = null;
66  let proactiveMessageActive = false;
67  let proactiveMessageCount = 0;
68  let bubbleAutoHideTimer = null;
69  const bubbleTiming = {
70    visibleMs: 4500,
71    gapMs: 18500,
72    autoHideMs: 120000,
73    initialDelayMs: 20000,
74  };
75
76  if (!chat_history_data || !Array.isArray(chat_history_data)){
77    chat_history_data = [];
78  }
79
80
81  // START EVENT LISTENERS
82
83  chatbot_icon.on("click", function () {
84    openChat();
85  });
86
87  chatbot_bubble.on("click", function () {
88    openChat();
89  });
90
91  chatbot_intro_close_btn.on("click", function () {
92    hideFromScreen(chatbot_intro_screen);
93    if (!getSessionId()) {
94      scheduleBubbleSequence()
95    }
96  });
97
98  intro_screen_chat_with_me_btn.on("click", function () {
99    hideFromScreen(chatbot_intro_screen);
100    showOnScreen(chatroom);
101
102    initShowMessage(true)
103    if (!getSessionId()) {
104      startManualChat();
105    }
106  });
107
108  chatroom_close_btn.on("click", function () {
109    hideFromScreen(chatroom);
110    if (!getSessionId()) {
111      scheduleBubbleSequence()
112    }
113  });
114
115  chatbot_session_closed_close_btn.on("click", function () {
116    hideFromScreen(chatbot_session_closed);
117    if (!getSessionId()) {
118      scheduleBubbleSequence()
119    }
120  });
121
122  chat_input_form.on("submit", function (e) {
123    e.preventDefault();
124
125    disableChatInput();
126
127    const message_input = $(this).find(
128        'input[name="scopic_chatbot_msg_input"]'
129    );
130    const user_question = message_input.val();
131
132    if (!user_question.trim()) {
133      enableChatInput();
134      return;
135    }
136
137    renderUserMessage(user_question);
138    message_input.val("");
139
140    sendChatMessage(user_question);
141  });
142
143  chatbot_bubble_close_btn.on("click", function (e) {
144    e.preventDefault();
145    e.stopPropagation();
146    bubbleHide(true);
147  })
148
149  window.addEventListener('beforeunload', function() {
150    let visitData = JSON.parse(localStorage.getItem(KEY_SEO_VISIT_DATA));
151    let sessionStart = sessionStorage.getItem(KEY_SESSION_START);
152
153    if (visitData && sessionStart) {
154      const sessionTime = Math.floor(Date.now() / 1000) - parseInt(sessionStart);
155      visitData.total_time_spent += sessionTime;
156      localStorage.setItem(KEY_SEO_VISIT_DATA, JSON.stringify(visitData));
157    }
158  });
159
160  // END EVENT LISTENERS
161
162
163  // START INITIALIZATION
164
165  init();
166
167  function init(){
168    applyLocaleStrings();
169    initHistory();
170    starChat();
171    initShowMessage();
172    initBundle();
173
174  }
175
176  // Keeps the widget's static copy in sync with the site locale (/ko pages).
177  // Runs on load and again on every client-side route change via scopicAIReinit().
178  function applyLocaleStrings(){
179    if (!chatbot_disclaimer_text.length) return;
180
181    const isKorean = /^\/ko(\/|$)/.test(window.location.pathname);
182    const text = chatbot_disclaimer_text.attr(isKorean ? "data-ko" : "data-en");
183
184    if (text) {
185      chatbot_disclaimer_text.text(text);
186    }
187  }
188
189  function initHistory(){
190    if (!getSessionId()){
191      removeLocalStorage(KEY_CHAT_HISTORY_STORAGE);
192      chat_history_data=[]
193    }
194  }
195
196  async function starChat() {
197    try {
198      const data = await trackPageVisit();
199
200      const USER_INTERACTION_ENDPOINT = `${API_BASE_URL}user_interaction`;
201
202      const requestOptions = {
203        method: "POST",
204        headers:getAuthHeaders(),
205        body: JSON.stringify(data),
206      };
207
208      const response = await fetch(USER_INTERACTION_ENDPOINT, requestOptions);
209
210
211      if (!response.ok) {
212        throw new Error("Error sending user interaction data.", response);
213      }
214
215      let responseData = await response.json();
216
217      if (responseData && responseData.session_id) {
218        const existingSessionId = getSessionId();
219        if (!existingSessionId) {
220          setSessionId(responseData.session_id);
221        }
222      }
223
224      if (responseData && responseData.messages && responseData.thresholds) {
225        userActionsData = {
226          messages: responseData.messages,
227          thresholds: responseData.thresholds,
228          user_info: responseData.user_info
229        };
230      } else {
231        userActionsData = null;
232        console.log('No actions data available (messages or thresholds are null/empty)');
233      }
234
235      initActionListeners();
236
237    } catch (error) {
238      console.error("Error in init():", error);
239      userActionsData = null;
240    }
241  }
242
243  async function startManualChat() {
244    try {
245      clearBubbleAnimationTimers()
246      bubbleHide(true)
247
248      const oldSessionId = getSessionId();
249      if (oldSessionId) {
250        sessionStorage.removeItem(`${KEY_WELCOME_SHOWN}_${oldSessionId}`);
251      }
252
253      sessionStorage.removeItem(KEY_LINGER_START);
254      sessionStorage.removeItem(KEY_LINGER_TRIGGERED);
255
256      removeLocalStorage(KEY_CHAT_HISTORY_STORAGE);
257      chat_history_data = [];
258      initShowMessage(true)
259      disableChatInput();
260
261      const locationInfo = await getLocationInfo();
262      const seoMetrics = getSEOMetrics();
263
264      const requestOptions = {
265        method: "POST",
266        headers: getAuthHeaders(),
267        body: JSON.stringify({
268          location_info: locationInfo,
269          seo_metrics: seoMetrics
270        }),
271      };
272
273      const response = await fetch(INIT_SESSION_ENDPOINT, requestOptions);
274
275      if (!response.ok) {
276        throw new Error("Error initializing chat session.", response);
277      }
278
279      const data = await response.json();
280
281      const session_id = data.session_id;
282      setSessionId(session_id);
283
284      enableChatInput();
285    } catch (error) {
286      console.error("Error initializing chat session:", error);
287    }
288  }
289
290  function initActionListeners() {
291    if (isUserInConversation()) {
292      console.log('User is already in conversation, skipping all action listeners');
293      return;
294    }
295
296    const actions = getActions();
297
298    if (!actions || actions.length === 0) {
299      showInitialWelcomeMessage('Hi there! Can I help you?');
300      return;
301    }
302
303    const pageLoadMessage = userActionsData?.messages?.on_page_load || 'Hi there! Can I help you?';
304    showInitialWelcomeMessage(pageLoadMessage);
305
306
307    actions.forEach(action => {
308      if (action.action === 'on_page_linger') {
309        setupPageLingerListener(action);
310      } else if (action.action === 'on_page_navigation') {
311        setupPageNavigationListener(action);
312      }
313    });
314  }
315
316  function setupPageLingerListener(action) {
317    const lingerTimeMs = action.trigger * 1000;
318
319    if (getSessionId() && isUserInConversation()) {
320      return;
321    }
322
323    if (sessionStorage.getItem(KEY_LINGER_TRIGGERED) === 'true') {
324      return;
325    }
326
327    let lingerStart = sessionStorage.getItem(KEY_LINGER_START);
328    if (!lingerStart) {
329      lingerStart = Date.now();
330      sessionStorage.setItem(KEY_LINGER_START, lingerStart.toString());
331      console.log('Linger timer started at:', lingerStart);
332    } else {
333      lingerStart = parseInt(lingerStart);
334    }
335
336    const elapsed = Date.now() - lingerStart;
337    const remaining = lingerTimeMs - elapsed;
338
339    if (remaining <= 0) {
340      if (getSessionId()) {
341        return;
342      }
343      triggerLingerAction(action);
344      return;
345    }
346
347    setTimeout(() => {
348      if (isUserInConversation()) {
349        console.log('User is in conversation, skipping page linger action');
350        return;
351      }
352
353      if (isChatOpen()) {
354        console.log('Chat is open, skipping page linger action');
355        return;
356      }
357
358      triggerLingerAction(action);
359    }, remaining);
360  }
361
362  function triggerLingerAction(action) {
363    sessionStorage.setItem(KEY_LINGER_TRIGGERED, 'true');
364
365    if (isChatOpen()) {
366      return;
367    }
368
369    updateActionMessage(action.message);
370    bubbleShow(true, action.message);
371  }
372
373  function setupPageNavigationListener(action) {
374    if (action.message) {
375      if (isUserInConversation()) {
376        return;
377      }
378
379      if (isChatOpen()) {
380        return;
381      }
382
383      updateActionMessage(action.message);
384
385      bubbleShow(true, action.message);
386      console.log('Showing page navigation message in bubble:', action.message);
387    } else {
388      console.log('No page navigation message available');
389    }
390  }
391
392  // END INITIALIZATION
393
394  // START AUTH HELPERS
395
396  function getAuthHeaders() {
397    const headers = {
398      "Content-Type": "application/json",
399    };
400    if (scopicAI.API_KEY) {
401      headers["X-API-Key"] = scopicAI.API_KEY;
402    }
403    return headers;
404  }
405
406  // END AUTH HELPERS
407
408
409  //START SEO METRICS HELPERS
410
411  function getSEOMetrics() {
412    const now = Math.floor(Date.now() / 1000);
413    const currentUrl = window.location.href;
414
415    let referrer = document.referrer;
416    if (!referrer || referrer.trim() === '') {
417      referrer = window.location.origin;
418    }
419
420    let visitData = JSON.parse(localStorage.getItem(KEY_SEO_VISIT_DATA)) || {
421      first_visit: now,
422      first_page_visited: currentUrl,
423      visits: [],
424      total_time_spent: 0
425    };
426
427    visitData.most_recent_visit = now;
428    visitData.visits.push({
429      timestamp: now,
430      url: currentUrl,
431      referrer: referrer,
432      start_time: now
433    });
434
435    const averageTimeSpent = calculateAverageTimeSpent(visitData);
436    localStorage.setItem(KEY_SEO_VISIT_DATA, JSON.stringify(visitData));
437
438    return {
439      most_recent_visit: visitData.most_recent_visit,
440      average_time_spent: averageTimeSpent,
441      referrer: referrer,
442      first_visit: visitData.first_visit,
443      first_page_visited: visitData.first_page_visited
444    };
445  }
446
447  function calculateAverageTimeSpent(visitData) {
448    if (visitData.visits.length === 0) {
449      return "0 mins";
450    }
451
452    let sessionStart = sessionStorage.getItem(KEY_SESSION_START);
453
454    if (!sessionStart) {
455      sessionStart = Math.floor(Date.now() / 1000);
456      sessionStorage.setItem(KEY_SESSION_START, sessionStart.toString());
457    } else {
458      sessionStart = parseInt(sessionStart);
459    }
460
461    const currentSessionTime = Math.floor(Date.now() / 1000) - sessionStart;
462
463    const totalTime = visitData.total_time_spent + currentSessionTime;
464    const totalVisits = visitData.visits.length;
465
466    const averageSeconds = Math.floor(totalTime / totalVisits);
467
468    return formatTimeSpent(averageSeconds);
469  }
470
471  async function trackPageVisit() {
472    const currentUrl = window.location.href;
473
474    let pagesVisited = JSON.parse(getLocalStorage(KEY_PAGES_VISITED)) || [];
475    let pageRevisits = JSON.parse(getLocalStorage(KEY_PAGE_REVISITS)) || {};
476
477    if (!pagesVisited.includes(currentUrl)) {
478      pagesVisited.push(currentUrl);
479    }
480
481    if (pageRevisits[currentUrl]) {
482      pageRevisits[currentUrl]++;
483    } else {
484      pageRevisits[currentUrl] = 1;
485    }
486
487    setLocalStorage(KEY_PAGES_VISITED, JSON.stringify(pagesVisited));
488    setLocalStorage(KEY_PAGE_REVISITS, JSON.stringify(pageRevisits));
489
490    const locationInfo = await getLocationInfo();
491    const seoMetrics = getSEOMetrics();
492    const sessionId = getSessionId();
493
494    const trackingData = {
495      pages_visited: pagesVisited,
496      page_revisits: pageRevisits,
497      current_page_url: currentUrl,
498      session_id: sessionId || null,
499      location_info: locationInfo,
500      seo_metrics: seoMetrics
501    };
502
503    return trackingData;
504  }
505
506  async function getLocationInfo() {
507    const cachedLocationInfo = getLocalStorage(KEY_LOCATION_INFO);
508
509    if (cachedLocationInfo) {
510      try {
511        return JSON.parse(cachedLocationInfo);
512      } catch (error) {
513        console.error("Error parsing cached location info.", error);
514      }
515    }
516
517    try {
518      const response = await fetch(scopicAI.GEOLOCATION_ENDPOINT);
519
520      if (!response.ok) {
521        throw new Error("Error fetching geolocation data.", response);
522      }
523
524      const data = await response.json();
525
526      const locationInfo = {
527        ip: data.ip,
528        hostname: data.hostname || window.location.hostname,
529        location: data.location,
530      };
531
532      setLocalStorage(KEY_LOCATION_INFO, JSON.stringify(locationInfo));
533
534      return locationInfo;
535    } catch (error) {
536      console.error("Error fetching geolocation data.", error);
537      const fallbackInfo = {
538        location: {
539          country_name: navigator.language.split("-")[1] || "Unknown",
540        },
541      };
542
543      setLocalStorage(KEY_LOCATION_INFO, JSON.stringify(fallbackInfo));
544
545      return fallbackInfo;
546    }
547  }
548
549  function trackBotLinkClick(event, targetUrl) {
550    const sessionId = getSessionId();
551
552    if (!sessionId) {
553      console.warn('No session ID found for tracking link click');
554      return;
555    }
556
557    const TRACK_INTERACTION_ENDPOINT = `${scopicAI.API_ENDPOINT}track/interaction`;
558
559    const payload = {
560      session_id: sessionId,
561      target_url: targetUrl,
562      timestamp: Date.now() / 1000
563    };
564
565    fetch(TRACK_INTERACTION_ENDPOINT, {
566      method: 'POST',
567      headers: getAuthHeaders(),
568      body: JSON.stringify(payload),
569    })
570        .then(response => {
571          if (!response.ok) {
572            throw new Error('Error tracking link click');
573          }
574          return response.json();
575        })
576        .then(data => {
577          console.log('Link click tracked:', data);
578        })
579        .catch(error => {
580          console.error('Error tracking link click:', error);
581        });
582  }
583  window.trackBotLinkClick = trackBotLinkClick;
584
585  // END SEO METRICS HELPERS
586
587  // START USER ACTIONS HELPERS
588  function getActions() {
589    if (!userActionsData || !userActionsData.messages || !userActionsData.thresholds) {
590      return null;
591    }
592
593    const actions = [];
594    const { messages, thresholds } = userActionsData;
595
596    if (messages.on_page_linger) {
597      actions.push({
598        action: 'on_page_linger',
599        message: messages.on_page_linger,
600        trigger: thresholds.linger_time_seconds,
601        triggerType: 'time'
602      });
603    }
604
605    if (messages.on_page_navigation) {
606      actions.push({
607        action: 'on_page_navigation',
608        message: messages.on_page_navigation,
609        trigger: 'onStart',
610        triggerType: 'event'
611      });
612    }
613
614    return actions;
615  }
616
617  // END USER ACTIONS HELPERS
618
619  // START CHAT WORKFLOWS
620  function displaySessionClosedScreen() {
621    let reasonText = '';
622    if (sessionClosedReason === 'inactivity') {
623      reasonText = 'The chat was closed due to inactivity.';
624    } else if (sessionClosedReason === 'server') {
625      reasonText = 'The chat was closed by the server.';
626    }
627
628    chatbot_session_closed_reason.text(reasonText);
629    showOnScreen(chatbot_session_closed);
630    console.log('Session closed screen shown. Reason:', sessionClosedReason);
631  }
632
633  function openChat() {
634    if (!chatbot_bubble.hasClass("hidden")) hideFromScreen(chatbot_bubble);
635
636    if (sessionClosedReason) {
637      displaySessionClosedScreen();
638      return;
639    }
640
641    clearChatHistory();
642
643    if (!getSessionId()) {
644      showOnScreen(chatbot_intro_screen);
645    } else if (getSessionId()) {
646      renderChatHistory();
647      showOnScreen(chatroom);
648      chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
649    }
650  }
651
652
653  function isChatOpen() {
654    return !chatroom.hasClass("hidden") || !chatbot_intro_screen.hasClass("hidden");
655  }
656
657  function isUserInConversation() {
658    return chat_history_data && chat_history_data.length >= 2;
659  }
660
661  function initShowMessage(render = false) {
662    if (chat_history_data.length === 0 || !chat_history_data || chat_history_data.length === 1 ) {
663      removeLocalStorage(KEY_CHAT_HISTORY_STORAGE);
664      chat_history_data = []
665      saveMessageOnSession('', initMessage);
666      if (render){
667        clearChatHistory();
668        renderChatbotMessage(initMessage);
669      }
670    }
671  }
672
673  function updateActionMessage(newInitMessage) {
674    if (isChatOpen()) {
675      return;
676    }
677    initMessage = newInitMessage;
678    proactiveMessageActive = true;
679    initShowMessage()
680  }
681
682  function showOnScreen(element) {
683    element.removeClass("hidden");
684  }
685
686  function hideFromScreen(element) {
687    element.addClass("hidden");
688  }
689
690  function sendChatMessage(message) {
691    clearInactivityMessage();
692
693    showOnScreen(chatbot_typing);
694    chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
695    lastHeight = chat_thread_window[0].scrollHeight;
696    messageIndex++;
697
698    const currentMessageId = `msg-${messageIndex}`;
699    const time = getCurrentTime()
700    let streamingMessageElement = $(`
701    <div class="send-msg-wrapper received" style="display: none" id="${currentMessageId}">
702      <i><img src="${chatbot_icon_path}" alt=""></i>
703      <div class="send-content">
704        <p></p>
705      </div>
706      <span>${time}</span>
707    </div>`);
708
709    const RESPONSE_TIMEOUT = 60000;
710    let responseTimeout = setTimeout(() => {
711      hideFromScreen(chatbot_typing);
712      renderChatbotMessage("Sorry, I didn't quite get that. Can you ask your question again?");
713      enableChatInput();
714    }, RESPONSE_TIMEOUT);
715
716    const socket = new WebSocket(WEBSOCKET_ENDPOINT+"?api_key="+scopicAI.API_KEY);
717    let fullResponse = '';
718    let responseData = null;
719    let isSessionTerminated = false;
720
721
722    function hasIncompleteMarkdownLink(text) {
723
724      const idx = text.lastIndexOf("](");
725      if (idx === -1) return false;
726      const close = text.indexOf(")", idx + 2);
727      return close === -1;
728    }
729
730    let streamRenderTimer = null;
731    const STREAM_RECHECK_MS = 120;
732
733    function renderStreaming(text) {
734      let safeText = stripTrailingIncompleteMarkdownLink(text);
735
736      let out = addAnchorLinks(safeText);
737
738
739      out = formatBoldText(out);
740      out = convertToBullets(out);
741
742      $(`#${currentMessageId} .send-content p`).html(out);
743    }
744
745    function scheduleStreamingRender() {
746
747      clearTimeout(streamRenderTimer);
748
749      streamRenderTimer = setTimeout(() => {
750        renderStreaming(fullResponse)
751
752
753        if (hasIncompleteMarkdownLink(fullResponse)) scheduleStreamingRender()
754      }, STREAM_RECHECK_MS);
755    }
756    function stripTrailingIncompleteMarkdownLink(text) {
757      if (!text) return text;
758
759
760      text = text.replace(/\[([^\]]+)\]\([^\)]*$/, '$1');
761      text = text.replace(/\[([^\]]*)$/, '$1');
762
763      return text;
764    }
765
766    showOnScreen(chatbot_typing);
767    socket.onopen = function() {
768      chat_history_panel.append(streamingMessageElement);
769      console.log('WebSocket connection established');
770
771      let payload = {
772        session_id: getSessionId(),
773        message: message,
774        current_page_url: window.location.href,
775        pages_visited: JSON.parse(getLocalStorage(KEY_PAGES_VISITED)) || []
776      };
777      if (proactiveMessageActive){
778        proactiveMessageActive = false
779        payload ={
780          ...payload,
781          proactive_message: initMessage,
782        }
783      }
784      socket.send(JSON.stringify(payload));
785    };
786
787    socket.onmessage = function(event) {
788      try {
789        const data = JSON.parse(event.data);
790
791
792        if (data && data?.error) {
793          errorHandler();
794          return;
795        }
796
797        if (data &&  data?.type  && data.type === 'message_chunk') {
798          hideFromScreen(chatbot_typing);
799          $(`#${currentMessageId}`).show();
800
801          fullResponse += data.content;
802
803          renderStreaming(fullResponse);
804
805          if (hasIncompleteMarkdownLink(fullResponse)) scheduleStreamingRender();
806
807          const heightElement = ($(`#${currentMessageId}`).outerHeight(true) -71);
808
809          if (heightElement > 200 ){
810            if (chat_thread_window[0].scrollHeight > lastHeight + 200) {
811              chat_thread_window.scrollTop(lastHeight - 200);
812            }else{
813              chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
814            }
815          }else{
816            chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
817          }
818
819
820        }
821
822        else if (data &&  data?.type  && data.type === 'message_complete') {
823          clearTimeout(streamRenderTimer)
824          hideFromScreen(chatbot_typing);
825          $(`#${currentMessageId}`).show();
826          try {
827            clearTimeout(responseTimeout);
828            responseData = JSON.parse(data.content);
829
830            if (responseData.response) {
831              fullResponse = responseData.response;
832
833              let formattedResponse = addAnchorLinks(fullResponse);
834              formattedResponse = formatBoldText(formattedResponse);
835              formattedResponse = convertToBullets(formattedResponse);
836
837              $(`#${currentMessageId} .send-content p`).html(formattedResponse);
838            }
839
840            if (responseData.response) {
841              saveMessageOnSession(message, responseData.response);
842            }
843
844            if (responseData.cta && responseData.cta.text && responseData.cta.url && (responseData.cta.presented === true)) {
845              renderCTA(responseData.cta);
846            }
847
848            const heightElement = ($(`#${currentMessageId}`).outerHeight(true) - 71);
849
850
851            if (heightElement > 200 ){
852              if (chat_thread_window[0].scrollHeight > lastHeight + 200) {
853                chat_thread_window.scrollTop(lastHeight - 200);
854              }else{
855                chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
856              }
857            }else{
858              chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
859            }
860            if (responseData.terminated === true) {
861              handleSessionTermination();
862            } else {
863              showInactivityMessage();
864            }
865
866            socket.close();
867          } catch (parseError) {
868            console.error('Error parsing message_complete content:', parseError);
869          }
870        }
871
872      } catch (error) {
873        console.error('Error processing WebSocket message:', error);
874      }
875    };
876
877    socket.onerror = function(error) {
878      console.error('WebSocket error:', error);
879      errorHandler();
880    };
881
882    function errorHandler() {
883      hideFromScreen(chatbot_typing);
884
885      renderChatbotMessage(
886          "Sorry, an error has occurred. Please try again later."
887      );
888
889      $(`#${currentMessageId}`).remove();
890
891      enableChatInput();
892    }
893
894    socket.onclose = function() {
895      console.log('WebSocket connection closed');
896      lastHeight = chat_thread_window[0].scrollHeight;
897      hideFromScreen(chatbot_typing);
898      if (!isSessionTerminated) {
899        enableChatInput();
900      }
901    };
902  }
903
904  function renderUserMessage(user_message) {
905    if (user_message === "") {
906      return;
907    }
908    const time = getCurrentTime()
909    chat_history_panel.append(`
910            <div class="send-msg-wrapper">
911                <div class="send-content">
912                    <p>${user_message}</p>
913                </div>
914                <span>${time}</span>
915            </div>
916        `);
917  }
918
919  function renderChatbotMessage(bot_reply) {
920    if (bot_reply === "") {
921      return;
922    }
923    const time = getCurrentTime()
924    formatted_reply = addAnchorLinks(bot_reply);
925    formatted_reply = formatBoldText(formatted_reply);
926    formatted_reply = convertToBullets(formatted_reply);
927
928    chat_history_panel.append(`
929            <div class="send-msg-wrapper received">
930                <i><img src="${chatbot_icon_path}" alt=""></i>
931                <div class="send-content">
932                    <p>${formatted_reply}</p>
933                </div>
934                <span>${time}</span>
935            </div>
936        `);
937  }
938
939  function showInactivityMessage() {
940    clearInactivityMessage();
941
942    intervalInactiveTime = setTimeout(function () {
943      const message = "Can I help you with anything else?";
944      renderChatbotMessage(message);
945      chat_thread_window.scrollTop(chat_thread_window[0].scrollHeight);
946
947      sessionTimeoutTimer = setTimeout(function() {
948        removeSessionId();
949        removeLocalStorage(KEY_CHAT_HISTORY_STORAGE);
950        chat_history_data = [];
951        clearChatHistory();
952        showSessionClosedMessage('inactivity');
953      }, 900000); // 15 minutes
954    }, 900000); // 15 minutes
955  }
956
957  function showSessionClosedMessage(reason) {
958    sessionClosedReason = reason;
959    hideFromScreen(chatroom);
960    hideFromScreen(chatbot_typing);
961    hideFromScreen(chatbot_bubble);
962    hideFromScreen(chatbot_intro_screen);
963
964    console.log('Session closed. Reason:', reason);
965
966    if (isChatOpen()) {
967      displaySessionClosedScreen();
968    }
969  }
970
971  function clearInactivityMessage() {
972    if (intervalInactiveTime) {
973      clearTimeout(intervalInactiveTime);
974      intervalInactiveTime = null;
975    }
976
977    if (sessionTimeoutTimer) {
978      clearTimeout(sessionTimeoutTimer);
979      sessionTimeoutTimer = null;
980    }
981  }
982
983  function renderChatHistory() {
984    chat_history_data.forEach((question_answer) => {
985      renderUserMessage(question_answer[0]);
986      renderChatbotMessage(question_answer[1]);
987    });
988  }
989
990  function clearChatHistory() {
991    chat_history_panel.empty();
992  }
993
994  function saveMessageOnSession(question, message) {
995    chat_history_data.push([question, message]);
996    setLocalStorage(
997        KEY_CHAT_HISTORY_STORAGE,
998        JSON.stringify(chat_history_data)
999    );
1000  }
1001
1002  function handleSessionTermination() {
1003    isSessionTerminated = true;
1004    console.log('Session terminated by server');
1005    clearInactivityMessage();
1006    disableChatInput();
1007    removeSessionId();
1008    removeLocalStorage(KEY_CHAT_HISTORY_STORAGE);
1009    chat_history_data = [];
1010
1011    sessionStorage.removeItem(KEY_LINGER_START);
1012    sessionStorage.removeItem(KEY_LINGER_TRIGGERED);
1013
1014    setTimeout(function() {
1015      clearChatHistory();
1016      showSessionClosedMessage('server');
1017    }, 5000);
1018  }
1019
1020  function renderCTA(cta) {
1021    if (!cta || !cta.text || !cta.url) return;
1022    const time = getCurrentTime()
1023
1024    let ctaHtml = `
1025      <div class="send-msg-wrapper received cta-wrapper">
1026        <i><img src="${chatbot_icon_path}" alt=""></i>
1027        <div class="send-content">
1028    `;
1029
1030    if (cta.message) {
1031      ctaHtml += `<p>${cta.message}</p>`;
1032    }
1033
1034    ctaHtml += `
1035          <a href="${cta.url}" target="_blank" class="cta-button" onclick="trackBotLinkClick(event, '${cta.url}')">${
1036        cta.text
1037    }</a>
1038        </div>
1039        <span>${time}</span>
1040      </div>
1041    `;
1042
1043    chat_history_panel.append(ctaHtml);
1044  }
1045
1046  function disableChatInput() {
1047    chat_input_form.prop("disabled", true);
1048    $("#scopic_chatbot_send_btn").prop("disabled", true);
1049    $("#scopic_chatbot_send_btn img.active").css("display", "none");
1050    $("#scopic_chatbot_send_btn img.disabled").css("display", "block");
1051  }
1052
1053  function enableChatInput() {
1054    chat_input_form.prop("disabled", false);
1055    $("#scopic_chatbot_send_btn").prop("disabled", false);
1056    $("#scopic_chatbot_send_btn img.active").css("display", "block");
1057    $("#scopic_chatbot_send_btn img.disabled").css("display", "none");
1058  }
1059
1060  // END CHAT WORKFLOWS
1061
1062  // START MESSAGE FORMATTING HELPERS
1063  function convertToBullets(response_msg) {
1064    return response_msg
1065        .replace(/\n/g, "<br>")
1066        .replace(/(\d+\.)\s+/g, "</li><li>$1 ");
1067  }
1068
1069  function getCurrentTime() {
1070    const currentTime = new Date();
1071    const hours = currentTime.getHours();
1072    const minutes = currentTime.getMinutes();
1073    const ampm = hours >= 12 ? "PM" : "AM";
1074    const hours12 = hours % 12 || 12;
1075    const formattedMinutes = (minutes < 10 ? "0" : "") + minutes;
1076    return `${hours12}:${formattedMinutes} ${ampm}`;
1077  }
1078
1079  function appendUtm(url) {
1080    const utm = new URL(url, window.location.href);
1081
1082    if (!utm.searchParams.has("utm_source")) {
1083      utm.searchParams.set('utm_source', 'scopic-chatbot')
1084    }
1085
1086    return utm.toString();
1087  }
1088
1089  function addAnchorLinks(response_msg) {
1090    const markdownRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
1091    let result = response_msg.replace(markdownRegex, function(match, text, url) {
1092      const finalUrl = appendUtm(url);
1093      const escapedUrl = finalUrl.replace(/'/g, "\\'");
1094      return `<a href="${finalUrl}" target="_blank" rel="noopener noreferrer" onmousedown="trackBotLinkClick(event, '${escapedUrl}')">${text}</a>`;
1095    });
1096
1097    const parts = result.split(/(<a[^>]*>.*?<\/a>)/g);
1098    result = parts.map(part => {
1099      if (part.startsWith('<a ')) {
1100        return part;
1101      }
1102      const urlRegex = /(https?:\/\/[^\s<]+)/g;
1103      return part.replace(urlRegex, function(url) {
1104        const finalUrl = appendUtm(url);
1105        const escapedUrl = finalUrl.replace(/'/g, "\\'");
1106        return `<a href="${finalUrl}" target="_blank" rel="noopener noreferrer" onmousedown="trackBotLinkClick(event, '${escapedUrl}')">${finalUrl}</a>`;
1107      });
1108    }).join('');
1109
1110    return result;
1111  }
1112
1113  function formatBoldText(response_msg) {
1114    const regex = /\*\*(.*?)\*\*/g;
1115    return response_msg.replace(regex, "<strong>$1</strong>");
1116  }
1117
1118  function formatTimeSpent(seconds) {
1119    if (seconds < 60) {
1120      return `${seconds} secs`;
1121    } else if (seconds < 3600) {
1122      const minutes = Math.floor(seconds / 60);
1123      const remainingSeconds = seconds % 60;
1124      return remainingSeconds > 0 ? `${minutes} mins ${remainingSeconds} secs` : `${minutes} mins`;
1125    } else {
1126      const hours = Math.floor(seconds / 3600);
1127      const minutes = Math.floor((seconds % 3600) / 60);
1128      return minutes > 0 ? `${hours} hours ${minutes} mins` : `${hours} hours`;
1129    }
1130  }
1131
1132  // END MESSAGE FORMATTING HELPERS
1133
1134  // START BUBBLE ANIMATION CONTROLS
1135
1136  function initBundle() {
1137    console.log('initBundle called');
1138
1139  }
1140
1141  function showInitialWelcomeMessage(welcomeMessage  = 'Hi there! Can I help you?') {
1142    const hasActiveConversation = chat_history_data && chat_history_data.length >= 2;
1143    const sessionId = getSessionId();
1144
1145    const welcomeShownKey = sessionId ? `${KEY_WELCOME_SHOWN}_${sessionId}` : KEY_WELCOME_SHOWN;
1146    const alreadyShown = sessionStorage.getItem(welcomeShownKey) === 'true';
1147
1148    if (alreadyShown) {
1149      return;
1150    }
1151
1152    if (!hasActiveConversation) {
1153      const delayMs = bubbleTiming.initialDelayMs;
1154
1155      setTimeout(() => {
1156        if (isChatOpen()) return;
1157        bubbleShow(true, welcomeMessage);
1158
1159        if (sessionId) {
1160          sessionStorage.setItem(welcomeShownKey, 'true');
1161        }
1162      }, delayMs);
1163    }else {
1164      console.log('[scopic] Skipping welcome message — user already has session');
1165    }
1166  }
1167
1168
1169  function clearBubbleAnimationTimers() {
1170    bubbleRunId++;
1171    bubbleTimeouts.forEach(clearTimeout);
1172    bubbleTimeouts = [];
1173
1174    if (bubbleLoopInternal) {
1175      clearInterval(bubbleLoopInternal);
1176      bubbleLoopInternal = null;
1177    }
1178
1179    chatbot_bubble.off("animationend webkitAnimationEnd");
1180  }
1181
1182  function isBubbleHidden () {
1183    return chatbot_bubble.hasClass("hidden");
1184  }
1185
1186  function bubbleShow(force = false, message= '') {
1187    if (isChatOpen()) {
1188      return;
1189    }
1190
1191    if (proactiveMessageCount >= MAX_PROACTIVE_MESSAGES_PER_PAGE) {
1192      return;
1193    }
1194
1195    if (force) {
1196      bubbleHide(true)
1197    }else{
1198      if (getSessionId()) return;
1199      if (bubbleState === "showing" || bubbleState === "visible") return;
1200    }
1201
1202    bubbleState = "showing";
1203
1204
1205    chatbot_bubble.off("animationend webkitAnimationEnd");
1206
1207    showOnScreen(chatbot_bubble);
1208
1209    try {
1210      const audio = new Audio(chatbot_audio_path);
1211      audio.play().catch(err => console.log("Audio muted (page click missing)"));
1212    } catch (e) { console.error(e); }
1213
1214    chatbot_bubble.removeClass("bubble-in bubble-out");
1215    chatbot_bubble[0].offsetHeight;
1216    if (message){
1217      chatbot_bubble.find('p').text(message);
1218    }
1219
1220    requestAnimationFrame(() => {
1221      chatbot_bubble.addClass("bubble-in");
1222      bubbleState = "visible";
1223    });
1224    proactiveMessageCount++;
1225
1226    if (bubbleAutoHideTimer) {
1227      clearTimeout(bubbleAutoHideTimer);
1228    }
1229    bubbleAutoHideTimer = setTimeout(() => {
1230      bubbleHide();
1231    }, bubbleTiming.autoHideMs);
1232  }
1233
1234  function bubbleHide(force = false) {
1235    if (bubbleAutoHideTimer) {
1236      clearTimeout(bubbleAutoHideTimer);
1237      bubbleAutoHideTimer = null;
1238    }
1239
1240    if (force) {
1241      chatbot_bubble.off("animationend webkitAnimationEnd");
1242      chatbot_bubble.removeClass("bubble-in bubble-out");
1243      hideFromScreen(chatbot_bubble);
1244      bubbleState = "hidden";
1245      return;
1246    }
1247
1248    if (bubbleState === "hidden" || bubbleState === "hiding" || isBubbleHidden()) return;
1249
1250    bubbleState = "hiding";
1251    chatbot_bubble.off("animationend webkitAnimationEnd");
1252
1253    chatbot_bubble.removeClass("bubble-in").addClass("bubble-out");
1254
1255    chatbot_bubble.one("animationend webkitAnimationEnd", () => {
1256      chatbot_bubble.removeClass("bubble-out");
1257      hideFromScreen(chatbot_bubble);
1258      bubbleState = "hidden";
1259    });
1260  }
1261
1262  function scheduleBubbleSequence() {
1263    clearBubbleAnimationTimers();
1264  }
1265
1266  // END BUBBLE ANIMATION CONTROLS
1267
1268  // START SESSION STORAGE HELPERS
1269
1270  // Cookie expiration in days (configurable)
1271  const SESSION_COOKIE_EXPIRATION_DAYS = 1;
1272
1273  function setSessionId(sessionId) {
1274    try {
1275      const expirationDate = new Date();
1276      expirationDate.setDate(expirationDate.getDate() + SESSION_COOKIE_EXPIRATION_DAYS);
1277      const expires = "expires=" + expirationDate.toUTCString();
1278      document.cookie = KEY_NAME_STORAGE + "=" + sessionId + ";" + expires + ";path=/";
1279    } catch (error) {
1280      console.error('Error setting session cookie:', error);
1281    }
1282  }
1283
1284  function getSessionId() {
1285    try {
1286      const name = KEY_NAME_STORAGE + "=";
1287      const decodedCookie = decodeURIComponent(document.cookie);
1288      const cookieArray = decodedCookie.split(';');
1289
1290      for (let i = 0; i < cookieArray.length; i++) {
1291        let cookie = cookieArray[i].trim();
1292        if (cookie.indexOf(name) === 0) {
1293          return cookie.substring(name.length, cookie.length);
1294        }
1295      }
1296      return null;
1297    } catch (error) {
1298      console.error('Error getting session cookie:', error);
1299      return null;
1300    }
1301  }
1302
1303  function removeSessionId() {
1304    try {
1305      document.cookie = KEY_NAME_STORAGE + "=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;";
1306    } catch (error) {
1307      console.error('Error removing session cookie:', error);
1308    }
1309  }
1310
1311  function setLocalStorage(key, value) {
1312    try {
1313      localStorage.setItem(key, value);
1314    } catch (error) {
1315      console.error('Error setting localStorage:', error);
1316    }
1317  }
1318
1319  function getLocalStorage(key) {
1320    try {
1321      return localStorage.getItem(key);
1322    } catch (error) {
1323      console.error('Error getting localStorage:', error);
1324      return null;
1325    }
1326  }
1327
1328  function removeLocalStorage(key) {
1329    try {
1330      localStorage.removeItem(key);
1331    } catch (error) {
1332      console.error('Error removing localStorage:', error);
1333    }
1334  }
1335
1336
1337  if (typeof window !== "undefined") {
1338    window.scopicAIReinit = function () {
1339      clearInactivityMessage();
1340      clearBubbleAnimationTimers();
1341      bubbleHide(true);
1342
1343      userActionsData = null;
1344      messageIndex = 0;
1345      proactiveMessageCount = 0;
1346      lastHeight = chat_thread_window[0]?.scrollHeight || 0;
1347      return Promise.resolve()
1348          .then(() => init())
1349          .then(() => console.log("[ScopicAI] scopicAIReinit() DONE"))
1350          .catch((e) => console.error("[ScopicAI] scopicAIReinit() ERROR", e));
1351
1352
1353    };
1354  }
1355  //END SESSION STORAGE HELPERS
1356});

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.