1function WpNANVirtualAssistant() { 2 3 var self = this; 4 5 const historyStorageKey = "wp_assistant_history_id"; 6 const userStorageKey = 'wp_assistant_user' 7 const previewDismissedStorageKey = "wp_assistant_preview_dismissed"; 8 const welcomeMessage = "Hi! Looking for a quote, have a question, or need help with something else?" 9 const defaultLauncherText = "Chat With Us"; 10 //const serverUrl = "http://assistant.od.com:8887/" 11 const serverUrl = "https://wpassistant.cossd.com/" 12 13 this.state = { 14 hostname: window.location.hostname.replace(/^www\./i, ""), 15 // hostname: "ccrvictoria.com", 16 history_id: null, 17 userId: null, 18 messages: [], 19 is_open: false, 20 conversation_revision: 0, 21 preview_dismissed: false, 22 first_question: welcomeMessage, 23 launcher_text: defaultLauncherText 24 }; 25 26 this.elements = {}; 27 28 this.init = function() { 29 if (document.querySelector("[data-wpva-widget]")) { 30 return; 31 } 32 this.createWidget(); 33 this.bindEvents(); 34 this.updateSendButton(); 35 this.applyHistoryId(); 36 this.applyOrCreateUser(); 37 this.applyMessages(); 38 this.maybeAutoOpen(); 39 }; 40 41 this.applyHistoryId = function(history_id= null) { 42 if (history_id) { 43 this._update_local_sorage(historyStorageKey, history_id); 44 this.state.history_id = history_id; 45 return; 46 } 47 this.state.history_id = history_id || this._get_from_local_storage(historyStorageKey); 48 } 49 50 this.applyOrCreateUser = function() { 51 const user_id = this._get_from_local_storage(userStorageKey); 52 if (user_id) { 53 this.state.user_id = user_id; 54 return; 55 } 56 let user_new_id = this._generate_user_id(); 57 this._update_local_sorage(userStorageKey, user_new_id); 58 this.state.user_id = user_new_id; 59 } 60 61 this.applyMessages = function() { 62 if (this.state.history_id) { 63 this._load_messages_from_server() 64 return 65 } 66 this._load_welcome_message() 67 } 68 69 this._get_default_welcome_message = function(content) { 70 return { 71 content: content || welcomeMessage, 72 role: "assistant" 73 } 74 } 75 76 this._visible_messages = function(messages) { 77 return (Array.isArray(messages) ? messages : []).filter(function(message) { 78 return message && ["assistant", "user", "action"].includes(message.role); 79 }); 80 } 81 82 this._with_welcome_message = function(messages, welcomeContent) { 83 const visible = this._visible_messages(messages); 84 const welcome = this._get_default_welcome_message(welcomeContent); 85 const first = visible[0]; 86 if ( 87 first && 88 first.role === "assistant" && 89 String(first.content || "").trim() === welcome.content.trim() 90 ) { 91 return visible; 92 } 93 return [welcome].concat(visible); 94 } 95 96 this._fetch_welcome_content = function() { 97 return this._fetch_widget_config().then(function(config) { 98 return config.first_question; 99 }); 100 } 101 102 this._fetch_widget_config = function() { 103 return fetch(serverUrl + "api/get-site-welcome", { 104 method: 'POST', 105 body: new URLSearchParams({hostname: this.state.hostname}) 106 }) 107 .then(response => response.json()) 108 .then(data => { 109 const payload = data && data.status === 'OK' && data.data ? data.data : {}; 110 const firstQuestion = typeof payload.first_question === 'string' && payload.first_question.trim() 111 ? payload.first_question.trim() 112 : welcomeMessage; 113 const launcherText = typeof payload.launcher_text === 'string' && payload.launcher_text.trim() 114 ? payload.launcher_text.trim() 115 : defaultLauncherText; 116 self.state.first_question = firstQuestion; 117 self.state.launcher_text = launcherText; 118 self.updatePreviewText(firstQuestion); 119 self.updateLauncherLabel(launcherText); 120 return { 121 first_question: firstQuestion, 122 launcher_text: launcherText 123 }; 124 }) 125 .catch(error => { 126 console.error("Error loading welcome message:", error); 127 return { 128 first_question: welcomeMessage, 129 launcher_text: defaultLauncherText 130 }; 131 }); 132 } 133 134 this._load_welcome_message = function() { 135 const conversationRevision = this.state.conversation_revision; 136 this.state.messages = [this._get_default_welcome_message()] 137 this.renderMessages(this.state.messages) 138 139 this._fetch_welcome_content() 140 .then(welcomeContent => { 141 if (conversationRevision !== self.state.conversation_revision || self.state.history_id) { 142 return; 143 } 144 self.state.messages = [self._get_default_welcome_message(welcomeContent)] 145 self.renderMessages(self.state.messages) 146 }); 147 } 148 149 this._load_messages_from_server = function() { 150 const conversationRevision = this.state.conversation_revision; 151 const post_params = { 152 'history_id': this.state.history_id, 153 'hostname': this.state.hostname, 154 'user_id': this.state.user_id 155 } 156 const historyRequest = fetch(serverUrl + "api/get-conversation-messages", { 157 method: 'POST', 158 body: new URLSearchParams(post_params) 159 }).then(response => response.json()); 160 161 Promise.all([historyRequest, this._fetch_welcome_content()]) 162 .then(function(results) { 163 if (conversationRevision !== self.state.conversation_revision) { 164 return; 165 } 166 const data = results[0]; 167 const welcomeContent = results[1]; 168 if (!data || data.status !== 'OK') { 169 self.state.history_id = null; 170 self._remove_from_local_storage(historyStorageKey); 171 self._load_welcome_message(); 172 return; 173 } 174 self.state.messages = self._with_welcome_message(data.messages, welcomeContent); 175 self.renderMessages(self.state.messages); 176 }) 177 .catch(error => { 178 console.error("Error loading messages from server:", error); 179 }); 180 } 181 182 this._send_reset_to_server = function () { 183 if (!this.state.history_id) return 184 let data = {history_id: this.state.history_id, 'reason': 'reset'} 185 let endpoint = serverUrl + "api/end-conversation-message"; 186 fetch(endpoint, {method: 'POST', body: new URLSearchParams(data)}) 187 } 188 189 this._push_va_event = function (eventType, eventData) { 190 var payload = { 191 eventType: eventType, 192 eventData: eventData || '' 193 }; 194 try { 195 window.dispatchEvent(new CustomEvent('wp-va-event', { detail: payload })); 196 } catch (error) { 197 console.error("Error dispatching VA analytics event:", error); 198 } 199 if (!window._wpVaEventsReady) { 200 window._wpVaEventQueue = window._wpVaEventQueue || []; 201 window._wpVaEventQueue.push(payload); 202 } 203 } 204 205 this._send_message_to_server = function (message) { 206 const writingIndicator = this.renderWritingIndicator(); 207 const conversationRevision = this.state.conversation_revision; 208 209 message['hostname'] = this.state.hostname; 210 if (this.state.history_id) { 211 message['history_id'] = this.state.history_id; 212 } 213 if (this.state.user_id) { 214 message['user_id'] = this.state.user_id; 215 } 216 let endpoint = serverUrl + "api/accept-conversation-message" 217 fetch(endpoint, {method: 'POST', body: new URLSearchParams(message)}) 218 .then(response => response.json()) 219 .then(data => { 220 writingIndicator.remove(); 221 if (conversationRevision !== self.state.conversation_revision) { 222 return; 223 } 224 if (data.status === "OK") { 225 self.applyHistoryId(data.data.history_id); 226 let new_message = data.data.message; 227 self.state.messages.push(new_message); 228 self.renderMessage(new_message); 229 self.scrollToLatestMessage(); 230 self._push_va_event('message'); 231 if (data.data && data.data.va_complete) { 232 self._push_va_event('complete', data.data.va_event_data || ''); 233 } 234 } else { 235 console.error("Error sending message to server:", data); 236 } 237 }) 238 .catch(error => { 239 writingIndicator.remove(); 240 console.error("Error sending message to server:", error); 241 }); 242 } 243 244 this._generate_user_id = function() { 245 let first_part = Date.now(); 246 let second_part = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) 247 return `${first_part}${second_part}`; 248 } 249 250 this._update_local_sorage = function(key, value) { 251 try { 252 window.localStorage.setItem(key, value); 253 } catch (error) { 254 console.error("Error updating local storage:", error); 255 } 256 } 257 258 this._get_from_local_storage = function(key) { 259 try { 260 return window.localStorage.getItem(key); 261 } catch (error) { 262 console.error("Error getting from local storage:", error); 263 return null; 264 } 265 } 266 267 this._remove_from_local_storage = function(key) { 268 try { 269 window.localStorage.removeItem(key); 270 } catch (error) { 271 console.error("Error removing value from local storage:", error); 272 } 273 } 274 275 this.getAppearance = function() { 276 const appearance = window.WpVaAppearance || {}; 277 const truthy = function(value, defaultValue) { 278 if (value === undefined || value === null || value === '') { 279 return defaultValue; 280 } 281 return value === true || value === 1 || value === "1" || value === "true"; 282 }; 283 return { 284 logoUrl: appearance.logo_url || "", 285 primaryColor: appearance.primary_color || "#ec4900", 286 textColor: appearance.text_color || "#ffffff", 287 autoOpen: truthy(appearance.auto_open, false), 288 showPreviewMessage: truthy(appearance.show_preview_message, true),
289 launcherBackgroundColor: appearance.launcher_background_color || "#000000", 290 launcherLabelColor: appearance.launcher_label_color || "#ffffff", 291 launcherIconBackgroundColor: appearance.launcher_icon_background_color || "#2b2b2b", 292 previewBackgroundColor: appearance.preview_background_color || "#ffffff", 293 previewTextColor: appearance.preview_text_color || "#405967", 294 chatIconUrl: appearance.chat_icon_url || "", 295 defaultAvatarUrl: appearance.default_avatar_url || "", 296 defaultAvatarKind: appearance.default_avatar_kind || "fallback" 297 }; 298 }; 299 300 this.applyAppearance = function() { 301 if (!this.elements.widget) { 302 return; 303 } 304 const appearance = this.getAppearance(); 305 this.elements.widget.style.setProperty("--wpva-primary", appearance.primaryColor); 306 this.elements.widget.style.setProperty("--wpva-user-text", appearance.textColor); 307 this.elements.widget.style.setProperty("--wpva-launcher-bg", appearance.launcherBackgroundColor); 308 this.elements.widget.style.setProperty("--wpva-launcher-label", appearance.launcherLabelColor); 309 this.elements.widget.style.setProperty("--wpva-launcher-icon-bg", appearance.launcherIconBackgroundColor); 310 this.elements.widget.style.setProperty("--wpva-preview-bg", appearance.previewBackgroundColor); 311 this.elements.widget.style.setProperty("--wpva-preview-text", appearance.previewTextColor); 312 this.updatePreviewVisibility(); 313 }; 314 315 this.maybeAutoOpen = function() { 316 if (!this.getAppearance().autoOpen) { 317 return; 318 } 319 const key = "wp_assistant_auto_open_at"; 320 const storedAt = parseInt(this._get_from_local_storage(key), 10); 321 const fourHours = 4 * 60 * 60 * 1000; 322 if (storedAt && Date.now() - storedAt < fourHours) { 323 return; 324 } 325 this._update_local_sorage(key, String(Date.now())); 326 this.open(); 327 }; 328 329 this.createElement = function(tagName, className, attributes) { 330 const element = document.createElement(tagName); 331 332 if (className) { 333 element.className = className; 334 } 335 336 Object.keys(attributes || {}).forEach(function(attribute) { 337 element.setAttribute(attribute, attributes[attribute]); 338 }); 339 340 return element; 341 }; 342 343 this.createAvatar = function() { 344 const appearance = this.getAppearance(); 345 const logoUrl = appearance.logoUrl; 346 const avatarUrl = logoUrl || appearance.defaultAvatarUrl; 347 const useFallbackStyle = !logoUrl && appearance.defaultAvatarKind !== 'site_icon'; 348 const avatar = this.createElement( 349 "span", 350 useFallbackStyle 351 ? "wpva-logo wpva-logo--image wpva-logo--default" 352 : "wpva-logo wpva-logo--image", 353 {"aria-hidden": "true"} 354 ); 355 if (avatarUrl) { 356 const image = this.createElement("img", "wpva-logo-image", { 357 src: avatarUrl, 358 alt: "" 359 }); 360 avatar.appendChild(image); 361 return avatar; 362 } 363 avatar.className = "wpva-logo"; 364 avatar.textContent = "N"; 365 return avatar; 366 }; 367 368 this.createLauncherIcon = function() { 369 const icon = this.createElement("span", "wpva-launcher-icon", {"aria-hidden": "true"}); 370 const chatIconUrl = this.getAppearance().chatIconUrl; 371 if (chatIconUrl) { 372 // Paint via CSS mask so the glyph uses the same color as launcher text 373 // (currentColor). <img> cannot inherit CSS color for SVG fills. 374 const glyph = this.createElement("span", "wpva-launcher-icon-image"); 375 glyph.style.setProperty("-webkit-mask-image", 'url("' + chatIconUrl + '")'); 376 glyph.style.setProperty("mask-image", 'url("' + chatIconUrl + '")'); 377 icon.appendChild(glyph); 378 } 379 return icon; 380 }; 381 382 this.updatePreviewText = function(text) { 383 if (!this.elements.previewText) { 384 return; 385 } 386 this.elements.previewText.textContent = text || welcomeMessage; 387 }; 388 389 this.updateLauncherLabel = function(text) { 390 const label = text || defaultLauncherText; 391 if (this.elements.launcherLabel) { 392 this.elements.launcherLabel.textContent = label; 393 } 394 if (this.elements.launcher && !this.state.is_open) { 395 this.elements.launcher.setAttribute("aria-label", label); 396 } 397 }; 398 399 this.isPreviewDismissed = function() { 400 return this.state.preview_dismissed || this._get_from_local_storage(previewDismissedStorageKey) === "1"; 401 }; 402 403 this.dismissPreview = function() { 404 this.state.preview_dismissed = true; 405 this._update_local_sorage(previewDismissedStorageKey, "1"); 406 this.updatePreviewVisibility(); 407 }; 408 409 this.updatePreviewVisibility = function() { 410 if (!this.elements.preview) { 411 return; 412 } 413 const appearance = this.getAppearance(); 414 const shouldShow = appearance.showPreviewMessage 415 && !this.state.is_open 416 && !this.isPreviewDismissed(); 417 this.elements.preview.hidden = !shouldShow; 418 this.elements.preview.setAttribute("aria-hidden", shouldShow ? "false" : "true"); 419 if (this.elements.widget) { 420 this.elements.widget.classList.toggle("wpva-widget--preview-visible", shouldShow); 421 } 422 }; 423 424 this.createWidget = function() { 425 this.state.preview_dismissed = this._get_from_local_storage(previewDismissedStorageKey) === "1"; 426 427 const widget = this.createElement("div", "wpva-widget", {"data-wpva-widget": ""}); 428 429 const preview = this.createElement("div", "wpva-preview", { 430 role: "status", 431 "aria-live": "polite" 432 }); 433 const previewClose = this.createElement("button", "wpva-preview-close", { 434 type: "button", 435 "aria-label": "Dismiss preview message" 436 }); 437 previewClose.textContent = "Ã"; 438 const previewBody = this.createElement("div", "wpva-preview-body"); 439 const previewAvatar = this.createAvatar(); 440 previewAvatar.className = (previewAvatar.className + " wpva-preview-avatar").trim(); 441 const previewText = this.createElement("p", "wpva-preview-text"); 442 previewText.textContent = this.state.first_question; 443 previewBody.appendChild(previewAvatar); 444 previewBody.appendChild(previewText); 445 preview.appendChild(previewClose);
446 preview.appendChild(previewBody); 447 448 const launcher = this.createElement("button", "wpva-launcher", { 449 type: "button", 450 "aria-label": defaultLauncherText, 451 "aria-expanded": "false", 452 "aria-controls": "wpva-dialog" 453 }); 454 const launcherLabel = this.createElement("span", "wpva-launcher-label"); 455 launcherLabel.textContent = defaultLauncherText; 456 launcher.appendChild(this.createLauncherIcon()); 457 launcher.appendChild(launcherLabel); 458 459 const dialog = this.createElement("section", "wpva-dialog", { 460 id: "wpva-dialog", 461 role: "dialog", 462 "aria-label": "Virtual assistant", 463 "aria-hidden": "true" 464 }); 465 const header = this.createElement("header", "wpva-header"); 466 const resetButton = this.createElement("button", "wpva-icon-button wpva-reset", { 467 type: "button", 468 "aria-label": "Restart conversation", 469 title: "Restart conversation" 470 }); 471 const closeButton = this.createElement("button", "wpva-icon-button wpva-close", { 472 type: "button", 473 "aria-label": "Close virtual assistant", 474 title: "Close" 475 }); 476 477 resetButton.textContent = "â»"; 478 closeButton.textContent = "Ã"; 479 header.appendChild(resetButton); 480 header.appendChild(closeButton); 481 482 const messages = this.createElement("div", "wpva-messages", { 483 role: "log", 484 "aria-live": "polite", 485 "aria-relevant": "additions" 486 }); 487 const composer = this.createElement("form", "wpva-composer"); 488 const input = this.createElement("input", "wpva-input", { 489 type: "text", 490 placeholder: "Type an answer", 491 "aria-label": "Message", 492 autocomplete: "off" 493 }); 494 const sendButton = this.createElement("button", "wpva-send", { 495 type: "submit", 496 "aria-label": "Send message", 497 disabled: "disabled" 498 }); 499 500 sendButton.textContent = "â¤"; 501 composer.appendChild(input); 502 composer.appendChild(sendButton); 503 504 dialog.appendChild(header); 505 dialog.appendChild(messages); 506 dialog.appendChild(composer); 507 widget.appendChild(dialog); 508 widget.appendChild(preview); 509 widget.appendChild(launcher); 510 511 this.elements = { 512 widget: widget, 513 preview: preview, 514 previewText: previewText, 515 previewClose: previewClose, 516 launcher: launcher, 517 launcherLabel: launcherLabel, 518 dialog: dialog, 519 resetButton: resetButton, 520 closeButton: closeButton, 521 messages: messages, 522 composer: composer, 523 input: input, 524 sendButton: sendButton 525 }; 526 this.applyAppearance(); 527 document.body.appendChild(widget); 528 }; 529 530 this.bindEvents = function() { 531 const assistant = this; 532 533 this.elements.launcher.addEventListener("click", function() { 534 assistant.toggle(); 535 }); 536 537 this.elements.previewClose.addEventListener("click", function(event) { 538 event.preventDefault(); 539 event.stopPropagation(); 540 assistant.dismissPreview(); 541 }); 542 543 this.elements.preview.addEventListener("click", function(event) { 544 if (event.target === assistant.elements.previewClose) { 545 return; 546 } 547 assistant.open(); 548 }); 549 550 this.elements.closeButton.addEventListener("click", function() { 551 assistant.close(); 552 }); 553 554 this.elements.resetButton.addEventListener("click", function() { 555 assistant.resetConversation(); 556 }); 557 558 this.elements.composer.addEventListener("submit", function(event) { 559 event.preventDefault(); 560 assistant.sendMessage(); 561 }); 562 563 this.elements.input.addEventListener("input", function() { 564 assistant.updateSendButton(); 565 }); 566 567 document.addEventListener("keydown", function(event) { 568 if (event.key === "Escape" && assistant.state.is_open) { 569 assistant.close(); 570 } 571 }); 572 }; 573 574 this.open = function() { 575 this.state.is_open = true; 576 this.elements.dialog.classList.add("wpva-dialog--open"); 577 this.elements.dialog.setAttribute("aria-hidden", "false"); 578 this.elements.launcher.setAttribute("aria-expanded", "true"); 579 this.elements.launcher.setAttribute("aria-label", "Close virtual assistant"); 580 this.updatePreviewVisibility(); 581 this._push_va_event('open'); 582 this.scrollToLatestMessage(); 583 584 window.setTimeout(function(assistant) { 585 if (assistant.state.is_open) { 586 assistant.elements.input.focus(); 587 } 588 }, 180, this); 589 }; 590 591 this.close = function() { 592 this.state.is_open = false;
593 this.elements.dialog.classList.remove("wpva-dialog--open"); 594 this.elements.dialog.setAttribute("aria-hidden", "true"); 595 this.elements.launcher.setAttribute("aria-expanded", "false"); 596 this.elements.launcher.setAttribute( 597 "aria-label", 598 this.state.launcher_text || defaultLauncherText 599 ); 600 this.updatePreviewVisibility(); 601 this.elements.launcher.focus(); 602 }; 603 604 this.toggle = function() { 605 if (this.state.is_open) { 606 this.close(); 607 return; 608 } 609 610 this.open(); 611 }; 612 613 this.resetConversation = function() { 614 if (!this.state.history_id) return 615 this._send_reset_to_server() 616 this.state.conversation_revision += 1; 617 this.state.history_id = null; 618 this._remove_from_local_storage(historyStorageKey); 619 this._load_welcome_message(); 620 this.elements.input.value = ""; 621 this.updateSendButton(); 622 }; 623 624 this.renderMessages = function(messages) { 625 this.elements.messages.replaceChildren(); 626 627 (Array.isArray(messages) ? messages : []).forEach(function(message) { 628 this.renderMessage(message); 629 }, this); 630 631 this.scrollToLatestMessage(); 632 }; 633 634 this.renderMessage = function(message) { 635 if (!message || !["assistant", "user", "action"].includes(message.role)) { 636 return; 637 } 638 const item = this.createElement("article", "wpva-message wpva-message--" + message.role); 639 const content = this.createElement("div", "wpva-message-content"); 640 const bubble = this.createElement("div", "wpva-message-bubble"); 641 const isAssistant = message.role === "assistant"; 642 const text = this.createElement(isAssistant ? "div" : "p", "wpva-message-text"); 643 if (isAssistant) { 644 text.innerHTML = window.WpVaMarkdown.format(message.content); 645 } else { 646 text.textContent = String(message.content || ""); 647 } 648 bubble.appendChild(text); 649 content.appendChild(bubble); 650 651 if (message.role === "assistant") { 652 item.appendChild(this.createAvatar()); 653 } 654 655 item.appendChild(content); 656 this.elements.messages.appendChild(item); 657 }; 658 659 this.renderWritingIndicator = function() { 660 const item = this.createElement( 661 "article", 662 "wpva-message wpva-message--assistant wpva-message--writing", 663 {role: "status", "aria-label": "Assistant is writing"} 664 ); 665 const content = this.createElement("div", "wpva-message-content"); 666 const bubble = this.createElement("div", "wpva-message-bubble"); 667 const text = this.createElement("p", "wpva-message-text"); 668 669 text.textContent = "assistant writing...."; 670 bubble.appendChild(text); 671 content.appendChild(bubble); 672 item.appendChild(content); 673 this.elements.messages.appendChild(item); 674 this.scrollToLatestMessage(); 675 676 return item; 677 }; 678 679 this.sendMessage = function() { 680 const text = this.elements.input.value.trim(); 681 if (!text) { 682 this.updateSendButton(); 683 return; 684 } 685 const message = { content: text, role: "user" }; 686 687 this.state.messages.push(message); 688 this.renderMessage(message); 689 this.elements.input.value = ""; 690 this.updateSendButton(); 691 this.scrollToLatestMessage(); 692 this.elements.input.focus(); 693 this._send_message_to_server(message); 694 }; 695 696 this.updateSendButton = function() { 697 this.elements.sendButton.disabled = !this.elements.input.value.trim(); 698 }; 699 700 this.scrollToLatestMessage = function() { 701 window.requestAnimationFrame(function(messagesElement) { 702 messagesElement.scrollTop = messagesElement.scrollHeight; 703 }.bind(null, this.elements.messages)); 704 }; 705} 706 707document.addEventListener("DOMContentLoaded", function() { 708 const assistant = new WpNANVirtualAssistant(); 709 assistant.init(); 710 window.wpVirtualAssistant = assistant; 711});
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.