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.