PageSourceSearch

https://www.smilesolutions.com.au/wp-content/plugins/arcadian-chatbot/assets/js/chat-interface.js?ver=2.0.1

js smilesolutions.com.au collected 2026-10-02 19:57:18 UTC 37,463 bytes, 1,286 lines download raw bytes

1/**
2 * Arcadian Chatbot Frontend JavaScript - RAG Version
3 *
4 * @package Arcadian_Chatbot
5 * @since 2.0.0
6 */
7
8(function ($) {
9  "use strict";
10
11  // Plugin namespace
12  window.ArcadianChatbot = window.ArcadianChatbot || {};
13
14  /**
15   * Chat Instance Class
16   */
17  class ChatInstance {
18    constructor(container) {
19      this.container = $(container);
20      this.instanceId = this.container.attr("id");
21      this.sessionId = null;
22      this.isTyping = false;
23      this.messageHistory = [];
24      this.settings = this.parseSettings();
25
26      this.init();
27    }
28
29    /**
30     * Initialize the chat instance
31     */
32    init() {
33      this.cacheElements();
34      this.bindEvents();
35      this.setupAutoResize();
36      this.loadSessionFromStorage();
37
38      // Initialize features
39      if (this.settings.sound) {
40        this.initSoundEffects();
41      }
42
43      //this.log("Chat instance initialized", this.instanceId);
44    }
45
46    /**
47     * Cache DOM elements
48     */
49    cacheElements() {
50      this.elements = {
51        messages: this.container.find(".arc-chat-messages"),
52        input: this.container.find(".arc-chat-input"),
53        sendBtn: this.container.find(".arc-send-btn"),
54        typingIndicator: this.container.find(".arc-typing-indicator"),
55        charCount: this.container.find(".arc-char-count"),
56        suggestions: this.container.find(".arc-suggestion-btn"),
57        quickActions: this.container.find(".arc-quick-action"),
58        emojiBtn: this.container.find(".arc-emoji-btn"),
59        minimizeBtn: this.container.find(".arc-chat-minimize"),
60        errorContainer: this.container.find(".arc-error-container"),
61        errorText: this.container.find(".arc-error-text"),
62        errorClose: this.container.find(".arc-error-close"),
63      };
64    }
65
66    /**
67     * Parse settings from container data attributes
68     */
69    parseSettings() {
70      return {
71        theme: this.container.data("theme") || "light",
72        position: this.container.data("position") || "inline",
73        typing: this.container.data("typing") === "true",
74        sound: this.container.data("sound") === "true",
75        animation: this.container.data("animation") || "fade",
76        streamResponse: true, // this.container.data("stream") === "true",
77      };
78    }
79
80    /**
81     * Bind event handlers
82     */
83    bindEvents() {
84      // Input events
85      this.elements.input.on("input", (e) => this.handleInputChange(e));
86      this.elements.input.on("keydown", (e) => this.handleKeyDown(e));
87      this.elements.input.on("paste", (e) => this.handlePaste(e));
88
89      // Send button
90      this.elements.sendBtn.on("click", (e) => this.handleSendClick(e));
91
92      // Suggestions
93      this.elements.suggestions.on("click", (e) =>
94        this.handleSuggestionClick(e)
95      );
96
97      // Quick actions
98      this.elements.quickActions.on("click", (e) => this.handleQuickAction(e));
99
100      // Error handling
101      this.elements.errorClose.on("click", (e) => this.hideError());
102
103      // Minimize/maximize
104      this.elements.minimizeBtn.on("click", (e) => this.toggleMinimize(e));
105
106      // Window events
107      // $(window).on("beforeunload", () => this.saveSessionToStorage());
108
109      // Emoji button (placeholder for future emoji picker)
110      this.elements.emojiBtn.on("click", (e) => this.handleEmojiClick(e));
111    }
112
113    /**
114     * Setup auto-resize for textarea and configure spell checking
115     */
116    setupAutoResize() {
117      // Configure spell checking attributes for better mobile experience
118      const input = this.elements.input[0];
119
120      // Enable spell checking
121      input.setAttribute("spellcheck", "true");
122
123      // Enable autocorrect and autocapitalize for better mobile experience
124      input.setAttribute("autocorrect", "on");
125      input.setAttribute("autocapitalize", "sentences");
126
127      // Enable autocomplete for better UX
128      input.setAttribute("autocomplete", "off");
129
130      // Set input mode for better mobile keyboard
131      input.setAttribute("inputmode", "text");
132
133      // Enable enter key hint for mobile
134      input.setAttribute("enterkeyhint", "send");
135
136      this.elements.input.on("input", () => {
137        input.style.height = "auto";
138        input.style.height = input.scrollHeight + 2 + "px";
139      });
140    }
141
142    /**
143     * Handle input change events
144     */
145    handleInputChange(e) {
146      const text = e.target.value;
147      const length = text.length;
148
149      // Update character counter
150      this.elements.charCount.text(length);
151
152      // Update counter styling
153      this.elements.charCount.removeClass("arc-warning arc-error");
154      if (length > 800) {
155        this.elements.charCount.addClass("arc-warning");
156      }
157      if (length > 950) {
158        this.elements.charCount.addClass("arc-error");
159      }
160
161      // Toggle send button
162      this.elements.sendBtn.prop("disabled", length === 0 || length > 1000);
163
164      // Hide suggestions when user starts typing
165      if (length > 0) {
166        this.hideSuggestions();
167      }
168    }
169
170    /**
171     * Handle keydown events
172     */
173    handleKeyDown(e) {
174      // Send on Enter (but not Shift+Enter)
175      if (e.key === "Enter" && !e.shiftKey) {
176        e.preventDefault();
177        this.sendMessage();
178      }
179
180      // Clear on Escape
181      if (e.key === "Escape") {
182        this.elements.input.val("").trigger("input");
183      }
184    }
185
186    /**
187     * Handle paste events
188     */
189    handlePaste(e) {
190      // Allow paste but check length after
191      setTimeout(() => {
192        const text = this.elements.input.val();
193        if (text.length > 1000) {
194          this.elements.input.val(text.substring(0, 1000));
195          this.showError("Message truncated to 1000 characters.");
196        }
197        this.elements.input.trigger("input");
198      }, 10);
199    }
200
201    /**
202     * Handle send button click
203     */
204    handleSendClick(e) {
205      e.preventDefault();
206      this.sendMessage();
207    }
208
209    /**
210     * Handle suggestion click
211     */
212    handleSuggestionClick(e) {
213      e.preventDefault();
214      const question = $(e.currentTarget).data("question");
215      if (question) {
216        this.elements.input.val(question).trigger("input");
217        this.sendMessage();
218      }
219    }
220
221    /**
222     * Handle quick action clicks
223     */
224    handleQuickAction(e) {
225      e.preventDefault();
226      const action = $(e.currentTarget).data("action");
227
228      switch (action) {
229        case "contact":
230          this.sendMessage("How can I contact Queen Victoria Market?");
231          break;
232        case "clear":
233          this.clearConversation();
234          break;
235        default:
236          this.log("Unknown quick action:", action);
237      }
238    }
239
240    /**
241     * Handle emoji button click (placeholder)
242     */
243    handleEmojiClick(e) {
244      e.preventDefault();
245      // Placeholder for emoji picker functionality
246      this.log("Emoji picker not implemented yet");
247    }
248
249    /**
250     * Send a message
251     */
252    async sendMessage(messageText = null) {
253      const message = messageText || this.elements.input.val().trim();
254
255      if (!message || this.isTyping) {
256        return;
257      }
258
259      // Close any existing EventSource connection before starting new one
260      if (this.currentEventSource) {
261        this.currentEventSource.close();
262        this.currentEventSource = null;
263      }
264
265      // Clear input and hide suggestions
266      if (!messageText) {
267        this.elements.input.val("").trigger("input");
268        this.hideSuggestions();
269      }
270
271      // Add user message to chat
272      this.addMessage(message, "user");
273
274      // Show typing indicator
275      this.showTypingIndicator();
276
277      if (this.settings.streamResponse) {
278        try {
279          await this.sendToServerToStartStream(message);
280          // Note: Don't hide typing indicator here - let the stream events handle it
281        } catch (error) {
282          this.handleError(error);
283          this.hideTypingIndicator();
284        }
285      } else {
286        try {
287          const response = await this.sendToServer(message);
288          this.handleServerResponse(response);
289        } catch (error) {
290          this.handleError(error);
291        } finally {
292          this.hideTypingIndicator();
293        }
294      }
295    }
296
297    /**
298     * Send message to server
299     */
300    async sendToServer(message) {
301      const formData = new FormData();
302      formData.append("action", "arc_chat_message");
303      formData.append("nonce", arc_chatbot.nonce);
304      formData.append("message", message);
305
306      if (this.sessionId) {
307        formData.append("session_id", this.sessionId);
308      }
309
310      const response = await fetch(arc_chatbot.ajax_url, {
311        method: "POST",
312        body: formData,
313        credentials: "same-origin",
314      });
315
316      if (!response.ok) {
317        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
318      }
319
320      return await response.json();
321    }
322
323    /**
324     * Handle server response
325     */
326    handleServerResponse(response) {
327      if (response.success) {
328        const data = response.data;
329
330        // Store session ID
331        if (data.session_id) {
332          this.sessionId = data.session_id;
333        }
334
335        // Add bot response
336        this.addMessage(data.response, "bot", {
337          searchResults: data.search_results_count || 0,
338          processingTime: data.processing_time || 0,
339          cached: data.cached_response || false,
340        });
341
342        // Play sound if enabled
343        if (this.settings.sound) {
344          this.playNotificationSound();
345        }
346      } else {
347        // Handle server error
348        const errorMessage =
349          response.data?.message || arc_chatbot.strings.error;
350        this.showError(errorMessage);
351
352        // Add error message to chat for certain errors
353        if (response.data?.code === "not_configured") {
354          this.addMessage(
355            "Sorry, I'm not properly configured yet. Please contact the site administrator.",
356            "bot"
357          );
358        }
359      }
360    }
361
362    /**
363     * Handle errors
364     */
365    handleError(error) {
366      this.log("Chat error:", error);
367
368      let errorMessage = arc_chatbot.strings.error;
369
370      if (error.message.includes("Failed to fetch")) {
371        errorMessage =
372          "Network error. Please check your connection and try again.";
373      } else if (error.message.includes("HTTP 500")) {
374        errorMessage = "Server error. Please try again in a few moments.";
375      } else if (error.message.includes("HTTP 429")) {
376        errorMessage =
377          "Too many requests. Please wait a moment before trying again.";
378      }
379
380      this.showError(errorMessage);
381      this.addMessage(
382        "Sorry, I encountered an error. Please try asking your question again.",
383        "bot"
384      );
385
386      if (this.currentEventSource) {
387        this.currentEventSource.close(); // Close on error
388      }
389
390      this.currentEventSource = null;
391
392      this.hideTypingIndicator();
393    }
394
395    /**
396     * Send message to server to start stream
397     */
398    async sendToServerToStartStream(message) {
399      const formData = new FormData();
400      formData.append("action", "arc_chat_message_init_stream");
401      formData.append("nonce", arc_chatbot.nonce);
402      formData.append("message", message);
403
404      if (this.sessionId) {
405        formData.append("session_id", this.sessionId);
406      }
407
408      try {
409        const response = await fetch(arc_chatbot.ajax_url, {
410          method: "POST",
411          body: formData,
412          credentials: "same-origin",
413        });
414
415        if (!response.ok) {
416          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
417        }
418
419        const responseData = await response.json();
420
421        if (responseData.success) {
422          const data = responseData.data;
423
424          // Store session ID and thread ID
425          if (data.session_id) {
426            this.sessionId = data.session_id;
427          }
428          // if (data.thread_id) {
429          //   this.threadId = data.thread_id;
430          // }
431
432          if (data.response) {
433            // Non-streaming response received
434            this.addMessage(data.response, "bot", {
435              searchResults: data.search_results_count || 0,
436              processingTime: data.processing_time || 0,
437              cached: data.cached_response || false,
438            });
439
440            // Play sound if enabled
441            if (this.settings.sound) {
442              this.playNotificationSound();
443            }
444
445            this.hideTypingIndicator();
446          } else {
447            // Start streaming
448            await this.sendToServerToReceiveStream(data.session_id, message);
449          }
450        } else {
451          // Handle server error
452          const errorMessage =
453            responseData?.data?.message || arc_chatbot.strings.error;
454          this.showError(errorMessage);
455          this.hideTypingIndicator();
456
457          // Add error message to chat for certain errors
458          if (responseData?.data?.code === "not_configured") {
459            this.addMessage(
460              "Sorry, I'm not properly configured yet. Please contact the site administrator.",
461              "bot"
462            );
463          }
464        }
465      } catch (error) {
466        console.error("Error in sendToServerToStartStream:", error);
467        this.handleError(error);
468        this.hideTypingIndicator();
469      }
470    }
471
472    // In your chat-interface.js file, replace the sendToServerToReceiveStream method with this:
473
474    async sendToServerToReceiveStream(sessionId, message) {
475      const params = new URLSearchParams({
476        action: "arc_chat_message_receive_stream",
477        nonce: arc_chatbot.nonce,
478        session_id: sessionId,
479        message: message,
480      });
481
482      const url = `${arc_chatbot.ajax_url}?${params.toString()}`;
483
484      // Create a message element for the bot response that we'll update
485      let botMessageEl;
486      let botMessageElText;
487      let fullResponse = "";
488
489      const sender = "bot";
490
491      // Start timer for processing time
492      const startTime = Date.now();
493      let initialResponseTime = 0;
494
495      // Store reference to 'this' for use in callbacks
496      const self = this;
497
498      try {
499        const evtSource = new EventSource(url);
500
501        // Store reference to close it later
502        self.currentEventSource = evtSource;
503
504        evtSource.onmessage = (message) => {
505          try {
506            // console.log("Received message:", message.data);
507
508            // Check for [DONE] message
509            if (message.data === "[DONE]") {
510              self.hideTypingIndicator();
511              evtSource.close();
512              self.currentEventSource = null;
513              return;
514            }
515
516            const dataWrapper = JSON.parse(message.data);
517            const event = dataWrapper.event;
518            const data = dataWrapper.data;
519
520            // console.log('Received event:', event, 'with data:', data);
521
522            switch (event) {
523              case "test":
524                console.log("Test event received:", data.message);
525
526                break;
527
528              case "response.created":
529                // console.log("Response created:", data);
530                break;
531
532              case "response.in_progress":
533                // console.log("Response in progress");
534                break;
535
536              case "response.output_item.added":
537                // console.log("Output item added:", data);
538                break;
539
540              case "response.output_text.delta":
541                if (data?.delta) {
542                  const chunk = data.delta;
543                  fullResponse += chunk;
544                  // console.log('Full response:', fullResponse);
545
546                  if (initialResponseTime === 0) {
547                    initialResponseTime = (
548                      (Date.now() - startTime) /
549                      1000
550                    ).toFixed(2);
551                  }
552
553                  if (!botMessageEl) {
554                    botMessageEl = self.createBotMessageElement();
555                    botMessageElText = botMessageEl.find(".arc-message-text");
556                  }
557                  self.updateBotMessageElement(
558                    botMessageEl,
559                    botMessageElText,
560                    fullResponse
561                  );
562                }
563                break;
564
565              case "message_completed":
566                if (data?.text) {
567                  fullResponse = data.text;
568                  if (!botMessageEl) {
569                    botMessageEl = self.createBotMessageElement();
570                    botMessageElText = botMessageEl.find(".arc-message-text");
571                  }
572                  self.updateBotMessageElement(
573                    botMessageEl,
574                    botMessageElText,
575                    fullResponse
576                  );
577                }
578                break;
579
580              case "response.completed":
581                // console.log("Response completed successfully");
582                self.hideTypingIndicator();
583
584                // IMPORTANT: Close the connection when done
585                evtSource.close();
586                self.currentEventSource = null;
587
588                if (self.settings.sound) {
589                  self.playNotificationSound();
590                }
591
592                // Calculate processing time
593                const processingTime = (
594                  (Date.now() - startTime) /
595                  1000
596                ).toFixed(2);
597
598                const metadata = {
599                  processingTime: processingTime,
600                  initialResponseTime: initialResponseTime,
601                };
602
603                if (sender === "bot") {
604                  self.addTooltipToMessage(botMessageEl, metadata);
605                }
606
607                self.saveMessageToHistory(fullResponse, sender, metadata);
608                break;
609
610              case "completed":
611                // Final completion event
612                self.hideTypingIndicator();
613                evtSource.close();
614                self.currentEventSource = null;
615                break;
616
617              case "error":
618                console.error("Stream error:", data.message);
619                evtSource.close(); // Close on error
620                self.currentEventSource = null;
621                self.handleError(new Error(data.message));
622                break;
623
624              default:
625                // console.log("Unhandled event:", event, data);
626                break;
627            }
628          } catch (error) {
629            console.error("Error parsing stream data:", error);
630            evtSource.close(); // Close on parse error
631            self.currentEventSource = null;
632            self.handleError(error);
633          }
634        };
635
636        evtSource.onerror = (error) => {
637          console.error("EventSource error:", error);
638
639          // Check if this is a connection error or server error
640          if (evtSource.readyState === EventSource.CLOSED) {
641            console.log("EventSource connection closed");
642          } else if (evtSource.readyState === EventSource.CONNECTING) {
643            console.log("EventSource reconnecting...");
644            // You might want to show a "reconnecting" message to user
645          }
646
647          // Close and cleanup on error
648          evtSource.close();
649          self.currentEventSource = null;
650          self.hideTypingIndicator();
651          self.handleError(new Error("Connection error"));
652        };
653
654        evtSource.onopen = (event) => {
655          // console.log("EventSource connection opened");
656        };
657      } catch (error) {
658        console.error("Stream setup error:", error);
659        this.handleError(error);
660        this.hideTypingIndicator();
661      }
662    }
663
664    // Helper method to create bot message element
665    createBotMessageElement() {
666      const timestamp = new Date();
667      const timeString = timestamp.toLocaleTimeString([], {
668        hour: "2-digit",
669        minute: "2-digit",
670      });
671
672      const messageEl = $(`
673    <div class="arc-message arc-message-bot" data-timestamp="${timestamp.getTime()}" style="display: none;">
674        <div class="arc-message-avatar">
675            <div class="arc-avatar-bot">🤖</div>
676        </div>
677        <div class="arc-message-content">
678            <div class="arc-message-bubble">
679                <div class="arc-message-text"></div>
680                <div class="arc-message-time">${timeString}</div>
681            </div>
682        </div>
683    </div>
684  `);
685
686      this.elements.messages.append(messageEl);
687      this.scrollToBottom();
688
689      return messageEl;
690    }
691
692    // Helper method to update bot message element
693    updateBotMessageElement(messageEl, textContainer, text) {
694      messageEl.show();
695      textContainer.html(this.formatBotMessage(text));
696      this.convertMarkdownHeadings(textContainer);
697      this.scrollToBottom();
698    }
699
700    /**
701     * Add message to chat
702     */
703    addMessage(text, sender, metadata = {}) {
704      const timestamp = new Date();
705      const timeString = timestamp.toLocaleTimeString([], {
706        hour: "2-digit",
707        minute: "2-digit",
708      });
709
710      // Create message element
711      const messageEl = $(`
712                <div class="arc-message arc-message-${sender}" data-timestamp="${timestamp.getTime()}">
713                    <div class="arc-message-avatar">
714                        <div class="arc-avatar-${sender}">
715                            ${sender === "bot" ? "🤖" : "👤"}
716                        </div>
717                    </div>
718                    <div class="arc-message-content">
719                        <div class="arc-message-bubble">
720                            <div class="arc-message-text"></div>
721                            <div class="arc-message-time">${timeString}</div>
722                        </div>
723                    </div>
724                </div>
725            `);
726
727      // Set message text (with basic HTML support for bot messages)
728      const textContainer = messageEl.find(".arc-message-text");
729      if (sender === "bot") {
730        // Allow basic HTML formatting in bot messages
731        textContainer.html(this.formatBotMessage(text));
732        this.convertMarkdownHeadings(textContainer);
733      } else {
734        textContainer.text(text);
735      }
736
737      // Add metadata tooltip for bot messages
738      if (sender === "bot") {
739        this.addTooltipToMessage(messageEl, metadata);
740      }
741
742      // Add to messages container
743      this.elements.messages.append(messageEl);
744
745      // Scroll to bottom
746      // this.scrollToBottom();
747      this.ScrollToTopOfLastMessage();
748
749      this.saveMessageToHistory(text, sender, metadata);
750    }
751
752    addTooltipToMessage(messageEl, metadata = {}) {
753      if (!messageEl) return;
754
755      // Add metadata tooltip for bot messages
756      if (Object.keys(metadata).length > 0) {
757        const tooltipInfo = [];
758        if (metadata.searchResults > 0) {
759          tooltipInfo.push(`${metadata.searchResults} sources found`);
760        }
761        if (metadata.initialResponseTime) {
762          tooltipInfo.push(
763            `${metadata.initialResponseTime}s initial response time`
764          );
765        }
766        if (metadata.processingTime) {
767          tooltipInfo.push(`${metadata.processingTime}s response time`);
768        }
769        if (metadata.cached) {
770          tooltipInfo.push("Cached response");
771        }
772
773        if (tooltipInfo.length > 0) {
774          // messageEl.attr("title", tooltipInfo.join(" • "));
775          messageEl.attr("data-info", tooltipInfo.join(" • "));
776        }
777      }
778    }
779
780    saveMessageToHistory(text, sender, metadata = {}) {
781      const timestamp = new Date();
782
783      // Store in history
784      this.messageHistory.push({
785        text: text,
786        sender: sender,
787        timestamp: timestamp.getTime(),
788        metadata: metadata,
789      });
790
791      // Limit history size
792      if (this.messageHistory.length > 50) {
793        this.messageHistory = this.messageHistory.slice(-40);
794      }
795
796      // Auto-save session
797      this.saveSessionToStorage();
798    }
799
800    /**
801     * Format bot message with HTML support and link styling
802     */
803    formatBotMessage(text) {
804      // Remove citations (content between 【 and 】)
805      text = this.removeCitations(text);
806
807      // Filter out incomplete markdown links during streaming
808      text = this.filterIncompleteMarkdown(text);
809
810      // First, convert any remaining markdown patterns that might have slipped through
811      text = this.convertMarkdownToHtml(text);
812
813      // Convert newlines to proper paragraphs
814      text = text.replace(/\n\n/g, "</p><p>");
815      text = text.replace(/\n/g, "<br>");
816      text = "<p>
816" + text + "</p>";
817
818      // Convert **bold** to <strong>
819      text = text.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
820
821      // Convert basic lists
822      text = text.replace(/^• (.+)$/gm, "<li>$1</li>");
823      text = text.replace(/(<li>.*<\/li>)/s, "<ul>$1</ul>");
824
825      // Style source links specifically
826      text = text.replace(
827        /📚 Related Pages:/g,
828        '<strong style="color: #667eea;">📚 Related Pages:</strong>'
829      );
830
831      // Ensure all links have proper styling
832      text = text.replace(/<a href="/g, '<a class="arc-response-link" href="');
833
834      return text;
835    }
836
837    /**
838     * Convert markdown headings (## or ###) to <h6> by walking the DOM
839     */
840    convertMarkdownHeadings(container) {
841      const el = container[0] || container;
842      const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
843      const nodesToReplace = [];
844
845      while (walker.nextNode()) {
846        const node = walker.currentNode;
847        if (/#{2,3}\s+/.test(node.textContent)) {
848          nodesToReplace.push(node);
849        }
850      }
851
852      nodesToReplace.forEach((node) => {
853        const text = node.textContent;
854        const match = text.match(/^(.*?)#{2,3}\s+(.+)$/);
855        if (match) {
856          const frag = document.createDocumentFragment();
857          if (match[1]) {
858            frag.appendChild(document.createTextNode(match[1]));
859          }
860          const h6 = document.createElement("h6");
861          h6.textContent = match[2];
862          frag.appendChild(h6);
863          const nextSibling = node.nextSibling;
864          node.parentNode.replaceChild(frag, node);
865          // Remove <br> immediately after the heading
866          if (nextSibling && nextSibling.nodeName === "BR") {
867            nextSibling.remove();
868          }
869        }
870      });
871    }
872
873    /**
874     * Wrap citations in spans with arc-citation class (JavaScript equivalent of PHP remove_citations)
875     */
876    removeCitations(text) {
877      if (!text || text.length === 0) {
878        return text;
879      }
880
881      // Wrap content between \u3010 (【) and \u3011 (】) in spans with arc-citation class
882      const cleanedText = text.replace(
883        /【([^】]*)】/g,
884        '<span class="arc-citation">【$1】</span>'
885      );
886
887      // Clean up any extra whitespace that might be left and trim
888      return cleanedText.trim();
889    }
890
891    /**
892     * Filter out incomplete markdown links during streaming
893     */
894    filterIncompleteMarkdown(text) {
895      if (!text || text.length === 0) {
896        return text;
897      }
898
899      // Remove [text](url that doesn't have a closing ) - remove everything
900      text = text.replace(/\[([^\]]+)\]\(([^)]*)$/g, "");
901
902      return text;
903    }
904
905    /**
906     * Convert any remaining markdown links to HTML (fallback)
907     */
908    convertMarkdownToHtml(text) {
909      // Handle [text](**url**) format
910      text = text.replace(
911        /\[([^\]]+)\]\(\*\*([^*]+)\*\*\)/g,
912        '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
913      );
914
915      // Handle standard [text](url) format
916      text = text.replace(
917        /\[([^\]]+)\]\(([^)]+)\)/g,
918        '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
919      );
920
921      // Handle [text](*url*) format
922      text = text.replace(
923        /\[([^\]]+)\]\(\*([^*]+)\*\)/g,
924        '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
925      );
926
927      return text;
928    }
929
930    /**
931     * Show typing indicator
932     */
933    showTypingIndicator() {
934      this.isTyping = true;
935      this.elements.typingIndicator.show();
936      this.scrollToBottom();
937    }
938
939    /**
940     * Hide typing indicator
941     */
942    hideTypingIndicator() {
943      this.isTyping = false;
944      this.elements.typingIndicator.hide();
945    }
946
947    /**
948     * Hide suggestions
949     */
950    hideSuggestions() {
951      this.container.find(".arc-suggestions-container").fadeOut(300);
952      this.container.find(".arc-suggestions-container").hide();
953    }
954
955    /**
956     * Show error message
957     */
958    showError(message) {
959      this.elements.errorText.text(message);
960      this.elements.errorContainer.show();
961
962      // Auto-hide after 5 seconds
963      setTimeout(() => {
964        this.hideError();
965      }, 10000);
966    }
967
968    /**
969     * Hide error message
970     */
971    hideError() {
972      this.elements.errorContainer.fadeOut(300);
973    }
974
975    /**
976     * Toggle minimize state
977     */
978    toggleMinimize(e) {
979      e.preventDefault();
980      this.container.toggleClass("arc-minimized");
981
982      if (this.container.hasClass("arc-minimized")) {
983        this.container
984          .find(".arc-chat-messages, .arc-chat-input-container")
985          .hide();
986      } else {
987        this.container
988          .find(".arc-chat-messages, .arc-chat-input-container")
989          .show();
990        this.scrollToBottom();
991      }
992    }
993
994    /**
995     * Clear conversation
996     */
997    clearConversation() {
998      if (confirm("Are you sure you want to clear this conversation?")) {
999        this.elements.messages
1000          .find(".arc-message:not(.arc-welcome-message)")
1001          .remove();
1002        this.messageHistory = [];
1003        this.sessionId = null;
1004        this.clearSessionStorage();
1005
1006        // Show suggestions again
1007        this.container.find(".arc-suggestions-container").fadeIn(300);
1008      }
1009    }
1010
1011    /**
1012     * Get conversation context
1013     */
1014    getConversationContext(limit = 3) {
1015      const recentMessages = this.messageHistory.slice(-limit * 2);
1016      return recentMessages
1017        .map((msg) => `${msg.sender}: ${msg.text}`)
1018        .join("\n");
1019    }
1020
1021    /**
1022     * Scroll to bottom of messages
1023     */
1024    scrollToBottom() {
1025      const messages = this.elements.messages[0];
1026      if (messages) {
1027        messages.scrollTop = messages.scrollHeight;
1028      }
1029    }
1030
1031    ScrollToTopOfLastMessage() {
1032      const messages = this.elements.messages[0];
1033      if (!messages) return;
1034
1035      // Find the last message element
1036      const lastMessage = this.elements.messages.find(".arc-message").last();
1037      if (lastMessage.length === 0) return;
1038
1039      messages.scrollTop = lastMessage[0].offsetTop;
1040    }
1041
1042    /**
1043     * Initialize sound effects
1044     */
1045    initSoundEffects() {
1046      // Create audio elements for sound effects
1047      this.sounds = {
1048        notification: new Audio(
1049          "data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgod
1049Dbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBjiSze3xgzQHFnLH8NyPPgkSZ7fy6qVSEwhNot8"
1050        ),
1051      };
1052    }
1053
1054    /**
1055     * Play notification sound
1056     */
1057    playNotificationSound() {
1058      if (this.sounds && this.sounds.notification) {
1059        this.sounds.notification.play().catch((e) => {
1060          // Ignore errors (e.g., user hasn't interacted yet)
1061        });
1062      }
1063    }
1064
1065    /**
1066     * Save session to localStorage
1067     */
1068    saveSessionToStorage() {
1069      if (!this.sessionId) return;
1070
1071      const sessionData = {
1072        sessionId: this.sessionId,
1073        messageHistory: this.messageHistory.slice(-20), // Save last 20 messages
1074        timestamp: Date.now(),
1075      };
1076
1077      try {
1078        localStorage.setItem(`arc_chat`, JSON.stringify(sessionData));
1079      } catch (e) {
1080        this.log("Failed to save session to localStorage:", e);
1081      }
1082    }
1083
1084    /**
1085     * Load session from localStorage
1086     */
1087    loadSessionFromStorage() {
1088      try {
1089        const stored = localStorage.getItem(`arc_chat`);
1090        if (!stored) return;
1091
1092        const sessionData = JSON.parse(stored);
1093
1094        // Check if session is not too old (24 hours) or if the session is from a different day
1095        const today = new Date();
1096        const todayTimestamp = today.getTime();
1097        const sessionDate = new Date(sessionData.timestamp);
1098
1099        if (
1100          todayTimestamp - sessionData.timestamp > 24 * 60 * 60 * 1000 ||
1101          sessionDate.getDate() !== today.getDate()
1102        ) {
1103          this.clearSessionStorage();
1104          return;
1105        }
1106
1107        this.sessionId = sessionData.sessionId;
1108
1109        // Restore message history (excluding welcome message)
1110        if (
1111          sessionData.messageHistory &&
1112          sessionData.messageHistory.length > 0
1113        ) {
1114          // Hide suggestions if there are messages
1115          this.hideSuggestions();
1116          this.messageHistory = sessionData.messageHistory;
1117
1118          // Restore messages to UI
1119          sessionData.messageHistory.forEach((msg) => {
1120            if (msg.sender && msg.text) {
1121              this.addMessageToUI(
1122                msg.text,
1123                msg.sender,
1124                msg.timestamp,
1125                msg.metadata || {}
1126              );
1127            }
1128          });
1129        }
1130      } catch (e) {
1131        this.log("Failed to load session from localStorage:", e);
1132        this.clearSessionStorage();
1133      }
1134    }
1135
1136    /**
1137     * Add message to UI only (without storing in history)
1138     */
1139    addMessageToUI(text, sender, timestamp, metadata = {}) {
1140      const timestampDate = new Date(timestamp);
1141      const timeString = timestampDate.toLocaleTimeString([], {
1142        hour: "2-digit",
1143        minute: "2-digit",
1144      });
1145
1146      const messageEl = $(`
1147                <div class="arc-message arc-message-${sender}">
1148                    <div class="arc-message-avatar">
1149                        <div class="arc-avatar-${sender}">
1150                            ${sender === "bot" ? "🤖" : "👤"}
1151                        </div>
1152                    </div>
1153                    <div class="arc-message-content">
1154                        <div class="arc-message-bubble">
1155                            <div class="arc-message-text"></div>
1156                            <div class="arc-message-time">${timeString}</div>
1157                        </div>
1158                    </div>
1159                </div>
1160            `);
1161
1162      const textContainer = messageEl.find(".arc-message-text");
1163      if (sender === "bot") {
1164        textContainer.html(this.formatBotMessage(text));
1165        this.convertMarkdownHeadings(textContainer);
1166      } else {
1167        textContainer.text(text);
1168      }
1169
1170      this.elements.messages.append(messageEl);
1171      this.scrollToBottom();
1172    }
1173
1174    /**
1175     * Clear session storage
1176     */
1177    clearSessionStorage() {
1178      try {
1179        localStorage.removeItem(`arc_chat`);
1180      } catch (e) {
1181        this.log("Failed to clear session storage:", e);
1182      }
1183    }
1184
1185    /**
1186     * Log debug messages
1187     */
1188    log(...args) {
1189      if (window.console && console.log) {
1190        console.log("[Arcadian Chatbot]", ...args);
1191      }
1192    }
1193
1194    /**
1195     * Destroy the chat instance
1196     */
1197    destroy() {
1198      this.saveSessionToStorage();
1199      this.container.off();
1200      $(window).off("beforeunload");
1201    }
1202  }
1203
1204  /**
1205   * jQuery Plugin
1206   */
1207  $.fn.arcadianChatbot = function (options) {
1208    return this.each(function () {
1209      if (!$(this).data("arcadian-chatbot")) {
1210        $(this).data("arcadian-chatbot", new ChatInstance(this));
1211      }
1212    });
1213  };
1214
1215  /**
1216   * Auto-initialize chatbots
1217   */
1218  $(document).ready(function () {
1219    // Initialize all chatbot containers
1220    $(".arc-chatbot-container").arcadianChatbot();
1221
1222    // Handle dynamic content
1223    $(document).on("DOMNodeInserted", ".arc-chatbot-container", function () {
1224      $(this).arcadianChatbot();
1225    });
1226  });
1227
1228  /**
1229   * Global utility functions
1230   */
1231  window.ArcadianChatbot.utils = {
1232    /**
1233     * Create a floating chatbot
1234     */
1235    createFloatingChat: function (options = {}) {
1236      const defaults = {
1237        theme: "light",
1238        welcomeMessage: "Hello! How can I help you today?",
1239        placeholder: "Ask me anything...",
1240        showBranding: true,
1241      };
1242
1243      const settings = Object.assign(defaults, options);
1244
1245      const chatHTML = `
1246                <div id="arc-floating-chat" 
1247                     class="arc-chatbot-container arc-theme-${settings.theme} arc-position-floating arc-rounded arc-shadow"
1248                     data-theme="${settings.theme}"
1249                     data-position="floating"
1250                     data-typing="true"
1251                     data-sound="false"
1252                     data-animation="fade">
1253                    <!-- Chat content would be generated here -->
1254                </div>
1255            `;
1256
1257      $("body").append(chatHTML);
1258      $("#arc-floating-chat").arcadianChatbot();
1259
1260      return $("#arc-floating-chat").data("arcadian-chatbot");
1261    },
1262
1263    /**
1264     * Get all active chat instances
1265     */
1266    getAllInstances: function () {
1267      const instances = [];
1268      $(".arc-chatbot-container").each(function () {
1269        const instance = $(this).data("arcadian-chatbot");
1270        if (instance) {
1271          instances.push(instance);
1272        }
1273      });
1274      return instances;
1275    },
1276
1277    /**
1278     * Destroy all chat instances
1279     */
1280    destroyAll: function () {
1281      this.getAllInstances().forEach((instance) => {
1282        instance.destroy();
1283      });
1284    },
1285  };
1286})(jQuery);

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.