1/* 2 * Modern JavaScript file for OpenAI chat inclusion. 3 */ 4(function() { 5 6 // Avoiding double inclusion issues. 7 if (window.ainiro) { 8 return; 9 } 10 11 // Creating our shadow DOM element. 12 const host = document.createElement('ainiro-chatbot'); 13 document.body.appendChild(host); 14 15 class AiniroChatbotEl extends HTMLElement { 16 constructor() { 17 super(); 18 this.root = this.attachShadow({ mode: 'open' }); 19 } 20 } 21 customElements.define('ainiro-chatbot', AiniroChatbotEl); 22 23 // Global handler to make sure we delete session list popup if anything is clicked in document. 24 document.addEventListener("click", function () { 25 if (window.ainiro && window.ainiro.shadow) { 26 const inShadow = window.ainiro.shadow.querySelector('.ainiro_sessions_list'); 27 if (inShadow && inShadow.parentNode) { 28 inShadow.parentNode.removeChild(inShadow); 29 } 30 } 31 }); 32 33 // Creating our primary namespace. 34 window.ainiro = { 35 36 // Shadow helpers. 37 shadow: host.shadowRoot || host.attachShadow({ mode: 'open' }), 38 $: function(sel) { return ainiro.shadow.querySelector(sel); }, 39 $$: function(sel) { return Array.from(ainiro.shadow.querySelectorAll(sel)); }, 40 $id: function(id) { return ainiro.shadow.getElementById(id); }, 41 42 /* 43 * Settings for chatbot. 44 * 45 * These settings are dynamically substituded as the script is requested from the server. 46 */ 47 ainiro_settings: { 48 url: 'https://ainiro.io', 49 button: 'Our AI Chatbot', 50 header: `Ask me about AI Chatbots`, 51 greeting: ``, 52 rtl: false, 53 references: false, 54 type: 'ainiro', 55 watermark: `Powered by <a href="https://ainiro.io" target="_blank">AINIRO.IO</a>`, 56 version: 'qwexxyz123', 57 recaptcha: '', 58 color: '#fefefe', 59 start: '#FF9300', 60 end: '#E84100', 61 link: '#fe8464', 62 theme: 'modern-glass', 63 placeholder: 'Ask me anything ...', 64 position: 'right', 65 clearButton: true, 66 copyButton: true, 67 follow_up: true, 68 new_tab: false, 69 code: false, 70 animation: 'ainiro_blink', 71 popup: 'Hi! I am Frank, I was made by AINIRO ð ', 72 extra: '', 73 hidden: false, 74 sticky: false, 75 attachments: false, 76 history: false, 77 meta: '' 78 }, 79 80 // References buffer for storing references during invocation. 81 references_list: [], 82 83 /* 84 * SignalR socket connection to backend. 85 */ 86 socket: null, 87 88 /* 89 * If true, we need to initialize session by running through questionnaires. 90 */ 91 execQuestionnaires: true, 92 93 /* 94 * If true, we need to initialise session with greeting, and conversation starters. 95 */ 96 initSession: true, 97 98 /* 99 * Callback function to be invoked when server is finished answering question. 100 */ 101 onFinished: null, 102 103 /* 104 * User ID. 105 */ 106 userId: null, 107 108 /* 109 * True if user has asked the chatbot questions already. 110 */ 111 hasSessionItems: false, 112 113 /* 114 * Temporary response from server. 115 */ 116 responseBuffer: null, 117 118 /* 119 * Initializing chatbot. 120 * 121 * Includes required JavaScript and CSS files, for then to create the UI for the chatbot button 122 * and the chatbot window. 123 */ 124 init: function() { 125 126 /* 127 * Figuring out theme to use. 128 * 129 * Notice, this little trickery allows us to create a global callback override 130 * that returns the theme we should use, allowing for theme selectors, etc. 131 */ 132 const theme = window.getAiniroChatbotCssFile ? 133 (window.getAiniroChatbotCssFile() ?? this.ainiro_settings.theme) : 134 this.ainiro_settings.theme; 135 136 /* 137 * Including CSS files required for chatbot. 138 * (Load inside shadow root to isolate styles.) 139 * 140 * Notice, our little injectShadowCSS function 141 */ 142 let styleUrl = `${this.ainiro_settings.url}/magic/system/openai/include-style.css?`; 143 styleUrl += `file=${encodeURIComponent(theme)}&`; 144 styleUrl += `position=${encodeURIComponent(this.ainiro_settings.position)}&`; 145 styleUrl += `color=${encodeURIComponent(this.ainiro_settings.color)}&`; 146 styleUrl += `link=${encodeURIComponent(this.ainiro_settings.link)}&`; 147 styleUrl += `start=${encodeURIComponent(this.ainiro_settings.start)}&`; 148 styleUrl += `end=${encodeURIComponent(this.ainiro_settings.end)}&`; 149 styleUrl += `v=${encodeURIComponent(this.ainiro_settings.version)}`; 150 this.injectShadowCSS(this.shadow, styleUrl); 151 152 // Ensuring we're using correct icons for buttons. 153 const style = document.createElement('style'); 154 style.textContent = ` 155 @font-face { 156 font-family: 'AiniroIcoFont'; 157 font-style: normal; 158 font-weight: 400; 159 src: url('https://ainiro.io/assets/css/fonts/icofont.woff2?v=22.0.0') format('woff2'), url('https://ainiro.io/assets/css/fonts/icofont.woff?v=22.0.0') format('woff'); 160 font-display: swap; 161 }`; 162 document.head.appendChild(style); 163 164 // Creating chatbot trigger button that opens chatbot. 165 const chatButton = document.createElement('button'); 166 chatButton.className = 'ainiro ainiro_' + this.ainiro_settings.position; 167 if (this.ainiro_settings.animation !== '') { 168 chatButton.classList.add(this.ainiro_settings.animation); 169 } 170 if (this.ainiro_settings.button === '') { 171 chatButton.innerHTML = '<i class="ainiro-icofont ainiro-icofont-chat"></i>'; 172 } else { 173 chatButton.innerHTML = this.ainiro_settings.button; 174 } 175 chatButton.style.display = 'none'; 176 chatButton.id = 'ainiro_chat_btn'; 177 chatButton.addEventListener('click', () => this.show()); 178 if (this.ainiro_settings.rtl) { 179 180 // Chatbot button should be rendered RTL (Farsi, Hebrew, Arabic, etc). 181 chatButton.style.direction = 'rtl'; 182 } 183 184 // Checking if chatbot should be invisible 185 if (this.ainiro_settings.hidden === true) { 186 chatButton.classList.add('ainiro_hidden'); 187 } 188 189 // Checking if we need to create a popup for button. 190 if (this.ainiro_settings.popup && this.ainiro_settings.popup !== '') { 191 chatButton.classList.add('ainiro_popup'); 192 chatButton.style.setProperty('--ainiro-dynamic-content', '"' + this.ainiro_settings.popup + '"'); 193 } 194 195 // Appending button to shadow. 196 this.shadow.appendChild(chatButton); 197 198 /* 199 * Creating chatbot window. 200 */ 201 const chatWindow = document.createElement('div'); 202 chatWindow.className = 'ainiro ainiro_' + this.ainiro_settings.position; 203 chatWindow.id = 'ainiro_chat_wnd'; 204 chatWindow.style.display = 'none'; 205 if (this.ainiro_settings.rtl) { 206 chatWindow.style.direction = 'rtl'; 207 } 208 this.shadow.appendChild(chatWindow); 209 210 // Creating top toolbar. 211 const toolbar = document.createElement('div'); 212 toolbar.className = 'ainiro_toolbar'; 213 chatWindow.appendChild(toolbar); 214 215 // Creating window header. 216 const header = document.createElement('div'); 217 header.className = 'ainiro_header'; 218 header.innerHTML = this.ainiro_settings.header; 219 toolbar.appendChild(header); 220 221 // Checking if we should render a "clear session" button. 222 if (this.ainiro_settings.clearButton) { 223 224 // Creating "clear session" button. 225 const clearChatButton = document.createElement('button'); 226 clearChatButton.innerHTML = '<i class="ainiro-icofont-duotone ainiro-icofont-purge"></i>'; 227 clearChatButton.className = 'ainiro_action ainiro_new_chat'; 228 clearChatButton.addEventListener('click', () => this.clear()); 229 clearChatButton.style.color = this.ainiro_settings.color; 230 toolbar.appendChild(clearChatButton); 231 } 232 233 // Creating maximize chat window button. 234 const maximizeButton = document.createElement('button'); 235 maximizeButton.innerHTML = '<i class="ainiro-icofont ainiro-icofont-duotone ainiro-icofont-expand-full"></i><i class="ainiro-icofont ainiro-icofont-duotone ainiro-icofont-contract-alt"></i>'; 236 maximizeButton.className = 'ainiro_action ainiro_maximize'; 237 maximizeButton.addEventListener('click', () => this.maximize()); 238 toolbar.appendChild(maximizeButton); 239 240 // Checking if we should store sessions. 241 if (this.ainiro_settings.history === true) { 242 const sessionButton = document.createElement('button'); 243 sessionButton.innerHTML = '<i class="ainiro-icofont ainiro-icofont-duotone ainiro-icofont-list"></i>'; 244 sessionButton.className = 'ainiro_action ainiro_sessions'; 245 sessionButton.addEventListener('click', () => this.showSession()); 246 toolbar.appendChild(sessionButton); 247 } 248 249 // Creating close chat window button. 250 const closeButton = document.createElement('button'); 251 closeButton.innerHTML = '<i class="ainiro-icofont ainiro-icofont-close"></i>'; 252 closeButton.className = 'ainiro_action ainiro_close'; 253 closeButton.addEventListener('click', () => this.hide()); 254 toolbar.appendChild(closeButton); 255 256 // Creating chat surface. 257 const chatSurface = document.createElement('div'); 258 chatSurface.className = 'ainiro_chat_surface'; 259 chatSurface.id = 'ainiro_chat_surf'; 260 chatWindow.appendChild(chatSurface); 261 262 // Checking if we should display watermark. 263 if (this.ainiro_settings.watermark && this.ainiro_settings.watermark !== '') { 264 265 // Displaying watermark. 266 const water = document.createElement('div'); 267 water.className = 'ainiro_watermark'; 268 water.innerHTML = this.ainiro_settings.watermark; 269 chatSurface.appendChild(water); 270 } 271 272 // Creating attachment buttons before file input such that we can access it in callback function. 273 const attBtn = document.createElement('button'); 274 const attRem = document.createElement('button'); 275 276 // Creating chatbot form including input textbox and submit button. 277 const chatForm = document.createElement('form'); 278 chatForm.id = 'ainiro_form'; 279 chatForm.className = 'ainiro_form'; 280 chatForm.addEventListener('submit', (e) => { 281 e.preventDefault(); 282 attBtn.classList.remove('ainiro_hidden'); 283 attRem.classList.add('ainiro_hidden'); 284 this.submit(); 285 }); 286 287 // Prompt textbox. 288 const textBox = document.createElement('input'); 289 textBox.id = 'ainiro_txt'; 290 textBox.className = 'ainiro_query'; 291 textBox.autocomplete = 'off'; 292 textBox.type = 'text'; 293 textBox.placeholder = this.ainiro_settings.placeholder; 294 chatForm.appendChild(textBox); 295 296 // Attachments button. 297 if (this.ainiro_settings.attachments === true) { 298 299 // Show filename label. 300 const lbl = document.createElement('div'); 301 lbl.className = 'ainiro_filename'; 302 lbl.id = 'ainiro_filename_label'; 303 chatWindow.appendChild(lbl); 304 305 // File upload control. 306 const fileInp = document.createElement('input'); 307 fileInp.id = 'ainiro_upload'; 308 fileInp.type = 'file'; 309 fileInp.style.display = 'none'; 310 fileInp.accept = '.csv,.xml,.yaml,.yml,.json,.txt,.md,.html,.htm,.css,.js,.py,.rb,.ts,.scss,.sql,.pdf,.docx,.png,.jpeg,.jpg,.webp,.gif'; 311 fileInp.addEventListener('change', (ev) => { 312 const files = Array.from(ev.target.files); 313 this.rawFiles = files; 314 attBtn.classList.add('ainiro_hidden'); 315 attRem.classList.remove('ainiro_hidden'); 316 lbl.innerHTML = ev.target.files[0].name; 317 }); 318 chatForm.appendChild(fileInp); 319 320 // Add attachments button. 321 attBtn.id = 'ainiro_attachments'; 322 attBtn.className = 'ainiro_attachments'; 323 attBtn.type = 'button'; 324 attBtn.addEventListener('click', () => { 325 fileInp.click(); 326 }); 327 attBtn.innerHTML = '<i class="ainiro-icofont-upload ainiro-icofont-lg"></i>'; 328 chatForm.appendChild(attBtn); 329 330 // Remove attachments button. 331 attRem.id = 'ainiro_delete_attachments'; 332 attRem.className = 'ainiro_attachments ainiro_hidden'; 333 attRem.type = 'button'; 334 attRem.addEventListener('click', () => { 335 this.rawFiles = null; 336 attBtn.classList.remove('ainiro_hidden'); 337 attRem.classList.add('ainiro_hidden'); 338 fileInp.value = null; 339 340 // Removing text of upload file button. 341 const l2 = this.shadow.getElementById('ainiro_filename_label'); 342 if (l2) { 343 l2.innerHTML = ''; 344 } 345 }); 346 attRem.innerHTML = '<i class="ainiro-icofont-duotone ainiro-icofont-purge ainiro-icofont-lg"></i>'; 347 chatForm.appendChild(attRem); 348 } 349 350 // Submit prompt button. 351 const submitButton = document.createElement('button'); 352 submitButton.id = 'ainiro_send'; 353 submitButton.className = 'ainiro_send'; 354 submitButton.type = 'submit'; 355 submitButton.innerHTML = '<i class="ainiro-icofont-location-arrow ainiro-icofont-lg"></i>'; 356 chatForm.appendChild(submitButton); 357 358 // Adding actual form to DOM. 359 chatWindow.appendChild(chatForm); 360 361 /* 362 * Checking if we've got something in our session storage 363 */ 364 const sessionItems = sessionStorage.getItem('ainiro_chatbot.session'); 365 if (sessionItems && sessionItems !== '') { 366 367 // We've got an existing session and user have already asked at least one question. 368 chatSurface.innerHTML = sessionItems; 369 this.runScriptsIn(this.shadow.getElementById('ainiro_chat_surf')); 370 371 // This prevents questionnaires from being shown. 372 this.execQuestionnaires = false;
373 374 // This prevents the watermark, greeting, and conversation starters from being shown. 375 this.initSession = false; 376 377 // Making sure surface is scrolled to the bottom when opened. 378 this.hasSessionItems = true; 379 } 380 381 // Making sure we close chatbot with ESC. 382 textBox.addEventListener('keydown', (e) => { 383 if (e.key === 'Escape') { 384 385 // User clicked escape key on keyboard while prompt textbox had focus. 386 this.hide(); 387 } 388 }); 389 390 // Checking if we already have a user ID, and if so, reusing it. 391 const userId = localStorage.getItem('ainiroUserId'); 392 if (userId) { 393 394 // Using existing user ID. 395 this.userId = userId; 396 397 } else { 398 399 // Creating a new user ID. 400 this.userId = 'u_' + Date.now() + Math.random(); 401 localStorage.setItem('ainiroUserId', this.userId); 402 } 403 404 // Creating a unique session. 405 this.session = 'c_' + Date.now() + Math.random(); 406 407 setTimeout(() => { 408 409 // Checking if we've got a query parameter that should trigger the chatbot immediately. 410 const urlParams = new URLSearchParams(window.location.search); 411 const initialPrompt = urlParams.get('ainiro_prompt'); 412 if (initialPrompt && initialPrompt !== '') { 413 ainiro_faq_question(null, initialPrompt); 414 } 415 }, 1); 416
417 // Checking if we should show the chatbot by default. 418 if (this.ainiro_settings.sticky === true) { 419 const ses = sessionStorage.getItem('ainiro_state'); 420 if (ses === 'open') { 421 this.show(); 422 } 423 } else { 424 sessionStorage.setItem('ainiro_state', 'closed'); 425 } 426 }, 427 428 /* 429 * Deletes DOM elements associated with chatbot, and cleans up DOM 430 */ 431 destroy: function() { 432 433 const wnd = this.shadow.getElementById('ainiro_chat_wnd'); 434 wnd.parentElement.removeChild(wnd); 435 const btn = this.shadow.getElementById('ainiro_chat_btn'); 436 btn?.parentElement.removeChild(btn); 437 sessionStorage.setItem('ainiro_state', 'closed'); 438 }, 439 440 /* 441 * Correctly injects @import statements from original CSS into root, and the rest of the CSS into our shadow DOM. 442 * This allows us to create CSS files importing fonts for instance. 443 */ 444 injectShadowCSS: async function(shadowRoot, styleUrl) { 445 446 // Fetching CSS file. 447 const res = await fetch(styleUrl, { mode: 'cors' }); 448 let cssText = await res.text(); 449 450 // Finding @import statements 451 const importRegex = /@import\s+url\(["']?([^"')]+)["']?\)[^;]*;/g; 452 453 // Adding all @import statements to head of document. 454 let match; 455 while ((match = importRegex.exec(cssText)) !== null) { 456 const importUrl = match[1]; 457 const link = document.createElement('link'); 458 link.rel = 'stylesheet'; 459 link.href = importUrl; 460 document.head.appendChild(link); 461 } 462 463 // Replacing all @import statements with empty text. 464 cssText = cssText.replace(importRegex, ''); 465 466 // Adding style as inline style into DOM. 467 const style = document.createElement('style'); 468 style.textContent = cssText; 469 shadowRoot.appendChild(style); 470 }, 471 472 /* 473 * Shows the chat window. 474 */ 475 show: function(onAfter = null) { 476 477 // Hiding chat button. 478 const btn = this.shadow.getElementById('ainiro_chat_btn'); 479 if (btn) { 480 btn.classList.add('ainiro_hide'); 481 btn.classList.add('ainiro_shown'); 482 } 483 484 this.includeResources(() => { 485 486 // Now we can safely invoke our real show function. 487 this._show(() => { 488 if (onAfter) { 489 onAfter(); 490 } 491 492 // Disabling button to avoid race conditions. 493 const sb = this.shadow.getElementById('ainiro_send'); 494 sb.disabled = false; 495 496 // Storing the fact the chatbot is open into session, in case we've got a "sticky" chatbot. 497 sessionStorage.setItem('ainiro_state', 'open'); 498 }); 499 }); 500 }, 501 502 /* 503 * Includes JS libraries. 504 */ 505 includeResources: function(onAfter) { 506 507 // Invoked after Marked has been downloaded. 508 const afterMarked = () => { 509 510 // Checking if we're supposed to open up hyperlinks in new tabs. 511 if (this.ainiro_settings.new_tab) { 512 513 // We're opening up hyperlinks in different tabs. 514 const renderer = { 515 link: function(href, title, text) { 516 if (text) { 517 if (title) { 518 return '<a target="_blank" href="'+ href +'" title="' + title + '">
518' + text + '</a>'; 519 } 520 return '<a target="_blank" href="'+ href +'">' + text + '</a>'; 521 } 522 return '<a target="_blank" href="'+ href +'">' + href + '</a>'; 523 } 524 }; 525 marked.use({ 526 renderer, 527 tokenizer: { 528 html(src) { 529 // 1) Verify the *opening* tag is a div whose class contains hljs_ignore 530 const openTagRe = /^<div[^>]*\bclass=(["'])(?:(?!\1).)*\bhljs_ignore\b(?:(?!\1).)*\1[^>]*>/i; 531 532 const open = src.match(openTagRe); 533 if (!open) return false; 534 535 let depth = 1; 536 let idx = open[0].length; 537 538 const divTagRe = /<\/?div\b[^>]*>/gi; 539 divTagRe.lastIndex = idx; 540 541 let m; 542 while ((m = divTagRe.exec(src))) { 543 if (m[0][1] === '/') { 544 depth--; 545 } else { 546 depth++; 547 } 548 if (depth === 0) { 549 const end = divTagRe.lastIndex; 550 const raw = src.slice(0, end); 551 return { type: 'html', raw, text: raw, pre: false, block: true }; 552 } 553 } 554 555 return false; 556 }, 557 }, 558 }); 559 } else { 560 561 marked.use({ 562 tokenizer: { 563 html(src) { 564 // 1) Verify the *opening* tag is a div whose class contains hljs_ignore 565 const openTagRe = /^<div[^>]*\bclass=(["'])(?:(?!\1).)*\bhljs_ignore\b(?:(?!\1).)*\1[^>]*>/i; 566 567 const open = src.match(openTagRe); 568 if (!open) return false; 569 570 let depth = 1; 571 let idx = open[0].length; 572 573 const divTagRe = /<\/?div\b[^>]*>/gi; 574 divTagRe.lastIndex = idx; 575 576 let m; 577 while ((m = divTagRe.exec(src))) { 578 if (m[0][1] === '/') { 579 depth--; 580 } else { 581 depth++; 582 } 583 if (depth === 0) { 584 const end = divTagRe.lastIndex; 585 const raw = src.slice(0, end); 586 return { type: 'html', raw, text: raw, pre: false, block: true }; 587 } 588 } 589 return false; 590 }, 591 }, 592 }); 593 594 } 595 marked.setOptions({ 596 mangle: false, 597 headerIds: false, 598 sanitize: false, 599 }); 600 marked.use(extendedTables()); 601 }; 602 603 // Allow safe reference to previous AMD define 604 var _ainiroOldDefine; 605 606 /* 607 * Checking if reCAPTCHA, Marked, and SignalR are already initialised. 608 */ 609 if ((this.ainiro_settings.recaptcha === '-1' || typeof mcaptcha !== 'undefined' || typeof grecaptcha !== 'undefined') && 610 typeof marked !== 'undefined' && 611 typeof signalR !== 'undefined' && 612 typeof extendedTables !== 'undefined' && 613 (this.ainiro_settings.code === false || typeof hljs !== 'undefined')) { 614 615 afterMarked(); 616 onAfter(); 617 618 if (_ainiroOldDefine !== undefined) { 619 window.define = _ainiroOldDefine; 620 _ainiroOldDefine = undefined; 621 } 622 return; 623 } 624 625 // Scripts we should add. 626 const jsFiles = [ 627 'https://cdnjs.cloudflare.com/ajax/libs/marked/13.0.0/marked.min.js', 628 'https://ainiro.io/assets/js/marked-tables.js', 629 'https://ainiro.io/assets/js/signalr.js', 630 ]; 631 if (this.ainiro_settings.recaptcha !== '' && this.ainiro_settings.recaptcha !== '-1') { 632 jsFiles.push('https://www.google.com/recaptcha/api.js?render=' + this.ainiro_settings.recaptcha); 633 } else { 634 jsFiles.push(this.ainiro_settings.url + '/magic/system/misc/magic-captcha-challenge.js') 635 } 636 if (this.ainiro_settings.code === true) { 637 jsFiles.push('https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js'); 638 } 639 640 // To avoid confusing any module loaders here, we reset the "define" object. 641 if (window.define !== undefined) { 642 _ainiroOldDefine = window.define; 643 window.define = undefined; 644 }
645 jsFiles.forEach(idx => { 646 const el = document.createElement('script'); 647 el.src = idx; 648 document.getElementsByTagName('body')[0].appendChild(el); 649 }); 650 651 const tick = () => { 652 653 if ((this.ainiro_settings.recaptcha === '-1' || typeof mcaptcha !== 'undefined' || typeof grecaptcha !== 'undefined') && 654 typeof marked !== 'undefined' && 655 typeof signalR !== 'undefined' && 656 (this.ainiro_settings.code === false || typeof hljs !== 'undefined')) { 657 658 afterMarked(); 659 onAfter(); 660 return; 661 } 662 setTimeout(tick, 250); 663 }; 664 tick(); 665 }, 666 667 /* 668 * Actual implementation of showing chat window, which is only invoked 669 * once reCAPTCHA has been successfully included on the page. 670 */ 671 _show: function(onAfter) { 672 673 // Disabling button to avoid race conditions. 674 const btn = this.shadow.getElementById('ainiro_send'); 675 btn.disabled = true; 676 677 // Showing chat window. 678 const wnd = this.shadow.getElementById('ainiro_chat_wnd'); 679 wnd.classList.add('show_ainiro_chatbot'); 680 681 // Making sure body element cannot scroll. 682 document.body.style.height = '100vh'; 683 document.body.style.overflowY = 'hidden'; 684 685 // In case there we've got the stupid Shopify chat thing on page, we hide it, since it's way too greedy on z-index. 686 const shopifyChatbot = document.getElementById('shopify-chat'); 687 if (shopifyChatbot) { 688 shopifyChatbot.style.display = 'none'; 689 } 690 691 // Creating socket connection unless it's already created. 692 if (!this.socket) { 693 694 // Creating our socket connection. 695 this.initSocket(function() { 696 697 // Initialising our chat surface now that our socket connection has been created. 698 this.initialiseChatSurface(onAfter); 699 700 }.bind(this)); 701 702 } else { 703 704 // Initialising chat surface immediately since we already have a socket connection. 705 this.initialiseChatSurface(onAfter); 706 } 707 }, 708 709 /* 710 * Initialises chatbot. 711 */ 712 initialiseChatSurface: function(onAfter) { 713 714 // Checking if we have a greeting for the chatbot. 715 if (this.initSession && this.ainiro_settings.greeting && this.ainiro_settings.greeting !== '') { 716 717 // Adding greeting. 718 const html = this.renderMarkdownWithScriptPassthrough(this.ainiro_settings.greeting); 719 this.addMessage(html, 'ainiro_machine greeting', false); 720 } 721 722 if (this.execQuestionnaires) { 723 724 // Fetching questionnaire for type from backend. 725 fetch( 726 this.ainiro_settings.url + 727 '/magic/system/openai/questionnaire?type=' + 728 encodeURIComponent(this.ainiro_settings.type)) 729 .then(res => { 730 return res.text(); 731 }) 732 .then(res => { 733 734 // Making sure we avoid executing questionnaire again. 735 this.execQuestionnaires = false; 736 737 // Making sure we have a questionnaire for type. 738 if (!res || res === '') { 739 740 // Adding conversation starters. 741 if (this.initSession) { 742 this.addConversationStarters(onAfter); 743 } else if (onAfter) { 744 onAfter(); 745 } 746 } else { 747 748 // Parsing JSON as object. 749 res = JSON.parse(res); 750 751 // Making sure we have a questionnaire for type. 752 if (res.questions.length === 0) { 753 754 // Adding conversation starters. 755 if (this.initSession) { 756 this.addConversationStarters(onAfter); 757 } else if (onAfter) { 758 onAfter(); 759 } 760 761 } else { 762 763 // Checking if we should run through questionnaire, and if not, returning early. 764 const previous = localStorage.getItem('ainiro-questionnaire.' + res.name); 765 if (previous && res.type === 'single-shot') { 766 767 // Trying to add conversation starters. 768 if (this.initSession) { 769 this.addConversationStarters(onAfter); 770 } else if (onAfter) { 771 onAfter(); 772 } 773 774 } else { 775 776 // Starting questionnaire loop. 777 this.questionnaire = { 778 questions: res, 779 current: 0, 780 }; 781 782 // Starting questionnaire question asking process. 783 this.askQuestion(function() { 784 this.addConversationStarters(onAfter); 785 }.bind(this)); 786 } 787 } 788 } 789 }); 790 791 } else if (this.initSession) { 792 793 // Trying to add conversation starters. 794 this.addConversationStarters(onAfter); 795 796 } else if (onAfter) { 797 798 // Invoking onafter callback. 799 onAfter(); 800 } 801 802 // Checking if we're supposed to scroll to the bottom. 803 if (this.hasSessionItems) { 804 805 this.scrollToBottom(false, true); 806 807 // Making sure we do not scroll the next time chatbot it opened. 808 this.hasSessionItems = false;
809 } 810 }, 811 812 /* 813 * Creates our socket connection towards our backend. 814 */ 815 initSocket: function(onAfter) { 816 817 // Creating socket connection. 818 this.socket = new signalR.HubConnectionBuilder() 819 .withAutomaticReconnect() 820 .withUrl(this.ainiro_settings.url + '/sockets', { 821 skipNegotiation: true, 822 transport: signalR.HttpTransportType.WebSockets, 823 }).build(); 824 825 // Subscribing to messages. 826 this.socket.on(this.session, (args) => { 827 828 // Creating object from JSON. 829 const obj = JSON.parse(args); 830 831 // Checking if we're finished. 832 if (obj.type) { 833 834 switch (obj.type) { 835 836 case 'render_html': 837 this.response += '<div class="hljs_ignore">' + obj.html + '</div>'; 838 break; 839 840 default: 841 console.log('Unknown client-side binding; \'' + obj.type + '\''); 842 } 843 } else if (obj.finished === true) { 844 845 // Checking if we've got a callback for onDone, at which point we invoke it. 846 this.chatMessageDone(this.response); 847 if (this.onDone) { 848 this.onDone(); 849 this.runScriptsIn(this.shadow.getElementById('ainiro_chat_surf')); 850 } 851 return; 852 853 } else if (obj.error === true) { 854 855 // Checking if we've got an error. 856 if (obj.error === true) { 857 858 // Appending message to temporary response. 859 const html = this.renderMarkdownWithScriptPassthrough(obj.message ?? 'An unspecified error occurred, sorry about that :/'); 860 861 // Updating value of last chat message. 862 const surf = this.shadow.getElementById('ainiro_chat_surf'); 863 const msg = surf.childNodes[surf.childNodes.length - 1]; 864 msg.innerHTML = html; 865 msg.className = 'ainiro_machine ainiro_error'; 866 867 // Scrolling to the bottom of surface to make sure we display currently written text. 868 this.scrollToBottom(true, true); 869 } 870 return; 871 872 } else if (obj.function_waiting) { 873 874 // Waiting for function invocation. 875 obj.message = '<span class="ainiro_function_waiting ainiro_function_animate"><i class="ainiro-icofont ainiro-icofont-web"></i>Waiting ...</span>'; 876 877 // Resetting references to avoid showing references since server is executing AI function. 878 this.references_list = []; 879 880 } else if (obj.function_result) { 881 882 // Function invocation succeeded. 883 this.response = this.response.replace('<span class="ainiro_function_waiting"><i class="ainiro-icofont ainiro-icofont-web"></i>Waiting ...</span>', ''); 884 this.response = this.response.replace('<span class="ainiro_function_waiting ainiro_function_animate"><i class="ainiro-icofont ainiro-icofont-web"></i>Waiting ...</span>', ''); 885 obj.message = '<p><span class="ainiro_function_succeeded ainiro_function_animate"><i class="ainiro-icofont ainiro-icofont-star"></i>'+ obj.function_result + '</span></p>\n\n'; 886 887 } else if (obj.function_error) { 888 889 // Function invocation failed. 890 this.response = this.response.replace('<span class="ainiro_function_waiting"><i class="ainiro-icofont ainiro-icofont-web"></i>Waiting ...</span>', ''); 891 this.response = this.response.replace('<span class="ainiro_function_waiting ainiro_function_animate"><i class="ainiro-icofont ainiro-icofont-web"></i>Waiting ...</span>', ''); 892 obj.message = '<span class="ainiro_function_failed"><i class="ainiro-icofont ainiro-icofont-close"></i>' + obj.function_error + '</span>\n\n'; 893 894 } else if (obj.type === 'integration') { 895 896 // Some sort of integration message. 897 switch (obj.integration_type) { 898 899 default: 900 this.addMessage(obj.text, 'ainiro_machine integration ' + obj.integration_type, true); 901 const surf = this.shadow.getElementById('ainiro_chat_surf'); 902 const html = surf.innerHTML; 903 sessionStorage.setItem('ainiro_chatbot.session', html); 904 this.scrollToBottom(true, true); 905 break; 906 } 907 } 908 909 // Verifying we've got some sort of message given. 910 if (obj.message) { 911 912 // Appending message to temporary response. 913 this.response += obj.message; 914 915 // Transforming Markdown 916 const html = this.renderMarkdownWithScriptPassthrough(this.response); 917 918 // Updating value of last chat message. 919 const surf = this.shadow.getElementById('ainiro_chat_surf'); 920 const msg = surf.childNodes[surf.childNodes.length - 1]; 921 msg.innerHTML = html; 922 923 // Removing animation classes. 924 this.response = this.response.replace(' ainiro_function_animate', ''); 925 926 // Scrolling to the bottom of surface to make sure we display currently written text. 927 this.scrollToBottom(false, false); 928 } 929 }); 930 931 // Starting socket connection. 932 this.socket.start().then(onAfter); 933 }, 934 935 /* 936 * Retrieves and adds conversation starters to UI 937 * if there are any conversation starters declared for chatbot. 938 */ 939 addConversationStarters: function(onAfter) { 940 941 // Fetching conversation starters for type from backend. 942 fetch(this.ainiro_settings.url + '/magic/system/openai/conversation-starters?type=' + encodeURIComponent(this.ainiro_settings.type)) 943 .then(res => { 944 return res.json(); 945 }) 946 .then(res => { 947 948 // Making sure we've got questions. 949 if (!res.questions || res.questions.length === 0) { 950 if (onAfter) { 951 onAfter(); 952 } 953 954 // Making sure we don't try to add conversation starters twice. 955 this.initSession = false;
956 return; // Nothing to do here ... 957 } 958 959 this.addConversationElements(res.questions); 960 if (onAfter) { 961 onAfter(); 962 } 963 964 // Making sure we don't add conversation starters twice. 965 this.initSession = false; 966 }); 967 }, 968 969 /* 970 * Adds conversation elements to surface, which might be conversation starters, 971 * or follow up questions. 972 */ 973 addConversationElements(questions) { 974 975 // Creating wrapper element for conversation starters. 976 const wrp = document.createElement('div'); 977 wrp.className = 'ainiro_starters'; 978 wrp.id = 'ainiro_starter'; 979 980 // Adding button for each question. 981 questions.forEach(idx => { 982 const el = document.createElement('button'); 983 el.className = 'ainiro_starter'; 984 el.innerHTML = idx; 985 el.setAttribute('onclick', `window.ask_follow_up(event)`); 986 wrp.appendChild(el); 987 }); 988 989 // Adding conversation starters to surface. 990 const surf = this.shadow.getElementById('ainiro_chat_surf'); 991 surf.appendChild(wrp); 992 this.scrollToBottom(true, false); 993 }, 994 995 /* 996 * Scrolls to the bottom of the chatbot surface. 997 */ 998 scrollToBottom: function(smooth, force) { 999 1000 // Retrieving element we're supposed to scroll. 1001 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1002 1003 if (force || surf.scrollTop + 50 > (surf.scrollHeight - surf.offsetHeight)) { 1004 1005 if (smooth) { 1006 const last = surf.childNodes[surf.childNodes.length - 1]; 1007 last.scrollIntoView({behavior: 'smooth', block: 'start'}); 1008 } else { 1009 surf.scrollTop = surf.scrollHeight; 1010 } 1011 } 1012 }, 1013 1014 /* 1015 * Maximize or minimizes the chat window. 1016 */ 1017 maximize: function() { 1018 1019 // Maximizing chat window. 1020 const wnd = this.shadow.getElementById('ainiro_chat_wnd'); 1021 wnd.classList.toggle('ainiro_maximized'); 1022 }, 1023 1024 /* 1025 * Function to clear chat window and start a new session. 1026 */ 1027 clear: function() { 1028 1029 // Clearing out chat window and session storage. 1030 sessionStorage.removeItem('ainiro_chatbot.session'); 1031 var lst = []; 1032 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1033 surf.childNodes.forEach(el => { 1034 lst.push(el); 1035 }); 1036 lst.forEach(el => { 1037 el.parentElement.removeChild(el); 1038 }); 1039 1040 // Making sure we don't have to execute questionnaires again. 1041 this.execQuestionnaires = false; 1042 1043 // Creating a new session and stopping existing socket connection. 1044 this.session = 'c_' + Date.now() + Math.random(); 1045 this.socket?.stop(); 1046 1047 this.initSession = true; 1048 this.initSocket(function() { 1049 this.initialiseChatSurface(); 1050 }.bind(this)); 1051 }, 1052 1053 showSession: function() { 1054 1055 // Retreiving session items for user. 1056 fetch(`${this.ainiro_settings.url}/magic/system/openai/history-list?user_id=` + encodeURIComponent(this.userId)) 1057 .then(res => { 1058 if (res.status >= 200 && res.status <= 299) { 1059 return res.text(); // read raw text first 1060 } else { 1061 throw res; 1062 } 1063 }) 1064 .then(text => { 1065 if (text.trim().length === 0) { 1066 return []; 1067 } 1068 return JSON.parse(text); 1069 }) 1070 .then(res => { 1071 this._showSession(res); 1072 }) 1073 .catch(err => { 1074 console.error("Fetch error:", err); 1075 }); 1076 }, 1077 1078 _showSession: function(items) { 1079 1080 // Checking if dropdown already exists, at which point we delete it. 1081 const existing = this.shadow.querySelector('.ainiro_sessions_list'); 1082 if (existing) { 1083 existing.parentNode.removeChild(existing); 1084 return; 1085 } 1086 1087 // Making sure we've got items at all. 1088 if (items.length === 0) { 1089 return; 1090 } 1091 1092 // Figuring out height of toolbar. 1093 const toolbar = this.shadow.querySelector('.ainiro_toolbar'); 1094 const topPosition = toolbar.clientHeight + 5; 1095 1096 // Finding parent element to inject popup into. 1097 const parentEl = this.shadow; 1098 1099 // Creating our popup div. 1100 const sessionEl = document.createElement('div'); 1101 sessionEl.className = 'ainiro_sessions_list'; 1102 sessionEl.style.top = topPosition + 'px'; 1103 1104 // Creating our unordered list. 1105 const ul = document.createElement('ul'); 1106 sessionEl.appendChild(ul); 1107 1108 // Adding each session item into popup. 1109 for (let idx = 0; idx < items.length; idx++) { 1110 const cur = document.createElement('li'); 1111 const link = document.createElement('a'); 1112 link.href = '#'; 1113 link.innerHTML = items[idx].name; 1114 link.addEventListener('click', (ev) => this.selectSession(ev, items[idx].session_id)); 1115 cur.appendChild(link); 1116 ul.appendChild(cur); 1117 } 1118 1119 // Adding popup into parent. 1120 parentEl.appendChild(sessionEl); 1121 }, 1122 1123 selectSession: function(event, session) { 1124 1125 event.stopPropagation(); 1126 event.preventDefault() 1127 1128 // Making sure we destroy drop down ... 1129 const existing = this.shadow.querySelector('.ainiro_sessions_list'); 1130 if (existing) { 1131 existing.parentNode.removeChild(existing); 1132 } 1133 1134 // Fetching session and making it our current session. 1135 fetch(`${this.ainiro_settings.url}/magic/system/openai/active-session`, { 1136 method: 'POST', 1137 headers: { 1138 "Content-Type": 'application/json' 1139 }, 1140 body: JSON.stringify({ 1141 session_id: session 1142 }) 1143 }) 1144 .then(res => { 1145 if (res.status >= 200 && res.status <= 299) { 1146 return res.json(); 1147 } else { 1148 throw res; 1149 } 1150 }) 1151 .then((res) => { 1152 1153 // Updating GUI ... 1154 const surface = this.shadow.getElementById('ainiro_chat_surf'); 1155 1156 // Deleting old items. 1157 const toDel = []; 1158 for (let idx = 0; idx < surface.childNodes.length; idx++) { 1159 switch (surface.childNodes[idx].className) { 1160 case 'ainiro_human': 1161 case 'ainiro_machine': 1162 case 'ainiro_starters': 1163 toDel.push(surface.childNodes[idx]); 1164 break; 1165 } 1166 } 1167 for (let idx = 0; idx < toDel.length; idx++) { 1168 toDel[idx].parentNode.removeChild(toDel[idx]); 1169 } 1170 1171 // Adding new items. 1172 for (let idx = 0; idx < res.length; idx++) { 1173 switch (res[idx].role) { 1174 1175 case 'user': 1176 this.addMessage(res[idx].content, 'ainiro_human'); 1177 break; 1178 1179 case 'assistant': 1180 this.addMessage('', 'ainiro_machine'); 1181 this.chatMessageDone(res[idx].content, idx === res.length - 1); 1182 break; 1183 } 1184 } 1185 1186 // Scrolling to bottom. 1187 this.scrollToBottom(true, true); 1188 1189 // Updating session, which needs to recreate the socket connection. 1190 this.session = session; 1191 this.socket?.stop(); 1192 this.initSocket(null); 1193 }); 1194 }, 1195 1196 /* 1197 * Hides the chat window. 1198 */ 1199 hide: function() { 1200 1201 // Shows chat button. 1202 const btn = this.shadow.getElementById('ainiro_chat_btn'); 1203 btn.classList.remove('ainiro_hide'); 1204 1205 // Hides chat window. 1206 const wnd = this.shadow.getElementById('ainiro_chat_wnd'); 1207 wnd.classList.remove('show_ainiro_chatbot'); 1208 1209 // Making sure body element cannot scroll. 1210 document.body.style.height = ''; 1211 document.body.style.overflowY = ''; 1212 1213 // Showing Shopify chat again (if it exists on page). 1214 const shopifyChatbot = document.getElementById('shopify-chat'); 1215 if (shopifyChatbot) { 1216 shopifyChatbot.style.display = 'block'; 1217 } 1218 sessionStorage.setItem('ainiro_state', 'closed'); 1219 }, 1220 1221 /* 1222 * Adds the specified message with the specified CSS class, and optionally 1223 * creates an animation to animate element into view. 1224 */ 1225 addMessage: function(msg, cls, animate = true) { 1226 1227 // Creating message wrapper. 1228 const el = document.createElement('div'); 1229 el.innerHTML = msg; 1230 if (animate) { 1231 cls += ' ainiro_just_added'; 1232 setTimeout(() => { 1233 el.classList.remove('ainiro_just_added'); 1234 }, 1000); 1235 } 1236 el.className = cls; 1237 1238 // Checking if we've got "ainiro_starter" element, at which point we insert the message *BEFORE* these. 1239 const starters = this.shadow.getElementById('ainiro_starter'); 1240 if (starters) { 1241 1242 // Appending message to surface container. 1243 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1244 surf.insertBefore(el, starters); 1245 1246 } else { 1247 1248 // Appending message to surface container. 1249 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1250 surf.appendChild(el); 1251 } 1252 }, 1253 1254 /* 1255 * Invoked when form is submitted. 1256 */ 1257 submit: function() { 1258 1259 // Making sure user provided any actual text. 1260 const txtEl = this.shadow.getElementById('ainiro_txt'); 1261 if (txtEl.value.trim() === '') { 1262 return; 1263 } 1264 1265 // Disabling send button. 1266 const btn = this.shadow.getElementById('ainiro_send'); 1267 btn.disabled = true; 1268 1269 // Making sure we remove conversation starters if they're in the DOM. 1270 const wrp = Array.from(this.shadow.querySelectorAll('.ainiro_starters')); 1271 if (wrp) { 1272 for (let idx = 0; idx < wrp.length; idx++) { 1273 wrp[idx].parentNode.removeChild(wrp[idx]); 1274 } 1275 } 1276 1277 // Removing text of upload file button. 1278 const lbl = this.shadow.getElementById('ainiro_filename_label'); 1279 if (lbl) { 1280 lbl.innerHTML = ''; 1281 } 1282 1283 // Adding query to surface. 1284 this.addMessage(txtEl.value, 'ainiro_human'); 1285 1286 // Common function to invoke once CAPTCHA has been resolved. 1287 const functor = (token) => { 1288 1289 // Checking if we're in "questionnaire mode". 1290 if (this.questionnaire) { 1291 1292 // Submitting questionnaire answer. 1293 this.submitAnswer(token); 1294 1295 } else { 1296 1297 // Submitting question to chatbot. 1298 this.submitQuestion(token); 1299 } 1300 }; 1301 1302 // Checking if we've got a reCAPTCHA site key. 1303 if (this.ainiro_settings.recaptcha && this.ainiro_settings.recaptcha !== '' && this.ainiro_settings.recaptcha !== '-1') { 1304 1305 // We have a reCAPTCHA site-key. 1306 grecaptcha.ready(function() { 1307 grecaptcha.execute(this.ainiro_settings.recaptcha, {action: 'submit'}).then(function(token) { 1308 functor(token) 1309 }.bind(this)); 1310 }.bind(this)); 1311 1312 } else if (this.ainiro_settings.recaptcha !== '-1') { 1313 1314 // No reCAPTCHA site-key is specified for type, using Magic CAPTCHA. 1315 mcaptcha.token(function (token) { 1316 functor(token); 1317 }.bind(this), 3); 1318 1319 } else { 1320 1321 // No CAPTCHA what so ever is being used. 1322 functor(); 1323 } 1324 }, 1325 1326 /* 1327 * Submits answer to questionnaire question to backend. 1328 */ 1329 submitAnswer: function(token) { 1330 1331 // Retrieving query input field and sanity checking input. 1332 const txtEl = this.shadow.getElementById('ainiro_txt'); 1333 if (txtEl.value.trim() === '') { 1334 return; 1335 } 1336 1337 // Disabling send button. 1338 const btn = this.shadow.getElementById('ainiro_send'); 1339 btn.disabled = true; 1340 1341 // Creating our payload. 1342 const payload = { 1343 type: window.getAiniroChatbotType ? window.getAiniroChatbotType() ?? this.ainiro_settings.type : this.ainiro_settings.type, 1344 session: this.session, 1345 user_id: this.userId, 1346 question: this.questionnaire.questions.questions[this.questionnaire.current].question, 1347 answer: txtEl.value, 1348 context: this.questionnaire.questions.questions[this.questionnaire.current].context, 1349 session: this.session, 1350 }; 1351 if (token) { 1352 payload.recaptcha_response = token; 1353 } 1354 1355 // Invoking backend, with reCAPTCHA response if we've got a site-key. 1356 fetch(`${this.ainiro_settings.url}/magic/system/openai/answer`, { 1357 method: 'POST', 1358 headers: { 1359 "Content-Type": 'application/json' 1360 }, 1361 body: JSON.stringify(payload) 1362 }).then(res => { 1363 if (res.status >= 200 && res.status <= 299) { 1364 return res.json(); 1365 } else { 1366 throw res; 1367 } 1368 }).then(() => { 1369 1370 // Retrieving query input field and resetting its prompt. 1371 const txtbox = this.shadow.getElementById('ainiro_txt'); 1372 txtbox.value = ''; 1373 1374 // Storing user's answer. 1375 this.questionnaire.questions.questions[this.questionnaire.current].answer = txtEl.value; 1376 1377 // Checking if we've got a callback for onDone, at which point we invoke it. 1378 if (this.onDone) { 1379 this.onDone(); 1380 } 1381 1382 // Making sure we enable form fields. 1383 this.onFinished(); 1384 1385 // Incrementing current question index. 1386 this.questionnaire.current += 1; 1387 1388 // Checking if we've got more questions, and if not, storing questionnaire, and returning early. 1389 if (this.questionnaire.current >= this.questionnaire.questions.questions.length) { 1390 1391 // Finalizing questionnaire. 1392 this.onQuestionnaireDone(); 1393 return; 1394 } 1395 1396 // Asking next question. 1397 this.askQuestion(); 1398 1399 }).catch(err => { 1400 1401 // Oops, error. 1402 err.json().then(errObj => { 1403 1404 // Appending message to temporary response. 1405 const html = this.renderMarkdownWithScriptPassthrough(errObj.message); 1406 1407 // Updating value of last chat message. 1408 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1409 const msg = surf.childNodes[surf.childNodes.length - 1]; 1410 msg.innerHTML = html; 1411 msg.className = 'ainiro_machine ainiro_error'; 1412 1413 // Scrolling to the bottom of surface to make sure we display currently written text. 1414 this.scrollToBottom(true, true); 1415 1416 // Checking if we've got a callback for onDone, at which point we invoke it. 1417 if (this.onDone) { 1418 this.onDone(); 1419 } 1420 1421 // Making sure we enable form fields. 1422 this.onFinished(); 1423 }); 1424 }); 1425 }, 1426 1427 /* 1428 * Invoked when questionnaire has been done and needs to be finalized. 1429 * 1430 * Might include invoking server-side action declared for questionnaire. 1431 */ 1432 onQuestionnaireDone: function() { 1433 1434 // Checking if we've got an action associated with questionnaire. 1435 if (this.questionnaire.questions.action && this.questionnaire.questions.action !== '') { 1436 1437 // Invoking action on server. 1438 const payload = { 1439 action: this.questionnaire.questions.action, 1440 values: {}, 1441 }; 1442 this.questionnaire.questions.questions.forEach(idx => { 1443 if (idx.name && idx.name !== '') { 1444 payload.values[idx.name] = idx.answer; 1445 } 1446 }); 1447 fetch(`${this.ainiro_settings.url}/magic/system/openai/questionnaire-action`, { 1448 method: 'POST', 1449 headers: { 1450 "Content-Type": 'application/json' 1451 }, 1452 body: JSON.stringify(payload) 1453 }).then(res => { 1454 if (res.status >= 200 && res.status <= 299) { 1455 return res.json(); 1456 } else { 1457 throw Error(res.statusText); 1458 } 1459 }).then(() => { 1460 1461 // Adding conversation starters if there are any. 1462 if (this.initSession) { 1463 this.addConversationStarters(); 1464 } 1465 }); 1466 1467 } else { 1468 1469 // Adding conversation starters if there are any. 1470 if (this.initSession) { 1471 this.addConversationStarters(); 1472 } 1473 } 1474 1475 // Checking if this is single-shot action at which point we store the fact that we've taken questionnaire in localStorage. 1476 if (this.questionnaire.questions.type === 'single-shot') { 1477 1478 // Storing the fact that we've taken questionnaire in localStorage. 1479 localStorage.setItem( 1480 'ainiro-questionnaire.' + this.questionnaire.questions.name, 1481 'true'); 1482 } 1483 1484 // Setting questionnaire to null makes sur
1484e LLM takes over. 1485 this.questionnaire = null; 1486 }, 1487 1488 /* 1489 * Asks the user a single question from the chatbot's questionnaire. 1490 */ 1491 askQuestion: function() { 1492 1493 // Adds the current questionnaire message as a machine / question. 1494 this.addMessage(this.questionnaire.questions.questions[this.questionnaire.current].question, 'ainiro_machine ainiro_question'); 1495 1496 // Making sure last message is scrolled into view. 1497 this.scrollToBottom(true, true); 1498 1499 // Waiting for 800 milliseconds before we check if we should show next questionnaire item. 1500 setTimeout(() => { 1501 1502 /* 1503 * Removing animation CSS class from question. 1504 */ 1505 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1506 surf.childNodes[surf.childNodes.length - 1].classList.remove('ainiro_question'); 1507 1508 // Checking if currently asked question was in fact a message, at which point we ask the next question. 1509 if (this.questionnaire.questions.questions[this.questionnaire.current].type === 'message') { 1510 1511 // Incrementing questionnaire index. 1512 this.questionnaire.current += 1; 1513 1514 // Checking if we've got more questions, and if not, storing questionnaire, and returning early. 1515 if (this.questionnaire.current >= this.questionnaire.questions.questions.length) { 1516 1517 // Finalizing questionnaire. 1518 this.onQuestionnaireDone(); 1519 1520 // Enabling send button. 1521 const btn = this.shadow.getElementById('ainiro_send'); 1522 btn.disabled = false; 1523 1524 return; 1525 } 1526 1527 // Removing animation class for last chatbot question. 1528 this.askQuestion(); 1529 1530 // Scrolling to bottom to make sure user sees the question. 1531 this.scrollToBottom(true, true); 1532 1533 } else { 1534 1535 // Enabling send button. 1536 const btn = this.shadow.getElementById('ainiro_send'); 1537 btn.disabled = false; 1538 } 1539 1540 }, 800); 1541 }, 1542 1543 /* 1544 * Submits question to chatbot. 1545 */ 1546 submitQuestion: function(token) { 1547 1548 // Retrieving query input field and disabling it. 1549 const txtEl = this.shadow.getElementById('ainiro_txt'); 1550 if (txtEl.value.trim() === '') { 1551 return; 1552 } 1553 1554 // Disabling send button. 1555 const btn = this.shadow.getElementById('ainiro_send'); 1556 btn.disabled = true; 1557 1558 // Adding wait message to surface. 1559 this.addMessage('<p><span class="ainiro-dot ainiro-dot-1" style="background-color: ' + this.ainiro_settings.color + '"></span><span class="ainiro-dot ainiro-dot-2" style="background-color: ' + this.ainiro_settings.color + '"></span><span class="ainiro-dot ainiro-dot-3" style="background-color: ' + this.ainiro_settings.color + '"></span></p>', 'ainiro_machine'); 1560 this.scrollToBottom(true, true); 1561 1562 // Resetting response. 1563 this.response = ''; 1564 1565 // Building our URL. 1566 let type = this.ainiro_settings.type; 1567 if (window.getAiniroChatbotType) { 1568 type = window.getAiniroChatbotType() ?? type; 1569 } 1570 let url = this.ainiro_settings.url + `/magic/system/openai/chat`; 1571 let payload = { 1572 prompt: txtEl.value, 1573 type: type, 1574 session: this.session, 1575 user_id: this.userId, 1576 chat: true, 1577 stream: true, 1578 referrer: window.location.href, 1579 extra: this.ainiro_settings.extra, 1580 permanent: this.ainiro_settings.history 1581 }; 1582 if (token) { 1583 payload.recaptcha_response = token; 1584 } 1585 if (this.ainiro_settings.meta && this.ainiro_settings.meta !== '') { 1586 payload.meta = this.ainiro_settings.meta; 1587 } 1588 if (this.ainiro_settings.references) { 1589 payload.references = true; 1590 } 1591 const formData = new FormData(); 1592 for(const name in payload) { 1593 formData.append(name, payload[name]); 1594 } 1595 for (let idx = 0; idx < (this.rawFiles || []).length; idx++) { 1596 formData.append('file', this.rawFiles[idx]); 1597 } 1598 1599 // Resetting onDone. 1600 this.onDone = null; 1601 1602 // Resetting references. 1603 this.references_list = []; 1604 1605 // Invoking backend. 1606 fetch(url, { 1607 method: 'POST', 1608 body: formData 1609 }).then(res => { 1610 if (!res.ok) { 1611 throw res; 1612 } 1613 return res.json(); 1614 }).then(res => { 1615
1616 // If server returns a cached answer, result will contain the response. 1617 if (res.result) { 1618 1619 // Invoking function responsible for wrapping up a chat response. 1620 this.chatMessageDone(res.result); 1621 1622 } else { 1623 1624 // Storing references. 1625 this.references_list = res.references ?? []; 1626 1627 // Server is not returning cached answer, and therefor we must store references, and make sure we adds these afterwards. 1628 this.onDone = () => { 1629 1630 // Adding references. 1631 if (this.references_list.length > 0) { 1632 this.addReferences(this.references_list); 1633 } 1634 }; 1635 } 1636 1637 // Retrieving query input field and resetting its prompt. 1638 const txtbox = this.shadow.getElementById('ainiro_txt'); 1639 txtbox.value = ''; 1640 const fileInp = this.shadow.getElementById('ainiro_upload'); 1641 if (fileInp) { 1642 fileInp.value = null; 1643 } 1644 this.rawFiles = null; 1645 1646 }).catch(err => { 1647 1648 // Oops, error. 1649 err.json().then(errObj => { 1650 1651 // Appending message to temporary response. 1652 const html = this.renderMarkdownWithScriptPassthrough(errObj.message); 1653 1654 // Updating value of last chat message. 1655 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1656 const msg = surf.childNodes[surf.childNodes.length - 1]; 1657 msg.innerHTML = html; 1658 msg.className = 'ainiro_machine ainiro_error'; 1659 1660 // Making sure we enable form fields. 1661 this.onFinished(); 1662 1663 // Scrolling to the bottom of surface to make sure we display currently written text. 1664 this.scrollToBottom(true, true); 1665 }); 1666 }); 1667 }, 1668 1669 /* 1670 * Invoked when a chat message is completed by server. 1671 */ 1672 chatMessageDone: function(response, addConvoFollowUps = true) { 1673 1674 // Defaulting Markdown to be transformed to entire result from server. 1675 let wholeMarkdown = response; 1676 1677 // Buffer for follow up questions. This will be empty if we don't have follow up questions. 1678 let followUpQuestions = []; 1679 1680 const markdownSections = wholeMarkdown.split('\n---'); 1681 if (this.ainiro_settings.follow_up && markdownSections.length > 1) { 1682 followUpQuestions = markdownSections.pop(); 1683 wholeMarkdown = markdownSections.join('\n---').trim(); 1684 } 1685 const html = this.renderMarkdownWithScriptPassthrough(wholeMarkdown); 1686 1687 // Updating value of last chat message. 1688 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1689 const msg = surf.childNodes[surf.childNodes.length - 1]; 1690 msg.innerHTML = html; 1691 1692 // Checking if we've got code syntax highlighting turned on. 1693 if (this.ainiro_settings.code === true) { 1694 1695 // Finding all PRE elements. 1696 for (let idx = 0; idx < msg.childNodes.length; idx++) { 1697 1698 if (msg.childNodes[idx].tagName === 'PRE') { 1699 hljs.highlightElement(msg.childNodes[idx]); 1700 msg.childNodes[idx].innerHTML = msg.childNodes[idx].innerHTML + '<button class="copy_code_button" onclick="ainiro.copyCode(event)"><i class="ainiro-icofont ainiro-icofont-copy"></i></button>'; 1701 } 1702 } 1703 } 1704 1705 // Adding references immediately. 1706 this.addReferences(this.references); 1707 1708 // Checking if we've got follow up questions. 1709 if (followUpQuestions.length > 0 && addConvoFollowUps) { 1710 1711 const followUp = []; 1712 const htmlFU = this.renderMarkdownWithScriptPassthrough(followUpQuestions); 1713 const domRoot = document.createElement('div'); 1714 domRoot.innerHTML = htmlFU; 1715 1716 const listItems = domRoot.childNodes; 1717 for (let idxNo = 0; idxNo < listItems.length; idxNo++) { 1718 if (listItems[idxNo].tagName == 'UL' || listItems[idxNo].tagName === 'OL') { 1719 for (let idxChildNo = 0; idxChildNo < listItems[idxNo].childNodes.length; idxChildNo ++) { 1720 if (listItems[idxNo].childNodes[idxChildNo].tagName === 'LI') { 1721 followUp.push(listItems[idxNo].childNodes[idxChildNo].innerText); 1722 } 1723 } 1724 } 1725 } 1726 this.addConversationElements(followUp); 1727 } 1728 1729 // Making sure we enable form fields. 1730 this.onFinished(); 1731 1732 // Scrolling to the bottom of surface to make sure we display currently written text. 1733 this.scrollToBottom(false, false); 1734 }, 1735 1736 extractScripts: function(src) { 1737 1738 const scripts = []; 1739 const TOKEN = `%%RAW_SCRIPT_${Math.random().toString(36).slice(2)}%%`; 1740 let i = 0, out = '', pos = 0; 1741 1742 const findNextFence = (s, from) => { 1743 const openRe = /(^|\r?\n)[ \t]*(?:>
1743[ \t]*)*(?:(?:\d+[.)]|[*\-+])[ \t]+)?([`~]{3,})([^\r\n]*)(?:\r?\n|$)/g; 1744 const slice = s.slice(from); 1745 const m = openRe.exec(slice); 1746 if (!m) return null; 1747 1748 const marks = m[2], fChar = marks[0], fLen = marks.length; 1749 const openAbsStart = from + m.index + (m[1] ? m[1].length : 0); 1750 const afterOpen = from + m.index + m[0].length; 1751 const fenceCharEsc = fChar === '`' ? '\\`' : '~'; 1752 const closeRe = new RegExp('(^|\\r?\\n)[ \\t]*(?:>[ \\t]*)*' + fenceCharEsc + '{' + fLen + ',}\\s*(?=\\r?\\n|$)', 'g'); 1753 1754 const slice2 = s.slice(afterOpen); 1755 const m2 = closeRe.exec(slice2); 1756 const endAbs = m2 ? (afterOpen + m2.index + m2[0].length) : s.length; 1757 return { start: openAbsStart, end: endAbs }; 1758 }; 1759 1760 const findNextScript = (s, from) => { 1761 const re = /<script\b[^>]*>[\s\S]*?<\/script\s*>/i; 1762 const slice = s.slice(from); 1763 const m = re.exec(slice); 1764 if (!m) return null; 1765 const start = from + m.index; 1766 const end = start + m[0].length; 1767 return { start, end, html: m[0] }; 1768 }; 1769 1770 while (pos < src.length) { 1771 1772 const fence = findNextFence(src, pos); 1773 const scr = findNextScript(src, pos); 1774 1775 if (!fence && !scr) { out += src.slice(pos); break; } 1776 1777 if (fence && (!scr || fence.start <= scr.start)) { 1778 out += src.slice(pos, fence.end); 1779 pos = fence.end; 1780 continue; 1781 } 1782 1783 if (scr) { 1784 out += src.slice(pos, scr.start); 1785 const key = `${TOKEN}${i++}__`; 1786 scripts.push({ key, html: scr.html }); 1787 out += key; 1788 pos = scr.end; 1789 continue; 1790 } 1791 } 1792 return { without: out, scripts }; 1793 }, 1794 1795 restoreScripts: function(html, scripts) { 1796 1797 let out = html; 1798 for (const s of scripts) out = out.split(s.key).join(s.html); 1799 return out; 1800 }, 1801 1802 renderMarkdownWithScriptPassthrough: function (md) { 1803 1804 const parts = this.extractScripts(md); 1805 const rendered = marked.parse(parts.without); 1806 return this.restoreScripts(rendered, parts.scripts); 1807 }, 1808 1809 /* 1810 * Invoked when user wants to copy code. 1811 */ 1812 copyCode: function(e) { 1813 let src = e.target || e.srcElement; 1814 if (src.tagName === 'I') { 1815 src = src.parentElement; 1816 } 1817 const msg = src.parentElement.innerText; 1818 navigator.clipboard.writeText(msg).then(() => { 1819 console.log('Code copied to clipboard'); 1820 }); 1821 }, 1822 1823 /* 1824 * Invoked when references are supposed to be added to the output response element. 1825 */ 1826 addReferences: function(references) { 1827 1828 // Verifying we actually have references. 1829 if (!references || references.length === 0) { 1830 return; 1831 } 1832 1833 // Creating bulleted list to contain references. 1834 const ul = document.createElement('ul'); 1835 ul.className = 'ainiro_references'; 1836 1837 // Looping through each reference and adding it as a DOM element to result element. 1838 references.forEach(idx => { 1839 1840 // Creating list item and hyperlink. 1841 const li = document.createElement('li'); 1842 const a = document.createElement('a'); 1843 a.href = idx.uri; 1844 a.innerText = idx.prompt; 1845 if (this.ainiro_settings.new_tab) { 1846 a.target = '_blank'; 1847 } 1848 li.appendChild(a); 1849 1850 // Adding list item (with hyperlink) to ul element. 1851 ul.appendChild(li); 1852 }); 1853 1854 // Adding ul element to surface. 1855 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1856 let last = surf.childNodes[surf.childNodes.length - 1]; 1857 if (last.classList.contains('ainiro_starters')) { 1858 last = surf.childNodes[surf.childNodes.length - 2]; 1859 } 1860 last.appendChild(ul); 1861 1862 // Storing HTML for surface area into session. 1863 const html = surf.innerHTML; 1864 sessionStorage.setItem('ainiro_chatbot.session', html); 1865 }, 1866 1867 /* 1868 * Invoked when server is finished streaming response from OpenAI. 1869 * 1870 * Enables textbox input field and submit button, and rmoves prompt and sets focus to input field. 1871 */ 1872 onFinished: function() { 1873 1874 // Enabling send button. 1875 const btn = this.shadow.getElementById('ainiro_send'); 1876 btn.disabled = false;
1877 1878 // Retrieving surface for chat messages. 1879 const surf = this.shadow.getElementById('ainiro_chat_surf'); 1880 1881 // Adding "copy response button", if we should. 1882 if (this.ainiro_settings.copyButton) { 1883 1884 // Chatbot is configured to have a "copy response" button. 1885 let lastMsg = surf.childNodes[surf.childNodes.length - 1]; 1886 if (lastMsg.classList.contains('ainiro_starters')) { 1887 lastMsg = surf.childNodes[surf.childNodes.length - 2]; 1888 } 1889 if (lastMsg.classList.contains('ainiro_machine')) { 1890 lastMsg.innerHTML = `<button class='ainiro_copy_response' onclick='ainiro.copyAnswer(event);'><i class="ainiro-icofont ainiro-icofont-copy"></i></button>` + lastMsg.innerHTML; 1891 } 1892 } 1893 1894 // Storing chat messages in local storage. 1895 const html = surf.innerHTML; 1896 sessionStorage.setItem('ainiro_chatbot.session', html); 1897 }, 1898 1899 /* 1900 * Invoked when user wants to copy the response from the AI. 1901 */ 1902 copyAnswer: function(e) { 1903 1904 let src = e.target || e.srcElement; 1905 if (src.tagName === 'I') { 1906 src = src.parentElement; 1907 } 1908 const msg = src.parentElement.innerText; 1909 navigator.clipboard.writeText(msg).then(() => { 1910 console.log('Answer copied to clipboard'); 1911 }); 1912 }, 1913 1914 /* 1915 * Opens the chatbot from an external button. 1916 * 1917 * This might be an FAQ button, or an external button, somehow triggering the AI chatbot. 1918 */ 1919 ask: function(e, msg = null) { 1920 1921 // Retrieving message user wants to ask, if any, prioritising explicit message passed into function. 1922 const question = msg ?? e.srcElement.innerText; 1923 1924 // Checking if we should show questionnaires. 1925 if (question && question !== '') { 1926 1927 /* 1928 * Since the user asks an FAQ question, we prevent questionnaires from being shown, 1929 * to allow question to being answered immediately, without forcing the user through 1930 * a quesitonnaire first. 1931 */ 1932 this.execQuestionnaires = false; 1933 } 1934 1935 // Opening chat window, and ensuring we submit form once the chat window is visible. 1936 this.show(() => { 1937 1938 // Setting query 1939 const query = this.shadow.getElementById('ainiro_txt'); 1940 query.value = question; 1941 1942 if (question && question !== '') { 1943 this.submit(); 1944 } 1945 }); 1946 }, 1947 1948 runScriptsIn: async function (root) { 1949 1950 const scripts = Array.from(root.querySelectorAll('.hljs_ignore script:not([data-executed])')); 1951 1952 for (const oldScript of scripts) { 1953 oldScript.setAttribute('data-executed', '1'); 1954 1955 const parent = oldScript.parentNode; 1956 if (!parent) { 1957 continue; 1958 } 1959 const newScript = document.createElement('script'); 1960 for (const { name, value } of Array.from(oldScript.attributes)) { 1961 if (name !== 'data-executed') { 1962 newScript.setAttribute(name, value); 1963 } 1964 } 1965 1966 const isExternal = !!oldScript.src; 1967 if (!isExternal) { 1968 newScript.textContent = oldScript.textContent || ''; 1969 } 1970 1971 const wait = isExternal 1972 ? new Promise(function (resolve) { 1973 newScript.addEventListener('load', function () { resolve(); }, { once: true }); 1974 newScript.addEventListener('error', function (ev) { 1975 console.error('External classic script failed to load:', newScript.src, ev); 1976 resolve(); 1977 }, { once: true }); 1978 }) 1979 : Promise.resolve(); 1980 1981 try { 1982 parent.replaceChild(newScript, oldScript); 1983 await wait; 1984 } catch (e) { 1985 console.error('Inline classic script threw while executing:', e, newScript); 1986 } 1987 } 1988 } 1989 }; 1990 1991 // Initializing chatbot. 1992 ainiro.init(); 1993 1994 // Making sure we're backwards compatible with FAQ invocations. 1995 window.ainiro_faq_question = window.ainiro.ask.bind(window.ainiro); 1996 1997 // Internal function to ask follow up question. 1998 window.ask_follow_up = function(e) { 1999 2000 // Changing value of textbox. 2001 const query = window.ainiro.shadow.getElementById('ainiro_txt'); 2002 const question = e.srcElement.innerText; 2003 query.value = question; 2004 2005 // Submitting form to retrieve answer to question. 2006 window.ainiro.submit(); 2007 } 2008 2009})();
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.