@meetelise/chat 1.21.0 → 1.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/pull_request_template.md +61 -0
- package/.idea/codeStyles/Project.xml +57 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/inspectionProfiles/Project_Default.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/.idea/workspace.xml +67 -0
- package/README.md +29 -14
- package/declarations.d.ts +12 -0
- package/package.json +5 -1
- package/public/demo/index.html +62 -4
- package/public/demo/secret.html +63 -0
- package/public/dist/index.js +3184 -1105
- package/public/dist/index.js.LICENSE.txt +19 -9
- package/public/index.html +6 -4
- package/src/MEChat.ts +207 -52
- package/src/MyPubnub.ts +657 -0
- package/src/WebComponent/LeadSourceClient.ts +300 -0
- package/src/WebComponent/Scheduler/date-picker.ts +1 -1
- package/src/WebComponent/Scheduler/time-picker.ts +86 -76
- package/src/WebComponent/Scheduler/tour-scheduler.ts +694 -764
- package/src/WebComponent/Scheduler/tour-type-option.ts +17 -3
- package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +418 -0
- package/src/WebComponent/actions/InputStyles.ts +32 -10
- package/src/WebComponent/actions/action-confirm-button.ts +16 -11
- package/src/WebComponent/actions/call-us-window.ts +341 -58
- package/src/WebComponent/actions/details-window.ts +30 -16
- package/src/WebComponent/actions/email-us-window.ts +89 -58
- package/src/WebComponent/actions/formatPhoneNumber.ts +15 -1
- package/src/WebComponent/actions/minimize-expand-button.ts +92 -0
- package/src/WebComponent/health-chat.ts +267 -0
- package/src/WebComponent/healthcare/healthcare-launcher-styles.ts +34 -0
- package/src/WebComponent/healthcare/healthcare-launcher.ts +100 -0
- package/src/WebComponent/healthchat-styles.ts +119 -0
- package/src/WebComponent/index.ts +1 -1
- package/src/WebComponent/launcher/Launcher.ts +919 -0
- package/src/WebComponent/{launcherStyles.ts → launcher/launcherStyles.ts} +172 -29
- package/src/WebComponent/launcher/mobile-launcher.ts +127 -0
- package/src/WebComponent/launcher/typeEmojiStyles.ts +161 -0
- package/src/WebComponent/launcher/typeMiniStyles.ts +60 -0
- package/src/WebComponent/launcher/typeMobileStyles.ts +50 -0
- package/src/WebComponent/leasing-chat-styles.ts +114 -0
- package/src/WebComponent/me-chat.ts +964 -351
- package/src/WebComponent/me-select.ts +48 -21
- package/src/WebComponent/mini-loader.ts +28 -0
- package/src/WebComponent/pubnub-chat-styles.ts +192 -0
- package/src/WebComponent/pubnub-chat.ts +707 -0
- package/src/WebComponent/pubnub-media.ts +208 -0
- package/src/WebComponent/pubnub-message-styles.ts +54 -0
- package/src/WebComponent/pubnub-message.ts +421 -0
- package/src/analytics.ts +114 -14
- package/src/assetUrls.ts +2 -0
- package/src/disclaimers.ts +56 -0
- package/src/fetchBuildingABTestType.ts +4 -0
- package/src/fetchBuildingInfo.ts +25 -17
- package/src/fetchFeatureFlag.ts +147 -0
- package/src/fetchLeadSources.ts +67 -1
- package/src/fetchPhoneNumberFromSource.ts +31 -0
- package/src/fetchWebchatPreferences.ts +55 -0
- package/src/getAvailabilities.ts +7 -3
- package/src/getBuildingPhoneNumber.ts +26 -0
- package/src/getShouldAllowScheduling.ts +16 -0
- package/src/getTimezoneString.ts +39 -0
- package/src/gtm.ts +17 -0
- package/src/handleChatId.ts +101 -0
- package/src/insertDNIIntoWebsite.ts +136 -0
- package/src/insertLeadSourceIntoSchedulerLinks.ts +50 -0
- package/src/postLeadSources.ts +39 -35
- package/src/svgIcons.ts +62 -53
- package/src/themes.ts +47 -121
- package/src/utils.ts +88 -1
- package/src/WebComponent/Launcher.ts +0 -559
- package/src/WebComponent/actions/text-us-window.ts +0 -279
- package/src/chatID.ts +0 -64
- package/src/createConversation.ts +0 -57
- package/src/fetchCurrentParsedLeadSource.ts +0 -24
- package/src/getRegisteredPhoneNumbers.ts +0 -56
package/src/MyPubnub.ts
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { AxiosError } from "axios";
|
|
2
|
+
import Pubnub, { ListenerParameters, MessageEvent } from "pubnub";
|
|
3
|
+
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { Building } from "./fetchBuildingInfo";
|
|
6
|
+
import { LogType, sendLoggingEvent } from "./analytics";
|
|
7
|
+
import { ChatStorageKey, createChatStorageKey } from "./handleChatId";
|
|
8
|
+
import LeadSourceClient from "./WebComponent/LeadSourceClient";
|
|
9
|
+
import { pushGtmEvent } from "./gtm";
|
|
10
|
+
import { isContainingEmail } from "./utils";
|
|
11
|
+
|
|
12
|
+
interface TokenResponse {
|
|
13
|
+
auth: {
|
|
14
|
+
result: {
|
|
15
|
+
token: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
keys: {
|
|
19
|
+
subscribe_key: string;
|
|
20
|
+
publish_key: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// This is what we expect from our BE
|
|
25
|
+
enum MessageType {
|
|
26
|
+
noReply = "no-reply",
|
|
27
|
+
text = "text",
|
|
28
|
+
media = "media",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface RawPubnubMessage {
|
|
32
|
+
channel: string;
|
|
33
|
+
message: {
|
|
34
|
+
// all the possible options from our BE
|
|
35
|
+
text: string;
|
|
36
|
+
customType: string;
|
|
37
|
+
messageType?: MessageType;
|
|
38
|
+
is_streaming?: boolean;
|
|
39
|
+
is_done?: boolean;
|
|
40
|
+
order?: number;
|
|
41
|
+
stream_id?: string;
|
|
42
|
+
media?: ChatMediaFile[];
|
|
43
|
+
};
|
|
44
|
+
publisher: string;
|
|
45
|
+
subscription: string;
|
|
46
|
+
timetoken: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SimpleTextChatMessage {
|
|
50
|
+
timestamp: number;
|
|
51
|
+
message: string;
|
|
52
|
+
isLeadMessage: boolean;
|
|
53
|
+
chunks: {
|
|
54
|
+
text: string;
|
|
55
|
+
order: number;
|
|
56
|
+
isDone: boolean;
|
|
57
|
+
}[];
|
|
58
|
+
type: SimpleMessageTypes.text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ChatMediaFile {
|
|
62
|
+
title: string | null;
|
|
63
|
+
description: string | null;
|
|
64
|
+
url: string | null;
|
|
65
|
+
}
|
|
66
|
+
export interface SimpleMediaChatMessage {
|
|
67
|
+
timestamp: number;
|
|
68
|
+
media: ChatMediaFile[];
|
|
69
|
+
isLeadMessage: boolean;
|
|
70
|
+
type: SimpleMessageTypes.media;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SimpleChatMessage = SimpleTextChatMessage | SimpleMediaChatMessage;
|
|
74
|
+
|
|
75
|
+
export const isSimpleTextChatMessage = (
|
|
76
|
+
message: SimpleChatMessage
|
|
77
|
+
): message is SimpleTextChatMessage => {
|
|
78
|
+
return message.type === SimpleMessageTypes.text;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const isSimpleMediaChatMessage = (
|
|
82
|
+
message: SimpleChatMessage
|
|
83
|
+
): message is SimpleMediaChatMessage => {
|
|
84
|
+
return message.type === SimpleMessageTypes.media;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export enum SimpleMessageTypes {
|
|
88
|
+
text = "text",
|
|
89
|
+
media = "media",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class MyPubnub {
|
|
93
|
+
private apiHost = "https://app.meetelise.com";
|
|
94
|
+
|
|
95
|
+
private leadSourceClient: LeadSourceClient | null = null;
|
|
96
|
+
private building: Building | null = null;
|
|
97
|
+
private buildingSlug: string;
|
|
98
|
+
private orgSlug: string;
|
|
99
|
+
|
|
100
|
+
private eliseResponseTimeout: NodeJS.Timeout | null = null;
|
|
101
|
+
|
|
102
|
+
pubnub: Pubnub | null = null;
|
|
103
|
+
leadUserId = "";
|
|
104
|
+
channel = "";
|
|
105
|
+
leadSource: string | null = null;
|
|
106
|
+
|
|
107
|
+
isCurrentlyStreamingMessage = false;
|
|
108
|
+
|
|
109
|
+
chatListener:
|
|
110
|
+
| ((res: { messages: SimpleChatMessage[]; isLoading: boolean }) => void)
|
|
111
|
+
| null = null;
|
|
112
|
+
|
|
113
|
+
rawPubnubMessages: RawPubnubMessage[] = [];
|
|
114
|
+
simpleChatMessages: SimpleChatMessage[] = [];
|
|
115
|
+
|
|
116
|
+
listenerParams: ListenerParameters = {
|
|
117
|
+
message: (messageEvent: MessageEvent) => {
|
|
118
|
+
// if the messageEvent is a no-reply, we ignore it and stop loading
|
|
119
|
+
if (messageEvent.message.messageType !== MessageType.noReply) {
|
|
120
|
+
this.rawPubnubMessages = [
|
|
121
|
+
...this.rawPubnubMessages,
|
|
122
|
+
{
|
|
123
|
+
channel: messageEvent.channel,
|
|
124
|
+
message: messageEvent.message,
|
|
125
|
+
publisher: messageEvent.publisher,
|
|
126
|
+
subscription: messageEvent.subscription,
|
|
127
|
+
timetoken: +messageEvent.timetoken,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
this.simpleChatMessages =
|
|
131
|
+
this.translatePubnubMessagesIntoSimpleChatMessages(
|
|
132
|
+
this.rawPubnubMessages
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const isWaitingForEliseResponse =
|
|
137
|
+
messageEvent.publisher !== "eliseai" &&
|
|
138
|
+
messageEvent.publisher !== "elise_health_ai";
|
|
139
|
+
this.chatListener?.({
|
|
140
|
+
messages: this.simpleChatMessages,
|
|
141
|
+
isLoading: isWaitingForEliseResponse,
|
|
142
|
+
});
|
|
143
|
+
if (!isWaitingForEliseResponse && this.eliseResponseTimeout) {
|
|
144
|
+
clearTimeout(this.eliseResponseTimeout);
|
|
145
|
+
}
|
|
146
|
+
this.isLoadingMessages = isWaitingForEliseResponse;
|
|
147
|
+
|
|
148
|
+
if (messageEvent.message.customType === "ai_message") {
|
|
149
|
+
const containsConfirmedTour = this.simpleChatMessages.some(
|
|
150
|
+
(message) => {
|
|
151
|
+
return (
|
|
152
|
+
message.type === SimpleMessageTypes.text &&
|
|
153
|
+
message.message.includes("confirmed for your tour")
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
if (containsConfirmedTour) {
|
|
158
|
+
pushGtmEvent("scheduledTourEvent", {
|
|
159
|
+
tourDetails: messageEvent.message,
|
|
160
|
+
buildingId: this.building?.id,
|
|
161
|
+
buildingSlug: this.buildingSlug,
|
|
162
|
+
orgSlug: this.orgSlug,
|
|
163
|
+
leadUserId: this.leadUserId,
|
|
164
|
+
channel: this.channel,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.checkAndHandleGTMForLeadEmail(messageEvent);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
isLoadingMessages = false;
|
|
173
|
+
isFirstChatMessageSent = false;
|
|
174
|
+
|
|
175
|
+
constructor(
|
|
176
|
+
buildingSlug: string,
|
|
177
|
+
buildingDetails: Building | null,
|
|
178
|
+
orgSlug: string,
|
|
179
|
+
leadSource: string | null = null,
|
|
180
|
+
leadUserId: string,
|
|
181
|
+
leadSourceClient: LeadSourceClient | null = null
|
|
182
|
+
) {
|
|
183
|
+
this.buildingSlug = buildingSlug;
|
|
184
|
+
this.building = buildingDetails;
|
|
185
|
+
this.orgSlug = orgSlug;
|
|
186
|
+
this.leadSource = leadSource;
|
|
187
|
+
this.leadUserId = leadUserId;
|
|
188
|
+
this.channel = `webchat_${leadUserId}`;
|
|
189
|
+
this.leadSourceClient = leadSourceClient;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
checkAndHandleGTMForLeadEmail = (messageEvent: Pubnub.MessageEvent): void => {
|
|
193
|
+
try {
|
|
194
|
+
if (messageEvent.message.customType !== "lead_message") {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!isContainingEmail(messageEvent.message.text)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
pushGtmEvent("leadProvidedEmail", {
|
|
201
|
+
buildingId: this.building?.id,
|
|
202
|
+
buildingSlug: this.buildingSlug,
|
|
203
|
+
orgSlug: this.orgSlug,
|
|
204
|
+
leadUserId: this.leadUserId,
|
|
205
|
+
channel: this.channel,
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
addChatListener(
|
|
213
|
+
listener: (response: {
|
|
214
|
+
messages: SimpleChatMessage[];
|
|
215
|
+
isLoading: boolean;
|
|
216
|
+
}) => void
|
|
217
|
+
): void {
|
|
218
|
+
this.chatListener = listener;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async initializePubnub(
|
|
222
|
+
chatStorageKey: ChatStorageKey
|
|
223
|
+
): Promise<Pubnub | undefined> {
|
|
224
|
+
if (!chatStorageKey.leadId) return;
|
|
225
|
+
this.leadUserId = chatStorageKey.leadId;
|
|
226
|
+
|
|
227
|
+
const pubnubToken = await this.fetchToken(this.leadUserId, this.channel);
|
|
228
|
+
if (!pubnubToken) return;
|
|
229
|
+
|
|
230
|
+
// These keys are OK to expose live, the authKey generated by the BE is what
|
|
231
|
+
// is used to authenticate the user. Ideally, should also add rate limiting
|
|
232
|
+
// and/or IP whitelisting to the BE endpoint that generates the token!!
|
|
233
|
+
this.pubnub = new Pubnub({
|
|
234
|
+
publishKey: pubnubToken.keys.publish_key,
|
|
235
|
+
subscribeKey: pubnubToken.keys.subscribe_key,
|
|
236
|
+
userId: this.leadUserId,
|
|
237
|
+
authKey: pubnubToken.auth.result.token,
|
|
238
|
+
origin: "meetelise.pubnubapi.com",
|
|
239
|
+
});
|
|
240
|
+
this.withAuthToken(() => new Promise(() => this.handleChatListeners()));
|
|
241
|
+
await this.withAuthToken(() => this.getChannelHistory());
|
|
242
|
+
return this.pubnub;
|
|
243
|
+
}
|
|
244
|
+
async fetchToken(
|
|
245
|
+
lead: string,
|
|
246
|
+
channel: string
|
|
247
|
+
): Promise<TokenResponse | null> {
|
|
248
|
+
try {
|
|
249
|
+
const response = await axios.get(
|
|
250
|
+
`${
|
|
251
|
+
this.apiHost
|
|
252
|
+
}/platformApi/webchat/pn/request-token?user_id=${lead}&channel=${channel}&${
|
|
253
|
+
this.building ? "" : `industry=health_care`
|
|
254
|
+
}`
|
|
255
|
+
);
|
|
256
|
+
return response.data;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (this.building) {
|
|
259
|
+
sendLoggingEvent({
|
|
260
|
+
logTitle: "PUBNUB_ERROR_FETCHING_TOKEN",
|
|
261
|
+
logData: { error },
|
|
262
|
+
logType: LogType.error,
|
|
263
|
+
buildingSlug: this.buildingSlug,
|
|
264
|
+
orgSlug: this.orgSlug,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
async fetchChannelExists(channel: string): Promise<boolean> {
|
|
271
|
+
try {
|
|
272
|
+
const response = await axios.get(
|
|
273
|
+
`${this.apiHost}/platformApi/webchat/check-channel-exists?channel_name=${channel}`
|
|
274
|
+
);
|
|
275
|
+
return response.data;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (this.building) {
|
|
278
|
+
sendLoggingEvent({
|
|
279
|
+
logTitle: "PUBNUB_ERROR_FETCHING_CHANNEL_EXISTS",
|
|
280
|
+
logData: { error },
|
|
281
|
+
logType: LogType.error,
|
|
282
|
+
buildingSlug: this.buildingSlug,
|
|
283
|
+
orgSlug: this.orgSlug,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async withAuthToken(apiRequestFunc: () => Promise<void>): Promise<void> {
|
|
291
|
+
try {
|
|
292
|
+
await apiRequestFunc();
|
|
293
|
+
} catch (error: unknown) {
|
|
294
|
+
// only want to retry with new token if the error is a 403
|
|
295
|
+
if (
|
|
296
|
+
error instanceof AxiosError &&
|
|
297
|
+
error &&
|
|
298
|
+
error.response &&
|
|
299
|
+
error.response.status === 403
|
|
300
|
+
) {
|
|
301
|
+
try {
|
|
302
|
+
if (!this.pubnub || !this.leadUserId || !this.channel) return;
|
|
303
|
+
|
|
304
|
+
const newToken = await this.fetchToken(this.leadUserId, this.channel);
|
|
305
|
+
if (!newToken) return;
|
|
306
|
+
|
|
307
|
+
this.pubnub.setAuthKey(newToken.auth.result.token);
|
|
308
|
+
|
|
309
|
+
await apiRequestFunc();
|
|
310
|
+
} catch (retryError) {
|
|
311
|
+
if (this.building) {
|
|
312
|
+
sendLoggingEvent({
|
|
313
|
+
logTitle: "PUBNUB_ERROR_REFETCHING_TOKEN",
|
|
314
|
+
logData: {
|
|
315
|
+
retryError,
|
|
316
|
+
},
|
|
317
|
+
logType: LogType.error,
|
|
318
|
+
buildingSlug: this.buildingSlug,
|
|
319
|
+
orgSlug: this.orgSlug,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async getChannelHistory(maxTotalMessageChunksToFetch = 1500): Promise<void> {
|
|
328
|
+
try {
|
|
329
|
+
let allMessages: RawPubnubMessage[] = [];
|
|
330
|
+
let startTimeToken: string | number | null = null;
|
|
331
|
+
const maxCountPerFetch = 100;
|
|
332
|
+
for (
|
|
333
|
+
let totalCount = 0;
|
|
334
|
+
totalCount < maxTotalMessageChunksToFetch;
|
|
335
|
+
totalCount += maxCountPerFetch
|
|
336
|
+
) {
|
|
337
|
+
const response: Pubnub.FetchMessagesResponse = await new Promise(
|
|
338
|
+
(resolve, reject) => {
|
|
339
|
+
if (!this.pubnub || !this.channel) return [];
|
|
340
|
+
const countToFetch = Math.min(
|
|
341
|
+
maxCountPerFetch,
|
|
342
|
+
maxTotalMessageChunksToFetch - totalCount
|
|
343
|
+
);
|
|
344
|
+
this.pubnub.fetchMessages(
|
|
345
|
+
{
|
|
346
|
+
channels: [this.channel],
|
|
347
|
+
count: countToFetch,
|
|
348
|
+
start: startTimeToken ?? undefined,
|
|
349
|
+
},
|
|
350
|
+
(status, response) => {
|
|
351
|
+
if (status.error) {
|
|
352
|
+
reject(status);
|
|
353
|
+
} else {
|
|
354
|
+
resolve(response);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const messages = response.channels[this.channel];
|
|
362
|
+
if (!messages) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
messages.sort((a, b) => +a.timetoken - +b.timetoken);
|
|
366
|
+
|
|
367
|
+
if (!messages || messages.length === 0) {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
if (this.channel && Object.keys(response.channels).length !== 0) {
|
|
371
|
+
const currentChannelMessages = response.channels[this.channel];
|
|
372
|
+
const parsedCurrentChannelMessages: RawPubnubMessage[] = [];
|
|
373
|
+
currentChannelMessages.forEach((message) => {
|
|
374
|
+
if (message.uuid) {
|
|
375
|
+
parsedCurrentChannelMessages.push({
|
|
376
|
+
channel: message.channel,
|
|
377
|
+
message: message.message,
|
|
378
|
+
publisher: message.uuid,
|
|
379
|
+
subscription: message.channel,
|
|
380
|
+
timetoken: +message.timetoken,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
allMessages = allMessages.concat(
|
|
385
|
+
parsedCurrentChannelMessages.filter(
|
|
386
|
+
(m) => m.message.messageType !== MessageType.noReply
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
startTimeToken = messages[0].timetoken;
|
|
391
|
+
|
|
392
|
+
if (
|
|
393
|
+
allMessages.length >= maxTotalMessageChunksToFetch ||
|
|
394
|
+
messages.length < maxCountPerFetch
|
|
395
|
+
) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.rawPubnubMessages = allMessages.slice(
|
|
400
|
+
0,
|
|
401
|
+
maxTotalMessageChunksToFetch
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
this.simpleChatMessages =
|
|
405
|
+
this.translatePubnubMessagesIntoSimpleChatMessages(
|
|
406
|
+
this.rawPubnubMessages
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
this.chatListener?.({
|
|
410
|
+
messages: this.simpleChatMessages,
|
|
411
|
+
isLoading: false,
|
|
412
|
+
});
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (this.building) {
|
|
415
|
+
sendLoggingEvent({
|
|
416
|
+
logTitle: "PUBNUB_WARN_FETCHING_HISTORY",
|
|
417
|
+
logData: { error },
|
|
418
|
+
logType: LogType.warn,
|
|
419
|
+
buildingSlug: this.buildingSlug,
|
|
420
|
+
orgSlug: this.orgSlug,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
handleChatListeners = (): void => {
|
|
427
|
+
if (!this.pubnub || !this.channel) return;
|
|
428
|
+
try {
|
|
429
|
+
this.pubnub.subscribe({ channels: [this.channel] });
|
|
430
|
+
this.pubnub.addListener(this.listenerParams);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (this.building) {
|
|
433
|
+
sendLoggingEvent({
|
|
434
|
+
logTitle: "PUBNUB_ERROR_ADDING_LISTENER",
|
|
435
|
+
logData: {
|
|
436
|
+
error,
|
|
437
|
+
channel: this.channel,
|
|
438
|
+
leadUserId: this.leadUserId,
|
|
439
|
+
website: location.href,
|
|
440
|
+
},
|
|
441
|
+
logType: LogType.error,
|
|
442
|
+
buildingSlug: this.buildingSlug,
|
|
443
|
+
orgSlug: this.orgSlug,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
handleDisconnect = (): void => {
|
|
449
|
+
if (this.eliseResponseTimeout) {
|
|
450
|
+
clearTimeout(this.eliseResponseTimeout);
|
|
451
|
+
this.eliseResponseTimeout = null;
|
|
452
|
+
}
|
|
453
|
+
this.removeChatListeners();
|
|
454
|
+
};
|
|
455
|
+
removeChatListeners = (): void => {
|
|
456
|
+
if (this.pubnub && this.channel) {
|
|
457
|
+
this.pubnub.unsubscribe({ channels: [this.channel] });
|
|
458
|
+
this.pubnub.removeListener(this.listenerParams);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
sendMessage = async (message: string): Promise<void> => {
|
|
463
|
+
if (message) {
|
|
464
|
+
if (!this.pubnub) {
|
|
465
|
+
// ONLY create/gets a chat session if user actually wants to chat
|
|
466
|
+
const chatStorageKey = createChatStorageKey(
|
|
467
|
+
this.buildingSlug,
|
|
468
|
+
true,
|
|
469
|
+
this.leadUserId
|
|
470
|
+
);
|
|
471
|
+
const myPubnub = await this.initializePubnub(chatStorageKey);
|
|
472
|
+
if (!myPubnub) return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await this.withAuthToken(async () => {
|
|
476
|
+
if (!this.pubnub || !this.channel) return;
|
|
477
|
+
|
|
478
|
+
if (this.eliseResponseTimeout) {
|
|
479
|
+
clearTimeout(this.eliseResponseTimeout);
|
|
480
|
+
this.eliseResponseTimeout = null;
|
|
481
|
+
}
|
|
482
|
+
this.eliseResponseTimeout = setTimeout(() => {
|
|
483
|
+
// eslint-disable-next-line no-console
|
|
484
|
+
console.error("Elise AI did not respond in time...");
|
|
485
|
+
if (this.building) {
|
|
486
|
+
sendLoggingEvent({
|
|
487
|
+
logTitle: "PUBNUB_ERROR_ELISEAI_MESSAGE_TIMEOUT",
|
|
488
|
+
logData: {
|
|
489
|
+
channel: this.channel,
|
|
490
|
+
leadUserId: this.leadUserId,
|
|
491
|
+
message,
|
|
492
|
+
},
|
|
493
|
+
logType: LogType.error,
|
|
494
|
+
buildingSlug: this.buildingSlug,
|
|
495
|
+
orgSlug: this.orgSlug,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}, 90000); // if after 90 seconds, no message - we log error
|
|
499
|
+
|
|
500
|
+
if (this.simpleChatMessages.length === 0) {
|
|
501
|
+
this.leadSourceClient?.checkAndHandleForLogLeadSource({
|
|
502
|
+
webchatAction: "chat",
|
|
503
|
+
stateId: null,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
await this.pubnub.publish({
|
|
507
|
+
channel: this.channel,
|
|
508
|
+
message: {
|
|
509
|
+
text: message,
|
|
510
|
+
customType: "lead_message",
|
|
511
|
+
buildingId: this.building?.id,
|
|
512
|
+
buildingSlug: this.buildingSlug,
|
|
513
|
+
userId: this.building?.userId, // this userid is actually the AI user!
|
|
514
|
+
leadSource: this.leadSource,
|
|
515
|
+
isDevState: this.shouldCreateAsDevState(),
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (!this.isFirstChatMessageSent) {
|
|
520
|
+
pushGtmEvent("firstChatMessageSent", {
|
|
521
|
+
message,
|
|
522
|
+
buildingId: this.building?.id,
|
|
523
|
+
buildingSlug: this.buildingSlug,
|
|
524
|
+
orgSlug: this.orgSlug,
|
|
525
|
+
leadUserId: this.leadUserId,
|
|
526
|
+
channel: this.channel,
|
|
527
|
+
});
|
|
528
|
+
this.isFirstChatMessageSent = true;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
pushGtmEvent("chatMessageSent", {
|
|
532
|
+
message,
|
|
533
|
+
buildingId: this.building?.id,
|
|
534
|
+
buildingSlug: this.buildingSlug,
|
|
535
|
+
orgSlug: this.orgSlug,
|
|
536
|
+
leadUserId: this.leadUserId,
|
|
537
|
+
channel: this.channel,
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (this.isLoadingMessages === false) this.isLoadingMessages = true;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
private shouldCreateAsDevState(): boolean {
|
|
545
|
+
return location.href.startsWith(
|
|
546
|
+
"https://app.meetelise.com/settings/widgets"
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
isLeadMessage = (message: RawPubnubMessage): boolean =>
|
|
551
|
+
message.publisher.includes("lead_") &&
|
|
552
|
+
message.message.customType === "lead_message";
|
|
553
|
+
|
|
554
|
+
translatePubnubMessagesIntoSimpleChatMessages = (
|
|
555
|
+
messages: RawPubnubMessage[]
|
|
556
|
+
): SimpleChatMessage[] => {
|
|
557
|
+
const parsedMessages: SimpleChatMessage[] = [];
|
|
558
|
+
const streamingIdToMessageChunks: {
|
|
559
|
+
[streamId: string]: RawPubnubMessage[];
|
|
560
|
+
} = {};
|
|
561
|
+
messages.forEach((message: RawPubnubMessage) => {
|
|
562
|
+
// Translate media messages
|
|
563
|
+
if (message.message.messageType === "media") {
|
|
564
|
+
parsedMessages.push({
|
|
565
|
+
timestamp: message.timetoken,
|
|
566
|
+
media: message.message.media ?? [],
|
|
567
|
+
isLeadMessage: this.isLeadMessage(message),
|
|
568
|
+
type: SimpleMessageTypes.media,
|
|
569
|
+
});
|
|
570
|
+
} else if (
|
|
571
|
+
// Translate non-streaming messages
|
|
572
|
+
!message.message.stream_id ||
|
|
573
|
+
(message.message.is_done && message.message.order === 0)
|
|
574
|
+
) {
|
|
575
|
+
parsedMessages.push({
|
|
576
|
+
timestamp: message.timetoken,
|
|
577
|
+
message: message.message.text,
|
|
578
|
+
isLeadMessage: this.isLeadMessage(message),
|
|
579
|
+
chunks: [
|
|
580
|
+
{
|
|
581
|
+
text: message.message.text,
|
|
582
|
+
order: 0,
|
|
583
|
+
isDone: true,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
type: SimpleMessageTypes.text,
|
|
587
|
+
});
|
|
588
|
+
} else {
|
|
589
|
+
// Translate streaming messages
|
|
590
|
+
if (streamingIdToMessageChunks[message.message.stream_id]) {
|
|
591
|
+
streamingIdToMessageChunks[message.message.stream_id].push(message);
|
|
592
|
+
} else {
|
|
593
|
+
streamingIdToMessageChunks[message.message.stream_id] = [message];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
Object.keys(streamingIdToMessageChunks).forEach((streamId) => {
|
|
598
|
+
const messages = streamingIdToMessageChunks[streamId];
|
|
599
|
+
const sortedChunks = this.getConsecutiveChunks(
|
|
600
|
+
messages.sort((a, b) => (a.message.order ?? 0) - (b.message.order ?? 0))
|
|
601
|
+
);
|
|
602
|
+
const text = sortedChunks.map((message) => message.message.text).join("");
|
|
603
|
+
const firstMessage = sortedChunks[0];
|
|
604
|
+
const newMessage: SimpleTextChatMessage = {
|
|
605
|
+
timestamp: firstMessage.timetoken,
|
|
606
|
+
message: text,
|
|
607
|
+
isLeadMessage: this.isLeadMessage(firstMessage),
|
|
608
|
+
chunks: sortedChunks.map((message) => ({
|
|
609
|
+
text: message.message.text,
|
|
610
|
+
order: message.message.order ?? 0,
|
|
611
|
+
isDone: message.message.is_done ?? false,
|
|
612
|
+
})),
|
|
613
|
+
type: SimpleMessageTypes.text,
|
|
614
|
+
};
|
|
615
|
+
this.isCurrentlyStreamingMessage =
|
|
616
|
+
this.isMessageStillStreamingChunks(sortedChunks);
|
|
617
|
+
parsedMessages.push(newMessage);
|
|
618
|
+
});
|
|
619
|
+
parsedMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
620
|
+
return parsedMessages;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
private getConsecutiveChunks = (
|
|
624
|
+
rawPubnubMessages: RawPubnubMessage[]
|
|
625
|
+
): RawPubnubMessage[] => {
|
|
626
|
+
const sortedRawPubnubMessages = rawPubnubMessages.sort(
|
|
627
|
+
(a, b) => (a.message.order ?? 0) - (b.message.order ?? 0)
|
|
628
|
+
);
|
|
629
|
+
const consecutiveMessages = [];
|
|
630
|
+
|
|
631
|
+
// get the LOWEST order that exists
|
|
632
|
+
let expectedOrder = sortedRawPubnubMessages[0].message.order ?? 0;
|
|
633
|
+
for (const message of sortedRawPubnubMessages) {
|
|
634
|
+
if ((message.message.order ?? -1) === expectedOrder) {
|
|
635
|
+
consecutiveMessages.push(message);
|
|
636
|
+
expectedOrder++;
|
|
637
|
+
} else {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return consecutiveMessages;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
private isMessageStillStreamingChunks = (
|
|
645
|
+
rawPubnubMessages: RawPubnubMessage[]
|
|
646
|
+
): boolean => {
|
|
647
|
+
const messageChunkMarkedAsDone = rawPubnubMessages.find(
|
|
648
|
+
(message) => message.message.is_done
|
|
649
|
+
);
|
|
650
|
+
if (!messageChunkMarkedAsDone) return true;
|
|
651
|
+
|
|
652
|
+
// We check to see if ALL the chunks have been received
|
|
653
|
+
return rawPubnubMessages.length === messageChunkMarkedAsDone.message.order;
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export default MyPubnub;
|