PageSourceSearch

https://ainiro.io/magic/system/openai/include-chatbot.js?reference…0by%20AINIRO%20%F0%9F%91%8B%20&v=qwexxyz123

js ainiro.io collected 2026-09-24 22:08:33 UTC 67,764 bytes, 2,009 lines download raw bytes

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.