PageSourceSearch

https://www.ketoheating.com/wp-content/plugins/wpwdyplugin/public/wp_virtual_assistant.js?ver=7.1.2

js ketoheating.com collected 2026-10-07 15:46:43 UTC 26,666 bytes, 711 lines download raw bytes

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.