with a hidden input value.\n * @param {{type: String, title: String, keyBackToKore: String, editable: Boolean, label: String}} input\n */\n generateNameField(input) {\n if (input.editable) {\n return
\n \n \n
\n }\n else {\n return
\n
{input.title}
\n
{input.label}
\n
\n
\n }\n }\n\n /**\n * Renders the @type {AdvancedFormMessage} message template. If the template has been\n * interacted with in the current chat session OR is returned in the chat history,\n * so not render out the form portion.\n */\n render() {\n if (this.props.message.isChatHistory || this.state.formSubmitted) {\n return
\n {this.generateSenderMessage()}\n
\n
\n {this.props.payload.title}\n
\n
\n {this.props.payload.description}\n
\n
\n
\n }\n return (\n
\n {this.generateSenderMessage()}\n
\n
\n {this.props.payload.title}\n
\n
\n {this.props.payload.description}\n
\n
\n
\n
\n );\n }\n}","import React from \"react\";\nimport Message from \"./Message\";\n\n/**\n * Class to display a live agent chat message.\n *\n * This class relies on an additional payload property set from chat-bot.jsx\n * which determines if it is a live agent chat. This allows the chat message\n * window to display the live agent's name over the bot name.\n */\nexport default class LiveAgentMessage extends Message {\n render() {\n return (\n
\n {this.generateSenderMessage(this.props.payload.agent_name)}\n
\n
\n
\n );\n }\n}","import Message from \"./Message.jsx\";\nimport React from 'react';\n\n/**\n * Class to display the default template not mapped response for when a template object\n * is received, but there is not appropriate class to map it to.\n */\nexport default class TemplateNotMappedMessage extends Message {\n render() {\n return (\n
\n {this.generateSenderMessage()}\n
\n Sorry, I currently have no way to retrieve an answer for you on that topic. For assistance, please contact a customer service representative at
(800) 480-2265.\n
\n
\n )\n }\n}","import React from \"react\";\nimport FormComponent from \"./Components/FormComponent\";\n\nexport default class PostLiveChatSurveyFrom extends FormComponent {\n /**\n * Determines if the field is required and if it is optional, returns a hidden input with an empty\n * value defined. This is required by Kore as they state they cannot handle a missing input they sent\n * and need an empty string at minimum. This should be placed BEFORE the mapped radio group as any\n * selection made after will overwrite this in the form submit handling.\n * @param {Object} field - The form input field being evaluated.\n * @returns Returns a hidden input to mask optional fields if needed.\n */\n createOptionalHidden(field) {\n if (field.required === 'true') {\n return;\n }\n\n return
\n }\n\n /**\n * Renders the collection of radio input objects per input group passed through from\n * the bot.\n * @param {Object} field The object that defines the radio grouping\n * @param {Number} index The current index of the radio group being rendered.\n * @returns The rendered collection of the radio input input grouping.\n */\n createRadioGroup(field, index) {\n return
\n
\n {field.label}\n
\n {this.createOptionalHidden(field)}\n {field.options.map((radio, index) => {\n return this.createRadioInput(field, radio, index)\n })}\n
\n }\n\n /**\n * Renders out the singular radio input from the form data. The main determination is against\n * if the field is optional or not. We then add the required attribute in the input based\n * on that determination.\n * @param {Object} field The object that defines the radio grouping\n * @param {Object} radio The radio input object that is being rendered in the radio group.\n * @param {Number} index The current index of the radio group being rendered.\n * @returns\n */\n createRadioInput(field, radio, index) {\n if (field.required === 'true') {\n return
\n \n \n
\n }\n\n return
\n \n \n
\n }\n\n render() {\n if (this.props.message.isChatHistory || this.state.formSubmitted) {\n return (\n
\n {this.generateSenderMessage()}\n
\n
\n {this.props.payload.heading}\n
\n
\n {this.props.payload.description}\n
\n
\n
\n )\n }\n return (\n
\n {this.generateSenderMessage()}\n
\n
\n {this.props.payload.heading}\n
\n
\n {this.props.payload.description}\n
\n
\n
\n
\n );\n }\n}","import React, {createRef} from 'react';\nimport Message from './Message.jsx';\nimport ChatEventCodes from '../utils/ChatEventCodes.js';\n\n/**\n * Class to display the login message template.\n */\nexport default class LoginMessage extends Message {\n constructor(props) {\n super(props);\n\n this.state = {\n userInteracted: false\n }\n this.messageRef = createRef();\n }\n\n componentDidMount() {\n super.componentDidMount();\n this.setEventListeners();\n }\n\n /**\n * Set the event listener on the message if the user clicks the login link.\n */\n setEventListeners() {\n if (this.props.message.isChatHistory || this.state.userInteracted || !this.messageRef?.current) {\n return;\n }\n this.messageRef.current.addEventListener('click', (/** @type {MouseEvent} */ e) => {\n e.preventDefault();\n if (!e.target.classList.contains('login-link')) {\n return;\n }\n this.setState({userInteracted: true}, () => {\n document.dispatchEvent(new MessageEvent(ChatEventCodes.LoginFlowStarted, {data: this.props.payload.loginUrl}));\n });\n });\n }\n\n render() {\n if (this.props.message.isChatHistory || this.state.userInteracted) {\n return
\n }\n\n return
\n {this.generateSenderMessage()}\n
\n
\n }\n}","import React from 'react';\nimport Message from './Message.jsx';\n\n/**\n * Class to display the message that returns when the callback request\n * was a success and confirms the user's informaiton.\n */\nexport default class MindfulCallbackSucessMessage extends Message {\n constructor(props) {\n super(props);\n }\n\n /**\n * Takes the appojntmentTime value and coverts it to a readable date string.\n * If the value passed from appointmentTime creates an invalid date, then\n * return the initial value.\n *\n * This can occur either if the appointmentTime selected was ASAP, or\n * if the value passed would not create a valid date.\n */\n getTimeValue() {\n if (isNaN(this.props.payload.appointmentTime * 1000)) {\n return this.props.payload.appointmentTime;\n }\n const date = new Date(this.props.payload.appointmentTime * 1000);\n return `${date.toLocaleDateString('en-US')} at ${date.toLocaleTimeString('en-US', {hour: 'numeric', minute: '2-digit'})}`\n }\n\n render() {\n return(\n
\n {this.generateSenderMessage()}\n
\n
\n );\n }\n}","import React from \"react\";\nimport ButtonMessage from \"./ButtonMessage.jsx\";\nimport DropdownMessage from \"./DropdownMessage.jsx\";\nimport ListViewMessage from \"./ListViewMessage.jsx\";\nimport QuickRepliesMessage from \"./QuickRepliesMessage.jsx\";\nimport SearchMessage from \"./SearchMessage.jsx\";\nimport TextMessageTemplate from \"./TextMessageTemplate.jsx\";\nimport TimeSlotTemplate from \"./TimeSlotMessage.jsx\";\nimport UserLiveChatMessage from \"./UserLiveChatMessage.jsx\";\nimport FormMessage from \"./FormMessage.jsx\";\nimport BotSurveyFeedbackMessage from \"./BotSurveyFeedbackMessage.jsx\";\nimport AdvancedFormMessage from \"./AvancedFormMessage.jsx\";\nimport LiveAgentMessage from \"./LiveAgentMessage.jsx\";\nimport TemplateNotMappedMessage from \"./TemplateNotMappedMessage.jsx\";\nimport PostLiveChatSurveyFrom from \"./PostLiveChatSurveyForm.jsx\";\nimport LoginMessage from \"./LoginMessage.jsx\";\nimport MindfulCallbackSucessMessage from \"./MindfulCallbackSuccessMessage.jsx\";\n\n/**\n * Class that handles the mapping of all message types, both if passed through normally,\n * or as the rendering of chat history.\n */\nexport default class Messages extends React.Component {\n constructor(props) {\n super(props);\n\n this.isPreviousHistoryInteractable = false;\n }\n\n /**\n * Maps the chat history messages as received from the DDaS web socket service.\n *\n * We do need to know in this context if a previous message in history was of an interactable\n * type. This is due to the fact that as the customer is interacting live with the bot in their\n * initial chat, we have message types where what we send to the bot is not what we show as\n * the user's chat bubble. This means however, when history comes back from the bot, it returns\n * as the hidden payload instead of the nice customer message we initially displayed.\n *\n * A good example of this is the timeslot template where the payload we send to the bot initially\n * is in seconds since epoch for the API return to utilize to schedule a call. This payload then\n * is returned in seconds since as the history response. If we don't re-map this, then history will\n * simply print out the seconds. Instead each interactable message type has a mapper specifically\n * to print out the more friendly message. In the instance of timeslot, we send through the seconds,\n * convert it to a date and then a string such as \"Call on DATE at TIME\". That is sent back to this and\n * rendered out as a text message template.\n *\n * @param {Object} message The message object being mapped.\n * @param {Number} index The current index of the message collection being mapped.\n * @returns A mapped message or template to display in the chat window.\n */\n mapChatHistoryMessages(message, index) {\n /**\n * Checking against user should weed out some bot returns where certain templates can sometimes\n * be truly interactable but use the same format. A user response shouldn't be represented as anything\n * other than a text return in current state. This should let concurrent bot responses flow to the\n * general template mapping logic.\n */\n if (this.isPreviousHistoryInteractable && index > 0 && message.messageSender === 'user') {\n this.isPreviousHistoryInteractable = false;\n const previousMessage = this.props.messages[index - 1];\n const maskedTextReturn = this.mapInteractiveChatHistoryString(previousMessage, previousMessage.payload.payload, message.payload);\n return
\n }\n this.isPreviousHistoryInteractable = false;\n if ((message.messageSender === 'user' || message.messageSender === 'bot') && (typeof message.payload === 'string' || message.payload instanceof String)) {\n return
\n }\n if ((message.messageSender === 'user' || message.messageSender === 'bot') && (message.payload?.text && (typeof message.payload.text === 'string' || message.payload.text instanceof String))) {\n return
\n }\n if (message.payload.type === 'template') {\n return this.mapTemplates(message, message.payload.payload, true);\n }\n }\n\n /**\n * Maps the interactive message types using their respective static masking methods.\n */\n mapInteractiveChatHistoryString(message, payload, currentValue) {\n if (message?.payload?.type !== 'template') {\n return '';\n }\n\n switch(payload.template_type) {\n case 'button':\n return ButtonMessage.getMaskedHistory(payload, currentValue);\n case 'dropdown_template':\n return '';\n case 'listView':\n return ListViewMessage.getMaskedHistory(payload, currentValue);\n case 'quick_replies':\n case 'quick_replies_welcome':\n return QuickRepliesMessage.getMaskedHistory(payload, currentValue);\n case 'timeSlotTemplate':\n return TimeSlotTemplate.getMaskedHistory(currentValue);\n default:\n return '';\n }\n }\n\n /**\n * Maps the message sent through to the correct type.\n */\n mapMessages(message) {\n if (message.messageSender === 'user') {\n return
\n }\n\n const payload = message.payload;\n /**TODO: This should be set to an ErrorMessageTemplate once created. */\n if (payload.type === 'botKitUnreachable') {\n return
\n }\n if (payload.type === 'WebSocketUnavailable' || payload.type === 'WebsocketError') {\n return
\n }\n if ((!payload.component?.type) || !payload.component.payload) {\n return
\n }\n\n const component = payload.component;\n if ((component.type === 'text' || component.type === 'template') && (component && component.payload && component.payload.text)) {\n return
\n }\n\n /**\n * There is a new odd double nested text message returning which wasn't being handled properly and was falling\n * through to the template mapping logic which failed on template type checking.\n */\n if (component.payload?.component?.payload && (typeof component.payload.component.payload === 'string' || component.payload.component.payload instanceof String)) {\n return
\n }\n\n if (component.type === 'template' && (typeof component.payload === 'string' || component.payload instanceof String)) {\n return
\n }\n\n if (component.type === 'template') {\n return this.mapTemplates(message, component.payload.payload);\n }\n }\n\n /**\n * Maps the template message to their respective concrete types.\n * @param {Object} message The message object constructed in the message handler of chatbot.jsx.\n * @param {Object} payload The payload of the message from the Kore response.\n * @param {Boolean} isMappingChatHistory A flag for when mapping chat history to indicate that the current\n * template being mapped is either interactable or not. Used the mask the chat history user response\n * if needed as described in mapChatHistoryMessages().\n * @returns A mapped template or a default message if the template type received isn't supported yet.\n */\n mapTemplates(message, payload, isMappingChatHistory = false) {\n switch (payload.template_type) {\n case 'advancedFormTemplate':\n return
\n case 'button':\n this.isPreviousHistoryInteractable = isMappingChatHistory;\n return
\n case 'botSurveyFeedbackTemplate':\n return
\n case 'dropdown_template':\n this.isPreviousHistoryInteractable = isMappingChatHistory;\n return
\n case 'form_template':\n return
\n case 'login':\n return
\n case 'listView':\n this.isPreviousHistoryInteractable = isMappingChatHistory;\n return
\n case 'liveAgentMessage':\n message.messageSender = payload.agent_name;\n message.agentChat = true;\n return
\n case 'mindfulCallbackSuccessTemplate':\n return
\n case 'postLiveChatSurveyTemplate':\n return
\n case 'quick_replies':\n case 'quick_replies_welcome':\n this.isPreviousHistoryInteractable = isMappingChatHistory;\n return
\n case 'search':\n return
\n case 'timeSlotTemplate':\n this.isPreviousHistoryInteractable = isMappingChatHistory;\n return
\n case 'userLiveChatTemplate':\n return
\n default:\n return
\n }\n }\n\n render() {\n return (\n
\n {this.props.messages.map((message, index) => {\n if (message.isChatHistory) {\n return this.mapChatHistoryMessages(message, index);\n }\n else {\n return this.mapMessages(message)\n }\n })}\n
\n );\n }\n}","import ChatEventCodes from \"./ChatEventCodes\";\n/**\n * A class to encapsulate the functionality of the websocket functions needed\n * to connect, interact, reconnect, and close chat with the Kore system.\n */\nexport default class KoreWebSocketManager {\n\n /**\n * Inititalizes a new instance of @typedef KoreWebSocketManager\n * @param {String} taskBotId The ID of the bot being connected to.\n * @param {String} apiRootUrl The API root for the WSS request.\n * @param {Function} renderChatHistory The parent class function to render chat history before making a connection.\n */\n constructor(taskBotId, apiRootUrl, renderChatHistory) {\n // This is still us assuming we are using our in-house service.\n this.wssApiUrl = `${apiRootUrl}/ddas-chatauthz/1.0/ddas-chatauthorizations/public`;\n /** @type {String} */\n this.url = '';\n /** @type {WebSocket} */\n this.webSocket = null;\n this.botInfo = {\n //'chatBot': chatBot,\n 'taskBotId': taskBotId\n };\n\n /**@type {Boolean} */\n this.isConnected = false;\n /**@type {Boolean} */\n this.isReconnecting = false;\n /** @type {Number} */\n this.pingTimer = 30000;\n /** @type {Number} */\n this.maxPongWait = 5000;\n this.pingIntervalId = undefined;\n this.pongTimer = undefined;\n this.pingTimeoutId = undefined;\n this.maxReconnectAttempts = 10;\n this.reconnectAttempts = 0;\n this.timeoutId = undefined;\n /** @type {Number} */\n this.maxIdleMinutes = 20;\n /** @type {Number} */\n this.maxIdleTime = this.maxIdleMinutes*60*1000;\n // Endpoint does not accept the URI-encoded query strings\n this.pageLocation = `${window.location.origin}${window.location.pathname}`.toLowerCase();\n this.pageLanguage = navigator.language.toLowerCase();\n /** @type {Boolean} */\n this.chatAuthenticated = false;\n /** @type {String} */\n this.chatApplyProduct = '';\n this.renderChatHistory = renderChatHistory;\n this.socketUrlAttempts = 0;\n this.maxSocketUrlAttempts = 1;\n this.blockSocket = false;\n this.authenticated = false;\n }\n\n /**\n * Builds the URL parameters for the call to our srvice to retreive the WSS URL.\n * Location and language should both come from the user's browser. ContinueChat\n * will set the chat history ask. ChatApplyProduct is set only by OAO currently\n * which can be a comma delimited list. We have for now determined that the first\n * product present is the most crucial and are only passing that single one. This\n * can be modified for future use cases.\n * @param {Boolean} continueChat If chat history should be retrieved or not.\n */\n buildUrlParams(continueChat) {\n const urlParams = new URLSearchParams();\n urlParams.append('botReference', this.botInfo.taskBotId);\n urlParams.append('location', this.pageLocation);\n urlParams.append('language', this.pageLanguage);\n urlParams.append('time', Date.now()); //Add this to bust the browser cache for the GET requests for a persistent user.\n if (continueChat) {\n urlParams.append('historyLimit', '10');\n }\n else {\n urlParams.append('historyLimit', '0');\n }\n this.chatApplyProduct = window.chatApplyProduct || '';\n if (this.chatApplyProduct.trim() !== '') {\n this.chatApplyProduct = window.chatApplyProduct;\n let chatApplyProduct;\n const commaIndex = this.chatApplyProduct.indexOf(','); //This is if OAO sets multiple products on a single page.\n if (commaIndex === -1) {\n chatApplyProduct = this.chatApplyProduct;\n }\n else {\n chatApplyProduct = this.chatApplyProduct.substring(0, commaIndex);\n }\n urlParams.append('applyProduct', chatApplyProduct);\n }\n\n return urlParams;\n }\n\n /**\n * Closes the current websocket and alerts the parent element that the connection is closed.\n */\n closeConnection() {\n if (!this.webSocket) {\n this.isConnected = false;\n this.isReconnecting = false;\n return;\n }\n this.isConnected = false;\n this.isReconnecting = false;\n // This probably is 1000 for \"close normal\" by default, but spec seems to leave some room for ambiguity in browsers\n this.webSocket.close(1000);\n this.webSocket = undefined;\n clearInterval(this.pingIntervalId);\n clearTimeout(this.pingTimeoutId);\n clearTimeout(this.timeoutId);\n document.dispatchEvent(new Event(ChatEventCodes.ChatDisconnected));\n console.log('Connection was closed.');\n }\n\n /**\n * Create the socket connection and then set the appropriate listeners against the connection.\n * @param {Boolean} continueChat Indicates if the chat is ongoing or if it should start new.\n */\n buildSocket(continueChat) {\n if(this.blockSocket) {\n this.blockSocket = false;\n return;\n }\n const socketUrl = new URL(this.url);\n if (continueChat) {\n socketUrl.searchParams.set('isReconnect', 'true');\n }\n const socket = new WebSocket(socketUrl);\n this.webSocket = socket;\n\n if (this.webSocket) {\n this.setEventListeners();\n }\n }\n\n /**\n * Creates the initial socket connection. If already connected ignore the call.\n * @param {Boolean} continueChat If the chat should be continued with chat history.\n */\n createSocketConnection(continueChat = false) {\n if (!this.isConnected) {\n this.blockSocket = false;\n if (window.chatAuthenticated) {\n this.authenticated = true;\n const pageLocation = window.location;\n /**\n * Initially we used string.startsWith and string.endsWith but ROL has a polyfill that is overrinding\n * these functions from a Microsoft aspx standpoint. So we needed to back down to an index search\n * for both functions.\n */\n const host = pageLocation.host;\n const domain = '.huntington.com';\n if (host.lastIndexOf('onlinebanking', 0) === 0 && host.indexOf(domain, host.length - domain.length) !== -1) {\n this.getSocketUrl(`${pageLocation.origin}/rol/Retail/ddas-chatauthz/1.0/ddas-chatauthorizations`, continueChat);\n }\n else {\n this.getSocketUrl('https://onlinebanking.huntington.com/rol/Retail/ddas-chatauthz/1.0/ddas-chatauthorizations', continueChat);\n }\n }\n else {\n this.getSocketUrl(this.wssApiUrl, continueChat);\n }\n }\n }\n\n /**\n * This sets the flag the is read in buildSocket() to stop the creation of the WS completely.\n * This generally only needs done in the instance where the web request for the socket URL\n * is pending but the user closes the chat window. Without this, the window will \"close\", but\n * the connection will occur in the background regardless.\n */\n forceBlockSocket() {\n this.blockSocket = true;\n }\n\n /**\n * Creates a new XMLHttpRequest and sends to get the WSS URL and the chat history (if applicable).\n * If chat history is returned, then render the chat history and create the socket connection. If not,\n * only create the socket connection.\n * @param {String} url The endpoint to request the WSS URL and chat history.\n * @param {Boolean} continueChat Flag indicating if the chat is being continued or to start fresh.\n */\n getSocketUrl(url, continueChat) {\n const request = new XMLHttpRequest();\n request.open('GET', `${url}?${this.buildUrlParams(continueChat)}`);\n request.withCredentials = true;\n request.responseType = 'json';\n request.onload = () => {\n if (request.status >= 400 || !request.response) {\n if (this.socketUrlAttempts < this.maxSocketUrlAttempts) {\n if (this.authenticated) {\n console.error('Error trying to connect to the auth service. Re-attempting with the unathenticated service.');\n this.authenticated = false;\n }\n this.socketUrlAttempts++;\n this.getSocketUrl(this.wssApiUrl, continueChat);\n return;\n }\n else {\n document.dispatchEvent(new Event(ChatEventCodes.WebsocketServiceUnavailable));\n console.error(`Error getting websocket URL: Status:${request.status}, Reason:${request.statusText}`);\n return;\n }\n }\n const chatAuthorization = request.response['payload']['ddas-chatauthorizations'][0];\n this.url = chatAuthorization.websocketUrl;\n const chatHistory = chatAuthorization.chatHistory;\n\n if (chatHistory?.messages && chatHistory.messages.length > 0) {\n this.renderChatHistory(chatHistory.messages);\n this.buildSocket(continueChat);\n }\n else {\n // Set to false since there is possibly an existing cookie but no history length\n // to display the welcome message again and start over.\n this.buildSocket(false);\n }\n };\n request.onerror = () => {\n if (this.socketUrlAttempts < this.maxSocketUrlAttempts) {\n if (this.authenticated) {\n this.authenticated = false;\n console.error('Error trying to connect to the auth service. Re-attempting with the unathenticated service.');\n }\n this.socketUrlAttempts++;\n this.getSocketUrl(this.wssApiUrl, continueChat);\n }\n else {\n document.dispatchEvent(new Event(ChatEventCodes.WebsocketServiceUnavailable));\n }\n };\n request.send();\n }\n\n /**\n * Clears the current timeout based on the last received message and\n * sets a new timeout based on the maxIdleTime. If the timeout is hit,\n * the connection will be closed and an event will be sent to close the\n * chat window.\n */\n handleSetAndClearTimeouts() {\n clearTimeout(this.timeoutId);\n this.timeoutId = setTimeout(() => {\n clearTimeout(this.pingTimeoutId);\n this.closeConnection();\n document.dispatchEvent(new Event(ChatEventCodes.ChatTimeout));\n }, this.maxIdleTime);\n }\n\n /**\n * Sends a ping to the websocket server. If a pong is not received within the defined\n * maximum wait time, then the socket will either: reconnect if we have not made max connection\n * attempts; close the socket connection.\n */\n ping() {\n this.webSocket.send(JSON.stringify({\n 'type': 'ping',\n 'resourceid': './bot-message',\n 'botInfo': this.botInfo,\n 'id': Date.now()\n }));\n this.pingTimeoutId = setTimeout(() => {\n clearInterval(this.pingIntervalId);\n this.closeConnection();\n }, this.maxPongWait);\n }\n\n /**\n * Clears the timout set in the ping() to prevent reconnection or closure of the socket connection.\n */\n pong() {\n clearTimeout(this.pingTimeoutId);\n }\n\n /**\n * Send for data through as a message to the bot.\n * @param {Object} formData The collection of input names/values from the form being submitted.\n */\n sendFormSubmission(formData) {\n if (this.isConnected) {\n this.webSocket.send(JSON.stringify({\n 'message': {\n 'messageObject': formData\n },\n 'resourceid': '/bot.message',\n 'botInfo': this.botInfo\n }));\n this.handleSetAndClearTimeouts();\n }\n }\n\n /**\n * If connected, send the typing ended message.\n */\n sendTypingEnded() {\n if (this.isConnected) {\n this.webSocket.send(JSON.stringify({\n 'event': 'stop_typing',\n 'message': {\n 'body': '',\n 'type': 'TYPING_STOP'\n },\n 'preDefinedEvent': {\n 'type': 'TYPING_STOP'\n },\n 'resourceid': '/bot.message',\n 'botInfo': this.botInfo\n }));\n }\n }\n\n /**\n * If connected, send the typing started message.\n */\n sendTypingStarted() {\n if (this.isConnected) {\n this.webSocket.send(JSON.stringify({\n 'event': 'typing',\n 'message': {\n 'body': '',\n 'type': ''\n },\n 'preDefinedEvent': {\n 'type': 'TYPING_STARTED'\n },\n 'resourceid': '/bot.message',\n 'botInfo': this.botInfo\n }));\n }\n }\n\n /**\n * Sends the user's input either through an interactive bot template or what the\n * user has typed and sent.\n * @param {String} inputValue The user's typed or clicked value to send to the bot.\n */\n sendUserInput(inputValue) {\n if (this.isConnected) {\n this.webSocket.send(JSON.stringify({\n 'message': {\n 'body': inputValue\n },\n 'resourceid': '/bot.message',\n 'botInfo': this.botInfo\n }));\n this.handleSetAndClearTimeouts();\n }\n }\n\n /**\n * Sends the user merge and continue event to the Kore bot after connection.\n * This should be sent after the initial \"Bot_Active\" message is received through\n * the websocket connection.\n */\n sendUserMergeContinue() {\n if (this.isConnected) {\n this.webSocket.send(JSON.stringify({\n 'clientMessageId': Date.now(),\n 'customEvent': 'USER_MERGE',\n 'resourceid': '/bot.clientEvent',\n 'botInfo': this.botInfo\n }));\n this.handleSetAndClearTimeouts();\n }\n }\n\n /**\n * Sets the event listeners against the websocket object.\n */\n setEventListeners() {\n this.webSocket.addEventListener('open', (e) => {\n if (this.isReconnecting) {\n this.isReconnecting = false;\n }\n this.isConnected = true;\n // Send the login flow ended message. The parent contorller will determine if the cookie still exists\n // and send the send request for the merge event to the WS.\n document.dispatchEvent(new Event(ChatEventCodes.ChatConnected));\n\n //Set an interval to call continuously against the defined ping timer. ping() will handle\n // what happens if a pong is not received in a defined period of time.\n this.pingIntervalId = setInterval(() => this.ping(), this.pingTimer);\n this.handleSetAndClearTimeouts();\n });\n\n this.webSocket.addEventListener('message', (e) => {\n if (e.origin !== 'wss://rtm.kore.ai') {\n console.error('Received message from invalid WS origin.');\n return;\n }\n this.socketRecievedMessage(e);\n });\n\n this.webSocket.addEventListener('error', (e) => {\n // Need to add a standalone error handling function. Will probably need to bubble\n // connection state in that as well so the parent component can handle UI display\n // for connection status.\n this.socketRecievedError(e);\n });\n\n this.webSocket.addEventListener('close', (e) => {\n // Seemingly only Firefox triggers \"going away\" websocket close events on close tab / navigate\n // These should be ignored so we maintain the chat widget state.\n if (e.code == 1001) {\n return;\n }\n\n this.isConnected = false;\n this.isReconnecting = false;\n this.blockSocket = false;\n // Bubble the event up to the parent element so it can also maintain connection state.\n document.dispatchEvent(new Event(ChatEventCodes.ChatDisconnected));\n });\n }\n\n /**\n * For now log the error in the console and dispatch the error event up to the main chat handler.\n * @param {Event} e The error event object.\n */\n socketRecievedError(e) {\n console.error(e);\n document.dispatchEvent(new Event(ChatEventCodes.ChatDisconnected));\n document.dispatchEvent(new Event(ChatEventCodes.WebsocketError));\n }\n\n /**\n * Extracts the data from the message event, determines the type, and follows the appropriate action\n * for the type of message.\n * @param {Event} e The message received event.\n */\n socketRecievedMessage(e) {\n const data = JSON.parse(e.data);\n console.log(data);\n switch (data.type) {\n case 'Agent_Session_End':\n document.dispatchEvent(new Event(ChatEventCodes.AgentSessionEnd));\n break;\n case 'Agent_Session_Start':\n document.dispatchEvent(new Event(ChatEventCodes.AgentSessionStart));\n break;\n case 'Bot_Active': {\n const chatConnectedData = {\n loginFlowSuccess: this.authenticated\n };\n document.dispatchEvent(new MessageEvent(ChatEventCodes.BotActive, {data: chatConnectedData}));\n break;\n }\n case 'bot_response':\n document.dispatchEvent(new MessageEvent(ChatEventCodes.BotResponse , {bubbles: true, data: data}));\n break;\n case 'botKitUnreachable':\n document.dispatchEvent(new MessageEvent(ChatEventCodes.BotKitUnreachable, {bubbles: true, data: data}));\n break;\n case 'pong':\n this.pong();\n break;\n default:\n console.log(e);\n }\n }\n}","import Cookies from \"js-cookie\";\n\n/**\n * Class to handle the cookie management needed for the chat bot. Currently\n * this is assumed to only manage against the .huntington.com domain.\n */\nexport default class KoreChatCookieManager {\n\n constructor() {\n this.domain = '.huntington.com';\n this.path = '/';\n }\n\n /**\n * Deletes the cookie with the provided name.\n * @param {String} cookieName The name of the cookie to be deleted.\n */\n deleteCookie(cookieName) {\n Cookies.remove(cookieName, {domain: this.domain, path: this.path});\n }\n\n /**\n * Gets the cookie with the provided name and returns it's value. If no value is present\n * on the cookie, it returns an empty string.\n * @param {String} cookieName The name of the cookie to have it's value read.\n * @returns Either the cookie value or an empty string.\n */\n readcookie(cookieName) {\n const cookieValue = Cookies.get(cookieName);\n return cookieValue || '';\n }\n\n /**\n * Creates the cookie with the appropriate name and value only if the cookie already\n * does not exist.\n * @param {String} cookieName The name of the cookie to be created.\n * @param {String} cookieValue The value of the cookie to be created.\n * @param {Number} expirationTime The optional expiration time of the cookie to be created.\n */\n createCookie(cookieName, cookieValue, expirationTime = undefined) {\n if (!Cookies.get(cookieName)) {\n expirationTime ?\n Cookies.set(cookieName, cookieValue, {path: this.path, domain: this.domain, secure: true, expires: expirationTime }) :\n Cookies.set(cookieName, cookieValue, {path: this.path, domain: this.domain, secure: true});\n }\n }\n\n /**\n * Updates the cookie with the appropriate name and value only if the cookie already\n * exists.\n * @param {String} cookieName The name of the cookie to be created.\n * @param {String} cookieValue The value of the cookie to be created.\n * @param {Number} expirationTime The optional expiration time of the cookie to be created.\n */\n updateCookie(cookieName, cookieValue, expirationTime = undefined) {\n if (Cookies.get(cookieName)) {\n expirationTime ?\n Cookies.set(cookieName, cookieValue, {path: this.path, domain: this.domain, secure: true, expires: expirationTime }) :\n Cookies.set(cookieName, cookieValue, {path: this.path, domain: this.domain, secure: true});\n }\n }\n}","/**\n * Class to handle the interactions with the Adobe Analytics API.\n */\nexport default class KoreAnalyticsHandler {\n constructor() {\n this.pageName = undefined;\n this.chatPageName = 'pub: global: chatbot';\n this.hasOpenedChat = false;\n this.hasSentMessage = false;\n this.hasClosedChat = false;\n this.rolAskUsHasBeenClicked = false;\n }\n\n /**\n * Set the custom props to send through the Adobe page setting interaction.\n * @param {String} pageName The page name of the interaction.\n */\n addChannelVars(pageName) {\n if (pageName && pageName.length) {\n\n const parts = pageName.toLowerCase().replace(/\\s/g, '').split(':');\n if (parts.length > 0) {\n s.channel = parts[0];\n }\n\n if (parts.length > 1) {\n s.prop1 = `${s.channel}: ${parts[1]}`;\n }\n else {\n s.prop1 = s.channel;\n }\n\n if (parts.length > 2) {\n s.prop2 = `${s.prop1}: ${parts[2]}`;\n }\n else {\n s.prop2 = s.prop1;\n }\n\n if (parts.length > 3) {\n s.prop3 = `${s.prop2}: ${parts[3]}`;\n }\n else {\n s.prop3 = s.prop2;\n }\n\n if (parts.length > 4) {\n s.prop3 = `${s.prop3}: ${parts[4]}`;\n }\n else {\n s.prop4 = s.prop3;\n }\n }\n }\n\n /**\n * \n */\n askUsClicked() {\n if (!this.isAdobeAvailable() || this.rolAskUsHasBeenClicked) {\n return;\n }\n this.prepAdobeCode();\n this.sendClickEvent(`ask-us-chatbot`, this.pageName);\n this.resetAdobeCode();\n this.rolAskUsHasBeenClicked = true;\n }\n\n /**\n * If the adobe global variable is present, then set the user sent message\n * event and trigger the synthetic click.\n */\n chatMessageSent() {\n if (!this.isAdobeAvailable() || this.hasSentMessage) {\n return;\n }\n this.prepAdobeCode('event51', true);\n this.sendClickEvent('send message to chatbot');\n this.resetAdobeCode();\n this.hasSentMessage = true;\n }\n\n /**\n * If the adobe global variable is present, then send the synthetic\n * click event for chat closed.\n */\n chatWindowClosed() {\n if (!this.isAdobeAvailable() || this.hasClosedChat) {\n return;\n }\n this.prepAdobeCode();\n this.sendClickEvent('exit chatbot');\n this.resetAdobeCode();\n this.hasClosedChat = true;\n this.hasOpenedChat = false;\n this.hasSentMessage = false;\n this.rolAskUsHasBeenClicked = false;\n }\n\n /**\n * If the adobe global variable is present, then set the user opened chat\n * event and trigger the synthetic click.\n */\n chatWindowOpened() {\n if (!this.isAdobeAvailable() || this.hasOpenedChat) {\n return;\n }\n this.prepAdobeCode('event50,event996');\n window.s.pageName = this.chatPageName;\n this.addChannelVars('pub: global: chatbot');\n window.s.t();\n this.resetAdobeCode();\n this.hasOpenedChat = true;\n this.hasClosedChat = false;\n }\n\n /**\n * Checks and returns if the Adobe global 's' object has been loaded\n * onto the present page.\n */\n isAdobeAvailable() {\n const isAvailable = typeof window.s !== 'undefined';\n if (!isAvailable) {\n console.warn('No global analytics object present.');\n }\n else if (!this.pageName) {\n this.pageName= window.s.pageName;\n }\n else {\n // Do nothing.\n }\n\n return isAvailable;\n }\n\n /**\n * Sets the Adobe object and the events to send before the chat trigger interactions.\n * @param {String} events The events being sent with the next interaction.\n * @param {Boolean} isLinkTracking If the interaction is triggered by a link or synthetic click.\n */\n prepAdobeCode(events, isLinkTracking = false) {\n window.s.clearVars();\n window.s.usePlugins = false;\n this.setEvents(events, isLinkTracking);\n }\n\n /**\n * Resets the Adobe object after the chat trigger interactions.\n */\n resetAdobeCode() {\n window.s.usePlugins = true;\n window.s.clearVars();\n }\n\n /**\n * Sends the synthetic click event to the adobe API.\n * @param {String} linkName The link name to send with the page name.\n */\n sendClickEvent(linkName, pageName = '') {\n let qualifiedName = '';\n if (pageName && pageName.length) {\n qualifiedName = `${linkName}|${pageName}`;\n }\n else if (!this.pageName && !this.chatPageName) {\n qualifiedName = `${linkName}`;\n }\n else {\n qualifiedName = `${linkName}|${this.chatPageName}`;\n window.s.pageName = this.chatPageName;\n }\n window.s.tl(this, 'o', `${qualifiedName}`);\n }\n\n /**\n * Clears the existing Adobe vars and sets the plugin status based on if\n * this is called before or after an interaction with the Adobe API.\n * @param {Boolean} usePlugins Whether to disable or enable adobe plugins.\n */\n setAdobeCode(usePlugins = false) {\n s.clearVars();\n s.usePlugins = usePlugins;\n }\n\n /**\n * Sets the event on the global Adobe object. If it is a link tracking event,\n * then also set the linkTrackEvents value with the event.\n * @param {String} events The event to set before triggering the Adobe API.\n * @param {Boolean} isLinkTracking If the interaction is triggered by a link or synthetic click.\n */\n setEvents(events, isLinkTracking = false) {\n if (!events || !events.length === 0) {\n return;\n }\n\n const eventsCollection = events.split(events, ',');\n if (isLinkTracking) {\n window.s.linkTrackEvents = window.s.linkTrackEvents || '';\n for (const event of eventsCollection) {\n if (window.s.linkTrackEvents.indexOf(events) === -1) {\n if (window.s.linkTrackEvents.length > 0) {\n window.s.linkTrackEvents += ',';\n }\n\n window.s.linkTrackEvents += event;\n }\n }\n }\n window.s.events = window.s.events || '';\n if (window.s.events.length) {\n s.events += ',';\n }\n window.s.events += events;\n }\n}","import KoreAnalyticsHandler from \"./KoreAnalyticsHandler\";\n\n/**\n * Class to manage the chat bot interactions and the Adobe API handler.\n */\nexport default class KoreAnalyticsManager {\n constructor() {\n this.analyticsHandler = new KoreAnalyticsHandler();\n }\n\n rolAskUsClicked() {\n this.analyticsHandler.askUsClicked();\n }\n\n userClosedChatWindow() {\n this.analyticsHandler.chatWindowClosed();\n }\n\n userOpenedChatWindow() {\n this.analyticsHandler.chatWindowOpened();\n }\n\n userSentMessage() {\n this.analyticsHandler.chatMessageSent();\n }\n}","import DOMPurify from 'dompurify';\n\nconst desktopHtml = `\n
`;\n\nconst mobileHtml = `\n
\n`;\n\n\nconst injectedStyle = `\n #nuanceC2CSearchPhone .search-button__wrapper--mobile {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n @media print, screen and (min-width: 70em) {\n #nuanceC2CSearchPhone .search-button__wrapper--mobile {\n display: none;\n }\n }\n\n @media print, screen and (max-width: 69.99em) {\n #nuanceC2CSearch .search-button__wrapper--desktop {\n display: none;\n }\n }\n\n #nuanceC2CSearchPhone .c-g-btn__ask-for-help.fab-launcher.rol-ask-us > * {\n pointer-events: none;\n }\n\n #nuanceC2CSearch .c-g-btn__ask-for-help.fab-launcher.rol-ask-us > * {\n pointer-events: none;\n }\n`;\nexport default class AskUsInjector {\n\n static setOnlineBankingButtons() {\n try {\n const host = window.location.host;\n const domain = '.huntington.com';\n if (host.lastIndexOf('onlinebanking', 0) === 0 && host.indexOf(domain, host.length - domain.length) !== -1) {\n const desktopItem = document.getElementById('nuanceC2CSearch');\n if (desktopItem) {\n desktopItem.innerHTML = DOMPurify.sanitize(desktopHtml);\n }\n const mobileItem = document.getElementById('nuanceC2CSearchPhone');\n if (mobileItem) {\n mobileItem.innerHTML = DOMPurify.sanitize(mobileHtml);\n }\n const rolHeaderContainer = document.querySelector('header.c-g-header .c-g-header__controls');\n if (rolHeaderContainer) {\n const style = document.createElement('style');\n style.textContent = DOMPurify.sanitize(injectedStyle);\n rolHeaderContainer.append(style);\n }\n }\n }\n catch(e) {\n console.error('Error injecting the ask us buttons', e);\n }\n }\n}","import Message from \"./Message\";\nimport React from \"react\";\n\n/**\n * Class to display the queue position message.\n * TODO: This is supposed to be displayed statically at the bottom of the window,\n * over being a live update message in the chat window.\n */\nexport default class QueuePositionMessage extends Message {\n\n componentDidMount() {\n // Do nothing other than stopping the Message componentDidMount from executing.\n }\n render() {\n if (!this.props.payload?.text || !this.props.payload?.queuePosition) {\n return '';\n }\n return(\n
\n );\n }\n}","import React from 'react';\nimport QueuePositionMessage from './QueuePositionMessage';\n\nexport default class QueuePosition extends React.Component {\n constructor(props) {\n super(props);\n }\n\n\n render() {\n return(\n
\n \n
\n );\n }\n}","import React from 'react';\n\nexport default class AwaitingMessageHandler extends React.Component {\n constructor(props) {\n super(props);\n\n this.defaultWaitTime = 250;\n this.awaitingResponseTimeout = undefined;\n }\n\n /**\n * Checks against the waiting prop passed through from chat-bot.jsx. If a message\n * has been received from there, then the prop will be reset to false. If that happens,\n * then clear the timeout set later in this function so this won't get called again.\n * \n * If the chat-bot is waiting for a response, then set a timeout to recursively call\n * this function to allow for a re-evaluation of the prop condition and is the chat is\n * no longer waiting for a response.\n * \n * @returns Either and empty string is the chat is no longer waiting, or the awaiting messaging and animation.\n */\n renderAwaitingResponseBlock() {\n if (!this.props.isWaiting){\n clearTimeout(this.awaitingResponseTimeout);\n return '';\n }\n\n this.awaitingResponseTimeout = setTimeout(() => {\n this.renderAwaitingResponseBlock();\n }, this.defaultWaitTime);\n\n return
\n }\n\n render() {\n return (\n
\n {this.renderAwaitingResponseBlock()}\n
\n )\n }\n}","import React from 'react';\n\nexport default class ChatWindowHeader extends React.Component {\n constructor(props) {\n super(props);\n\n this.reRenderTime = 250;\n }\n\n isConnecting() {\n if (!this.props.isConnecting) {\n return
\n }\n\n setTimeout(() => {\n this.isConnecting();\n }, this.reRenderTime);\n\n return
\n }\n\n render() {\n return(\n
\n \n {this.isConnecting()}\n \n
\n )\n }\n}","import React, {createRef} from 'react';\nimport KeyCodes from '../../utils/KeyCodes.js';\nimport Messages from '../templates/Messages.jsx';\nimport KoreWebSocketManager from '../utils/KoreWebSocketManager.js';\nimport ChatEventCodes from '../utils/ChatEventCodes.js';\nimport KoreChatCookieManager from '../utils/KoreChatCookieManager.js';\nimport KoreAnalyticsManager from '../utils/KoreAnalyticsManager.js';\nimport AskUsInjector from '../utils/AskUsInjector.js';\nimport QueuePosition from '../templates/QueuePosition.jsx';\nimport AwaitingMessageHandler from '../utils/AwaitingMessageHandler.jsx';\nimport ChatWindowHeader from './ChatWindowHeader.jsx';\n\nexport default class ChatBot extends React.Component {\n constructor(props) {\n super(props);\n\n this.state = {\n /**@type {String} The BotName passsed through from the HTML injection.*/\n chatBot: props.botName,\n /**@type {String} The Bot ID passsed through from the HTML injection.*/\n taskBotId: props.botId,\n /**@type {String} The root for the AGW API service on the initial root element. */\n apiRootUrl: props.apiRootUrl,\n /**@type {String} The root for the CSS file on the initial root element. */\n cssUrl: props.cssUrl,\n /**@type {String} The indicator from the login system is the user has authenticated previously during chat.*/\n chatAuthenticated: props.chatAuthenticated,\n /** @type {Array