1ff-chat-ui
Version:
chatbot to communicate with taught ai
998 lines (913 loc) • 32.8 kB
JavaScript
import { io } from 'socket.io-client';
import {
chatMarkup,
getDisplayInfo,
imageMarkup,
initiatorProfile,
paymentHeader,
rolesHTML,
timeMarkup,
getPopUp,
videoMarkup,
} from './chat-widgets';
import { styles } from './styles';
import { assistant } from './config/assistant';
import { events } from './config/events';
import { roles } from './config/roles';
import { socketConfig } from './config/socket';
import { theme } from './config/theme';
import { translations } from './config/translations';
import cssMinify from './css-minify';
import {
constructLink,
extractStringWithBrackets,
getAnswerConfig,
getTerm,
getUserId,
initializeAddClassMethod,
isExpired,
splitText,
clearCarets,
getStringInAngleBrackets,
} from './helpers';
import { emailLoader, errorMessage, input, loadingDots, resendButton, scroll } from './utils';
import {
onChatHistory,
onConnect,
onDisconnect,
onStreamData,
onStreamEnd,
onStreamError,
onStreamStart,
socketEmitChat,
} from './socket-services';
import { actionService } from './action-service';
import { customEventTypes, standardEventTypes } from './custom/tracking-events';
import { experimentsPrompt } from '../../src/lib/config/prompts-affected';
const nodeEvents = require('events');
export const intentions = new nodeEvents.EventEmitter();
export const STORAGE_KEY = 'history';
export const CHAT_SEEN_KEY = 'chatSeen';
export const ALREADY_REGISTERED_KEY = 'showAlreadyRegisteredUser';
export const SHOW_PAYMENT_BUTTON_KEY = 'showPaymentButton';
export const SOCKET_IO_URL = 'http://localhost:5000';
export const UNSENT_MESSAGES_KEY = 'unsent';
export const GO_THROUGH_QUIZ_KEY = 'hasToGoThroughQuiz';
export const EXISTING_PRODUCT_LINK_KEY = 'existingProductLink';
export const intentionType = {
email: 'email_intent',
payment: 'payment_intent',
emailError: 'email_validation_error',
emailSuccess: 'email_validation_success',
};
const ChatUi = {
theme,
assistant,
events,
roles,
socketConfig,
translations,
socket: null,
elements: null,
userId: null,
term: null,
url: null,
link: null,
initialHeight: null,
containerId: 'chatbot-container',
typingEvents: [],
timeStart: null,
timerId: null,
typingTimerIds: [],
answersFromStream: '',
boldedText: '',
video: '',
showModalForRegisteredUser: '',
showPaymentButton: '',
lastQuestionData: {
term: '',
user_id: '',
message: '',
role: roles.user,
},
/**
* Initializes the chat module.
* @param {object} config - The configuration object.
* @example
* // Example usage:
* const config = {
* url: 'http://localhost:5000',
* assistantConfig: {
* image: 'https://assets.appsforfit.com/assets/avatars/practitioner-1.png',
* role: 'Lead Nutrition Expert, PhD',
* name: 'Jenny Wilson',
* welcome: 'Chat for 1 min, and get diet advise for free!',
* ctaTextContent: 'Customize Your Plan!',
* },
* customTheme: {
* '--lumina': '#f0f2f5',
* '--whisper': '#ffffff',
* '--seraph': '#21bb5a',
* '--ember': '#cacadb',
* '--zephyr': '43, 49, 57',
* '--enigma': '#FFAE19',
* },
* };
* init(config);
*/
init(config = {}) {
if (this.shouldHideChat()) {
this.closeSocket();
return;
}
this.setConfig(config);
this.setMessageObject();
this.setCustomVars();
this.setDomContent();
this.setSocket();
this.setIntentionEvents();
this.setImageFullScreen();
},
setImageFullScreen() {
document.querySelector('body').addEventListener('click', (e) => {
console.log('document', document);
if (e.target.classList.contains('media-image')) {
const fullScreen = document.querySelector('.fullscreen-background-filter');
fullScreen.querySelector('img').src = e.target.src;
fullScreen.classList.toggle('show-image');
}
});
},
setIntentionEvents() {
intentions.on(intentionType.emailError, (response) => {
emailLoader.hide();
this.elements.emailInput.disabled = false;
this.elements.sendButton.style.pointerEvents = 'auto';
this.lastQuestionData.message = '';
if (response.status === 409) {
this.track(customEventTypes.emailExist);
localStorage.setItem(ALREADY_REGISTERED_KEY, 'true');
this.showOptionsForRegisteredUser();
return;
}
if (response.status === 422) {
this.elements.errorEmail.textContent = response.errors.email[0];
this.elements.errorEmail.classList.remove('hidden');
this.track(customEventTypes.emailWrong);
}
});
intentions.on(intentionType.emailSuccess, () => {
this.lastQuestionData.message = this.elements.emailInput.value;
this.appendHtml({ role: roles.user, content: this.elements.emailInput.value });
socketEmitChat(this);
emailLoader.hide();
input.hide(this);
store.set('answers', { 'saved-email': this.elements.emailInput.value });
this.elements.emailInput.value = '';
this.elements.emailInput.addClass('hidden');
this.track(customEventTypes.emailEntered);
});
},
setConfig(config) {
this.url = config.url || SOCKET_IO_URL;
this.containerId = config.containerId || this.containerId;
// optional parts of the config that need to be initialized as object at least
config.assistantConfig = config.assistantConfig || {};
config.customTheme = config.customTheme || {};
config.translations = config.translations || {};
this.socketConfig = config.socketConfig || this.socketConfig;
this.theme = { ...this.theme, ...config.customTheme };
this.assistant = { ...this.assistant, ...config.assistantConfig };
this.mainContainer = document.getElementById(this.containerId);
this.translations = { ...this.translations, ...config.translations };
this.showModalForRegisteredUser = localStorage.getItem(ALREADY_REGISTERED_KEY);
this.showPaymentButton = localStorage.getItem(SHOW_PAYMENT_BUTTON_KEY);
this.hasToGoThroughQuiz = localStorage.getItem(GO_THROUGH_QUIZ_KEY);
},
setMessageObject() {
this.lastQuestionData.term = getTerm();
this.lastQuestionData.user_id = getUserId();
},
loadUserHistory(history) {
input.show(this);
this.elements.messageIncrementor.appendChild(timeMarkup(history[0].time));
this.historyTraverse(history);
const { content } = history.pop();
const lastMessageByAssistant = this.getLastMessageElement('.assistant');
this.link = lastMessageByAssistant.querySelector('a');
if (this.hasToGoThroughQuiz) {
this.showSuccessfulPaymentMessage();
return;
}
if (this.link) {
this.setCtaButton();
if(getTerm() === experimentsPrompt.finalPage){
this.link.remove();
}
}
if (content.includes(intentionType.email)) {
this.setEmailVisibility();
}
if (this.showModalForRegisteredUser) {
this.showOptionsForRegisteredUser();
}
if (content.includes(intentionType.payment) || this.showPaymentButton) {
this.setPaymentIntent();
}
},
historyTraverse(history) {
let counter = 1;
let isLastMessage = false;
history.forEach((data) => {
let appended = false;
if (counter > 1) {
let updatedContent = actionService.clearButtonCodes(data.content);
updatedContent = clearCarets(updatedContent);
data.content = updatedContent;
}
if (counter === history.length) {
isLastMessage = true;
}
if (counter === 1) {
if (data.content.includes('^')) {
appended = true;
this.initMedia(data.content);
this.initNewLine(data, isLastMessage);
}
}
if (!appended) {
this.appendHtml(data, isLastMessage);
this.initMedia(data.content);
}
counter++;
});
},
initNewLine(data, isLastMessage) {
const splittedContent = splitText(data.content, '^');
for (let i = 0; i < splittedContent.length; i++) {
data.content = splittedContent[i];
const newData = {
...data,
content: splittedContent[i],
};
this.appendHtml(newData, isLastMessage);
}
},
initMedia(content) {
const link = getStringInAngleBrackets(content);
const extractedLink = link[0];
if (extractedLink) {
this.appendMedia(extractedLink);
}
},
appendUnsentMessage() {
const data = {
content: localStorage.getItem(UNSENT_MESSAGES_KEY),
time: new Date().toISOString(),
role: roles.user,
};
this.appendHtml(data);
this.onError();
},
shouldHideChat() {
const { time } = this.getLastUserMessage();
let hasExpired;
if (time) {
hasExpired = isExpired(time);
}
// when time has expired chatSeen must be removed from localStorage
if (hasExpired) {
localStorage.removeItem(CHAT_SEEN_KEY);
}
// when user has clicked on ctaButton chatSeen is being set to true
const chatSeen = localStorage.getItem(CHAT_SEEN_KEY);
return chatSeen === 'true' ? true : false;
},
getLastUserMessage() {
const messages = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
const messageFound = messages.reverse().find((message) => message.role === roles.user);
const lastMessage = messageFound ? messageFound : {};
return lastMessage;
},
/**
* Closes the socket connection if it is open.
*
* @returns {void}
*/
closeSocket() {
this.socket && this.socket.close();
},
/**
* Initializes the socket connection with the server.
* It connects to the server using the specified URL and socket options.
* Sets up event listeners for all events defined
*
* @returns {void}
*/
setSocket() {
this.socket = io.connect(this.url, this.socketConfig);
this.socket.on(this.events.connect, onConnect.bind(this));
this.socket.on(this.events.disconnect, onDisconnect.bind(this));
this.socket.on(this.events.chatHistory, onChatHistory.bind(this));
this.socket.on(this.events.streamStart, onStreamStart.bind(this));
this.socket.on(this.events.streamData, onStreamData.bind(this));
this.socket.on(this.events.streamEnd, onStreamEnd.bind(this));
this.socket.on(this.events.streamError, onStreamError.bind(this));
// TODO do something on server error
// this.socket.on("error", (reason) => {});
},
processTextInCaseOfSquareBrackets() {
if (this.chunk.includes('[')) {
this.answersFromStream = this.chunk;
} else if (this.answersFromStream) {
this.answersFromStream += this.chunk;
}
if (this.answersFromStream.includes(']')) {
if (this.answersFromStream.includes(intentionType.payment)) {
this.setPaymentIntent();
return;
}
if (this.answersFromStream.includes(intentionType.email)) {
this.setEmailVisibility();
return;
}
this.answersFromStream = actionService.clearButtonCodes(this.answersFromStream);
this.addOptions();
this.chunk = '';
}
},
processTextInCaseOfCurlyBrackets() {
if (this.chunk.includes('{')) {
this.boldedText = this.chunk;
this.chunk = '';
}
if (this.boldedText || (this.boldedText && this.chunk.includes('}'))) {
this.boldedText += this.chunk;
this.chunk = '';
}
if (this.boldedText.includes('}')) {
this.link = constructLink(this.boldedText);
if (this.link) {
this.setCtaButton();
}
let strongTaggedText = this.boldedText.replace('{', '').replace('}', '');
strongTaggedText = `<strong>${strongTaggedText}</strong>`;
const lastMessageTextContainer = this.getLastMessageElement('.assistant .js-assistant-message');
lastMessageTextContainer.innerHTML += strongTaggedText;
this.boldedText = '';
}
},
processStringInCaseOfAngleBrackets() {
if (this.chunk.includes('<')) {
this.answersFromStream = this.chunk;
}
if (this.answersFromStream.includes('>')) {
this.answersFromStream = this.answersFromStream.replace('<', '').replace('>', '');
console.log('this.answersFromStream', this.answersFromStream);
this.appendMedia(this.answersFromStream);
this.answersFromStream = '';
this.chunk = '';
}
},
refreshLocalStorageHistory(history) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
},
singleChoice(e) {
let textContent = e.currentTarget.textContent;
this.lastQuestionData.message = textContent;
const data = {
role: roles.user,
content: textContent,
time: new Date().toISOString(),
};
if (this.isFirstUserMessage()) {
this.track(customEventTypes.firstMessage);
this.track(customEventTypes.buttonClick + e.currentTarget.dataset.sequence);
}
socketEmitChat(this);
this.appendHtml(data);
const answersContainer = document.querySelector('.answers-container');
if (answersContainer) {
answersContainer.remove();
}
},
addOptions() {
const element = this.getLastMessageElementConsistingMessage('.assistant');
const answerConfig = getAnswerConfig(this.answersFromStream);
const answersContainer = document.createElement('div');
const moveBtnNumber = '10';
answersContainer.classList.add('answers-container');
[...answerConfig.list].forEach((answer, index) => {
let optionElement = document.createElement('div');
optionElement.textContent = answer.content;
optionElement.setAttribute('data-sequence', (index + 1).toString());
optionElement = actionService.handleAction(optionElement, answer.actions, answersContainer);
optionElement.addEventListener('click', this[answerConfig.answersType].bind(this));
answersContainer.appendChild(optionElement);
});
this.answersFromStream = '';
[...answerConfig.list].forEach((btn) => {
if (!btn.actions.includes(moveBtnNumber)) {
element.appendChild(answersContainer);
}
});
},
getLastMessageElement(role) {
return this.elements.messageIncrementor.querySelectorAll(role)[
this.elements.messageIncrementor.querySelectorAll(role).length - 1
];
},
getLastMessageElementConsistingMessage(role) {
const parentElements = this.elements.messageIncrementor.querySelectorAll(role);
var lastElement = null;
for (let i = parentElements.length - 1; i >= 0; i--) {
const parent = parentElements[i];
const child1 = parent.querySelector('.js-assistant-message');
if (child1) {
lastElement = parent;
break; // Exit the loop as soon as a match is found
}
}
return lastElement;
},
isFirstUserMessage() {
let history = JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
return !history.find((obj) => {
return obj.role === roles.user;
});
},
/**
* It shows the CTA button, sets the href attribute to the link, and hides the prompt container.
*
* @param {string} link - The link to be set.
* @returns {void}
*/
setCtaButton() {
this.elements.ctaButton.classList.remove('hidden');
this.attachLinkEventListener();
if (getTerm() === experimentsPrompt.finalPage) {
this.setCtaButtonToClose();
} else {
this.elements.ctaButton.setAttribute('href', this.link);
}
input.hide(this);
this.track(customEventTypes.linkProvided);
this.elements.ctaButton.addEventListener('click', () => {
this.track(customEventTypes.linkClicked);
});
},
setCtaButtonToClose(){
this.elements.ctaButton.setAttribute('href', 'javascript:void(0)');
this.elements.ctaButton.addEventListener('click', (e) => {
e.preventDefault();
this.closeWidget();
});
},
attachLinkEventListener() {
const links = document.querySelectorAll('.js-assistant-message a');
const link = links[links.length - 1];
if (link) {
link.addEventListener('click', () => {
this.track(customEventTypes.linkClicked);
this.closeWidget();
});
}
},
/**
* Sets custom variables and applies them to the main container element and font family.
* Theme-specific properties are handled separately.
*
* @returns {void}
*/
setCustomVars() {
Object.entries(this.theme).forEach(([property, value]) => {
this.mainContainer.style.setProperty(property, value);
});
},
/**
* Sets the DOM content of the chat widget by creating and appending the necessary HTML elements,
* applying the provided styles, setting up the widget elements, and attaching event listeners.
*
* @returns {void}
*/
setDomContent() {
const style = document.createElement('style');
this.initialHeight = window.innerHeight;
style.textContent = cssMinify(styles(this.initialHeight));
this.mainContainer.classList.add('chat-container');
this.mainContainer.innerHTML += chatMarkup(this);
this.mainContainer.appendChild(style);
this.setElements();
this.elements.messageIncrementor.appendChild(initiatorProfile(this));
scroll.remove();
this.attachListeners();
initializeAddClassMethod();
},
setElements() {
this.elements = {
chatbotContainer: document.getElementById('chatbot-container'),
messageInput: document.getElementById('chat-prompt'),
sendButton: document.getElementById('send-button'),
messageIncrementor: document.getElementById('message-incrementor'),
closeButton: document.getElementById('close-widget'),
ctaButton: document.getElementById('cta-button'),
promptContainer: document.getElementById('prompt-container'),
loadingDots: document.querySelector('.js-wave'),
paymentButton: document.getElementById('chat-pay'),
paymentContainer: document.getElementById('primer-form-container'),
closePaymentButton: document.querySelector('.close-payment-form'),
paymentViewForm: document.getElementById('chat-payment-view'),
emailInput: document.getElementById('chat-email'),
errorEmail: document.querySelector('.js-error-email'),
paymentFormLoader: document.querySelector('.js-payment-form-loader'),
};
},
/**
* Appends the HTML content to the chat message container.
*
* @param {Object} data - The chat message data object.
* @returns {void}
*/
appendHtml(data, isLastMessage = false) {
const { role, content } = data;
const result = rolesHTML[role](content);
this.elements.messageIncrementor.appendChild(result.element || result);
this.setLastMessageButtons(result.extractedString, isLastMessage);
},
// logic with last message buttons should be refactored
setLastMessageButtons(extractedString, isLastMessage) {
if ([intentionType.payment, intentionType.email].includes(extractedString)) {
return;
}
if (extractedString && extractedString !== '' && isLastMessage) {
input.hide(this);
this.answersFromStream = '[' + extractedString + ']';
this.addOptions();
}
},
/**
* Sends the initial message to the socket server.
* If the socket is connected, it emits the chat event with the last question data.
* If the socket is disconnected, it appends the last question data to the local storage history.
*
* @returns {void}
*/
sendAssistantInitialMessage() {
this.assistant.initialMessage.content = this.formatInitialMessage(this.assistant.initialMessage.content);
const data = {
...this.lastQuestionData,
role: roles.assistant,
message: this.assistant.initialMessage.content,
};
this.socket.emit(this.events.chat, data);
},
/**
* Loads initial message from the assistant object and checks if the message contains any brackets.
* If it does, it extracts the string between the brackets and sets the initial message to the extracted string.
* It also adds the options to the chat widget.
* If the message doesn't contain any brackets, it sets the initial message to the message content.
* It also hides the input field and adds the options to the chat widget.media-image
*
* @returns {void}
*/
async loadAssistantInitialMessage() {
await this.loadInitialMessageSettings();
loadingDots.show();
this.sendAssistantInitialMessage();
await this.delay(1500);
loadingDots.hide();
const { extractedString, updatedMessage } = extractStringWithBrackets(this.assistant.initialMessage.content);
const link = getStringInAngleBrackets(this.assistant.initialMessage.content);
const extractedLink = link[0];
const data = {
content: updatedMessage,
...this.assistant.initialMessage,
};
this.elements.messageIncrementor.appendChild(timeMarkup(data.time));
if (extractedLink) {
this.appendMedia(extractedLink);
}
if (data.content.includes('^')) {
const splitMessage = splitText(data.content, '^');
await this.appendHtmlInChunks(splitMessage, data);
} else {
this.appendHtml(data);
}
// this.addImageAction();
if (extractedString) {
this.hideInput(extractedString);
}
},
async loadInitialMessageSettings() {
let actionCodes = actionService.getActionCodes(
this.assistant.initialMessage.content,
actionService.ACTION_CODE_REGEX,
);
let popUpHtml = getPopUp(actionCodes[0]);
if (popUpHtml) {
this.elements.chatbotContainer.appendChild(popUpHtml);
setTimeout(() => {
document.querySelector('.modal-wrapper').classList.add('hidden');
}, 3000);
await this.delay(3000);
}
},
/**
*
* @param {*} extractedLink
* appends the media (image/video) to the initial message
*/
appendMedia(extractedLink) {
const mediaBody = document.createElement('span');
mediaBody.classList.add('assistant');
if (extractedLink.includes('www.youtube.com')) {
mediaBody.appendChild(videoMarkup(extractedLink));
} else {
mediaBody.appendChild(imageMarkup(extractedLink));
}
this.elements.messageIncrementor.appendChild(mediaBody);
},
/**
* adds new message to lastQuestionData.message, clears the input field and visualizes it
*
* @returns {void}
*/
addNewMessage() {
if (!this.elements.emailInput.classList.contains('hidden')) {
this.emailSendHandler();
return;
}
if (this.isFirstUserMessage()) {
this.track(customEventTypes.firstMessage);
}
const content = this.elements.messageInput.value.trim();
this.typingHandler();
input.focus(this);
if (content === '') {
return;
}
const data = { role: roles.user, content, time: new Date().toISOString() };
const lastMessages = this.lastQuestionData.message ? this.lastQuestionData.message.split('\n') : [];
lastMessages.push(content);
this.lastQuestionData.message = lastMessages.join('\n');
this.appendHtml(data);
this.elements.messageInput.value = '';
},
/**
* Handles the error event by updating the last user message element to allow resending the message.
* It adds a click event listener to the element and removes the hidden class from the resend icon.
*
* @returns {void}
*/
onError() {
window.debugMode && console.log('onError: ', this);
loadingDots.hide();
errorMessage.show();
resendButton.hideAll();
const { content } = this.getLastUserMessage();
this.lastQuestionData.message = localStorage.getItem(UNSENT_MESSAGES_KEY) || content;
resendButton.show(this);
},
/**
* Retrieves the last user message element from the message incrementor.
* If the element exists, it clones and replaces it to ensure it is the latest instance.
*
* @returns {Element|null} - The last user message element if found, otherwise null.
*/
getLastUserMessageElement() {
const oldChild = this.getLastMessageElement('.user');
if (oldChild) {
const newLast = oldChild.cloneNode(true);
oldChild.parentNode.replaceChild(newLast, oldChild);
return newLast;
}
return null;
},
/**
* Closes the widget by clearing the main container, closing the socket connection,
* and setting the chat as seen in localStorage.
*
* @returns {void}
*/
closeWidget() {
this.mainContainer.innerHTML = '';
scroll.add();
this.closeSocket();
localStorage.setItem(CHAT_SEEN_KEY, true);
},
goBack() {
this.closeSocket();
history.back();
},
/**
* Attaches event listeners to the chat widget elements.
*
* @returns {void}
*/
attachListeners() {
this.elements.closeButton?.addEventListener('click', this.goBack.bind(this));
this.elements.sendButton.addEventListener('click', this.addNewMessage.bind(this));
this.elements.ctaButton.addEventListener('click', this.closeWidget.bind(this));
this.elements.messageInput.addEventListener('keydown', this.onKeyDown.bind(this));
this.elements.emailInput.addEventListener('keydown', this.onKeyDownEmail.bind(this));
this.elements.paymentButton.addEventListener('click', this.emitPaymentIntentions.bind(this));
window.onresize = this.onResize;
},
onResize() {
const element = document.querySelector('.chat-widget');
const windowHeight = window.innerHeight;
element.style.height = windowHeight + 'px';
},
/**
* Handles the keydown event and sends a message when the Enter key is pressed.
*
* @param {KeyboardEvent} event - The keydown event object.
* @returns {void}
*/
onKeyDown(event) {
if (event.key === 'Enter') {
event.preventDefault();
this.addNewMessage();
}
this.typingHandler();
},
typingHandler() {
const timerId = setTimeout(() => {
socketEmitChat(this);
}, 3000);
this.typingTimerIds.forEach((t) => clearTimeout(t));
this.typingTimerIds.push(timerId);
},
emailSendHandler() {
const data = this.getCurrentCustomerData();
emailLoader.show();
this.elements.emailInput.disabled = true;
this.elements.sendButton.style.pointerEvents = 'none';
intentions.emit(intentionType.email, data);
},
getCurrentCustomerData() {
const storedCustomerUuid = localStorage.getItem('__pd')
? JSON.parse(localStorage.getItem('__pd')).customerUuid
: null;
const data = {
email: this.elements.emailInput.value,
customerUuid: storedCustomerUuid || this.lastQuestionData.user_id,
};
return data;
},
showSuccessfulPaymentMessage() {
localStorage.setItem(GO_THROUGH_QUIZ_KEY, true);
const { element } = rolesHTML[roles.assistant](this.translations.tm1226);
this.elements.messageIncrementor.appendChild(element);
const last = this.getLastMessageElement('.assistant');
const answersContainer = document.createElement('div');
answersContainer.classList.add('answers-container');
this.link = '/';
this.elements.ctaButton.textContent = this.translations.tm530;
this.setCtaButton();
this.elements.ctaButton.addEventListener('click', () => {
localStorage.removeItem(GO_THROUGH_QUIZ_KEY);
});
// in case the user does not click on take the quiz button
setTimeout(() => {
this.elements.ctaButton.click();
}, 7000);
last.appendChild(answersContainer);
this.elements.paymentViewForm.addClass('hidden');
this.elements.paymentButton.addClass('hidden');
this.elements.sendButton.addClass('hidden');
input.hide(this);
},
showOptionsForRegisteredUser() {
const buttonLink = localStorage.getItem(EXISTING_PRODUCT_LINK_KEY);
const { element } = rolesHTML[roles.assistant](this.translations.tm716);
this.elements.messageIncrementor.appendChild(element);
const last = this.getLastMessageElement('.assistant');
const answersContainer = document.createElement('div');
answersContainer.classList.add('answers-container');
const continueToMyPlanButton = document.createElement('a');
continueToMyPlanButton.href = buttonLink;
const enterNewEmail = document.createElement('div');
continueToMyPlanButton.textContent = this.translations.tm526;
enterNewEmail.textContent = this.translations.tm715;
this.elements.emailInput.disabled = true;
this.elements.sendButton.style.pointerEvents = 'none';
enterNewEmail.addEventListener('click', () => {
this.elements.emailInput.value = '';
this.elements.emailInput.disabled = false;
this.elements.sendButton.style.pointerEvents = 'auto';
// NOTE: this message is not being send to socket until valid;
this.lastQuestionData.message = this.elements.emailInput.value;
answersContainer.remove();
this.appendHtml({ role: roles.user, content: this.translations.tm715 });
localStorage.removeItem(ALREADY_REGISTERED_KEY);
localStorage.removeItem(EXISTING_PRODUCT_LINK_KEY);
});
continueToMyPlanButton.addEventListener('click', () => {
this.lastQuestionData.message = this.translations.tm526;
localStorage.removeItem(ALREADY_REGISTERED_KEY);
localStorage.removeItem(EXISTING_PRODUCT_LINK_KEY);
});
last.appendChild(answersContainer);
answersContainer.appendChild(continueToMyPlanButton);
answersContainer.appendChild(enterNewEmail);
},
emitPaymentIntentions() {
this.elements.paymentButton.disabled = true;
localStorage.setItem(SHOW_PAYMENT_BUTTON_KEY, true);
intentions.emit(intentionType.payment, {
...this.elements,
paymentHeader,
onPaymentSuccess: this.showSuccessfulPaymentMessage.bind(this),
});
},
setPaymentIntent() {
// set chosen box
const lastMessageTextContainer = this.getLastMessageElement('.assistant .js-assistant-message');
const configuredOption = getDisplayInfo();
lastMessageTextContainer.innerHTML += configuredOption.price + ' ' + configuredOption.period;
input.hide(this);
//show payment button
this.elements.paymentButton.classList.remove('hidden');
this.elements.paymentButton.disabled = false;
this.answersFromStream = '';
this.chunk = '';
this.track(standardEventTypes.addToCart);
this.track(customEventTypes.priceSeen);
},
setEmailVisibility() {
this.elements.messageInput.addClass('hidden');
this.elements.emailInput.classList.remove('hidden');
this.answersFromStream = '';
this.chunk = '';
this.track(standardEventTypes.contact);
this.track(customEventTypes.emailField);
},
track(eventType) {
const event = tracking.event({
eventType: eventType,
systemType: backEndVars.systemType,
uri: window.location.pathname,
domain: window.location.hostname,
email: store.getAnswers()['saved-email'] || null,
phone: store.get('__ph') || null,
customerUuid: store.getCustomerUuid(),
additionalData: {},
utmParams: store.getMarketingInfo('lastUtmParams'),
});
tracking.trackClient(event);
trackEventInGTM(event);
},
onKeyDownEmail(event) {
if (event.key === 'Enter') {
event.preventDefault();
this.addNewMessage();
}
this.elements.errorEmail.addClass('hidden');
},
hideInput(extractedString) {
input.hide(this);
this.answersFromStream = extractedString;
this.addOptions();
},
//Returns a promise that is resolved after a given time parameter.
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
},
//An async function, that appends the splitted message in chunks, with a total delay of 1000ms for each chunk.
async appendHtmlInChunks(splitMessage, data) {
for (let i = 0; i < splitMessage.length; i++) {
loadingDots.show();
await this.delay(500);
const newData = {
...data,
content: splitMessage[i],
};
this.appendHtml(newData);
loadingDots.hide();
await this.delay(500);
}
},
//If the message contains "^" symbol and looks something like this "^^ ^^ Do^^ you ^^^want^ ^^to ^^^ lose weight [Yes|No]^ ^ ^^^
//and returs it like this "Do^ you^ want^ to^ lose weight [Yes|No]"
formatInitialMessage(initialMessage) {
if (initialMessage.includes('^')) {
const splittedTextArr = splitText(initialMessage, '^');
initialMessage = this.rebuildInitialMessage(splittedTextArr);
}
return initialMessage;
},
//Takes an array and builds a string from it, and append a caret sign (^) at the end of each element;
rebuildInitialMessage(splittedTextArr) {
let formattedText = '';
for (let i = 0; i < splittedTextArr.length; i++) {
if (splittedTextArr[i] === splittedTextArr[splittedTextArr.length - 1]) {
formattedText += splittedTextArr[i];
} else {
formattedText += splittedTextArr[i] + '^ ';
}
}
return formattedText;
},
};
export default ChatUi;