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.