@meetelise/chat 1.20.88 → 1.20.90
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/package.json +1 -1
- package/public/demo/index.html +4 -0
- package/public/dist/index.js +121 -13
- package/src/WebComponent/me-chat.ts +2 -1
- package/src/WebComponent/pubnub-chat-styles.ts +59 -0
- package/src/WebComponent/pubnub-chat.ts +25 -6
- package/src/WebComponent/pubnub-message.ts +225 -0
- package/src/analytics.ts +9 -1
|
@@ -116,7 +116,19 @@ export const pubnubChatStyles = css`
|
|
|
116
116
|
padding: 6px 12px;
|
|
117
117
|
line-height: 130%;
|
|
118
118
|
}
|
|
119
|
+
.displayed-message-image {
|
|
120
|
+
max-width: 100%;
|
|
121
|
+
max-height: 300px;
|
|
122
|
+
width: auto;
|
|
123
|
+
height: auto;
|
|
124
|
+
}
|
|
125
|
+
.redirect-link {
|
|
126
|
+
color: black;
|
|
127
|
+
}
|
|
119
128
|
|
|
129
|
+
#loading-message {
|
|
130
|
+
padding: 16px;
|
|
131
|
+
}
|
|
120
132
|
.loading-dot {
|
|
121
133
|
width: 6px;
|
|
122
134
|
height: 6px;
|
|
@@ -188,4 +200,51 @@ export const pubnubChatStyles = css`
|
|
|
188
200
|
border: none;
|
|
189
201
|
cursor: pointer;
|
|
190
202
|
}
|
|
203
|
+
|
|
204
|
+
.website-preview {
|
|
205
|
+
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
overflow: hidden;
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
justify-content: center;
|
|
212
|
+
|
|
213
|
+
margin: 8px;
|
|
214
|
+
}
|
|
215
|
+
.website-preview-image {
|
|
216
|
+
max-width: 100%;
|
|
217
|
+
max-height: 200px;
|
|
218
|
+
width: auto;
|
|
219
|
+
height: auto;
|
|
220
|
+
}
|
|
221
|
+
.website-preview-body {
|
|
222
|
+
padding: 8px 0px 12px;
|
|
223
|
+
}
|
|
224
|
+
.website-preview-title {
|
|
225
|
+
font-size: 12px;
|
|
226
|
+
font-weight: bold;
|
|
227
|
+
margin: 0;
|
|
228
|
+
display: -webkit-box;
|
|
229
|
+
-webkit-box-orient: vertical;
|
|
230
|
+
-webkit-line-clamp: 2;
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
text-overflow: ellipsis;
|
|
233
|
+
height: 24px;
|
|
234
|
+
}
|
|
235
|
+
.website-preview-description {
|
|
236
|
+
font-size: 10px;
|
|
237
|
+
margin: 0;
|
|
238
|
+
|
|
239
|
+
display: -webkit-box;
|
|
240
|
+
-webkit-box-orient: vertical;
|
|
241
|
+
-webkit-line-clamp: 3;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
max-height: 34px;
|
|
245
|
+
}
|
|
246
|
+
.redirect-link-preview {
|
|
247
|
+
color: inherit;
|
|
248
|
+
text-decoration: none;
|
|
249
|
+
}
|
|
191
250
|
`;
|
|
@@ -8,6 +8,7 @@ import { SendMessageIconWhite, XBlackOutlineIcon } from "../svgIcons";
|
|
|
8
8
|
import { defaultBrandColor } from "../themes";
|
|
9
9
|
import { hexToAlmostWhite } from "../utils";
|
|
10
10
|
import { pubnubChatStyles } from "./pubnub-chat-styles";
|
|
11
|
+
import "./pubnub-message";
|
|
11
12
|
|
|
12
13
|
@customElement("pubnub-chat")
|
|
13
14
|
export class PubnubChat extends LitElement {
|
|
@@ -49,6 +50,17 @@ export class PubnubChat extends LitElement {
|
|
|
49
50
|
@state()
|
|
50
51
|
isLoadingMessages = false;
|
|
51
52
|
|
|
53
|
+
@state()
|
|
54
|
+
websitePreviewMapping: {
|
|
55
|
+
[messageId: string]: {
|
|
56
|
+
title: string | null;
|
|
57
|
+
description: string | null;
|
|
58
|
+
image: string | null;
|
|
59
|
+
favicon: string | null;
|
|
60
|
+
isLoading: boolean;
|
|
61
|
+
}[];
|
|
62
|
+
} = {};
|
|
63
|
+
|
|
52
64
|
sendMessage = async (message: string): Promise<void> => {
|
|
53
65
|
this.messageInput.value = "";
|
|
54
66
|
await this.myPubnub?.sendMessage(message);
|
|
@@ -62,7 +74,13 @@ export class PubnubChat extends LitElement {
|
|
|
62
74
|
}
|
|
63
75
|
);
|
|
64
76
|
this.onMount();
|
|
77
|
+
// TODO (erol): scroll to bottom on all children load
|
|
78
|
+
// this is a hacky way to do it
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
this.scrollToChatBottom();
|
|
81
|
+
}, 1000);
|
|
65
82
|
}
|
|
83
|
+
|
|
66
84
|
scrollToChatBottom = (): void => {
|
|
67
85
|
this.messageBody.scrollTo({
|
|
68
86
|
top: this.messageBody.scrollHeight - this.messageBody.clientHeight,
|
|
@@ -70,7 +88,7 @@ export class PubnubChat extends LitElement {
|
|
|
70
88
|
});
|
|
71
89
|
};
|
|
72
90
|
|
|
73
|
-
updated(): void {
|
|
91
|
+
async updated(): Promise<void> {
|
|
74
92
|
this.scrollToChatBottom();
|
|
75
93
|
}
|
|
76
94
|
|
|
@@ -133,11 +151,12 @@ export class PubnubChat extends LitElement {
|
|
|
133
151
|
: undefined,
|
|
134
152
|
})}
|
|
135
153
|
>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
154
|
+
<pubnub-message
|
|
155
|
+
.message=${message}
|
|
156
|
+
.myPubnub=${this.myPubnub}
|
|
157
|
+
.onMount=${() => this.scrollToChatBottom()}
|
|
158
|
+
>
|
|
159
|
+
</pubnub-message>
|
|
141
160
|
</li>
|
|
142
161
|
`;
|
|
143
162
|
})}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { html, LitElement, TemplateResult } from "lit";
|
|
2
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import MyPubnub, { ChatMessage } from "../MyPubnub";
|
|
4
|
+
import { pubnubChatStyles } from "./pubnub-chat-styles";
|
|
5
|
+
import { v4 as uuid } from "uuid";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
|
|
8
|
+
interface WebsitePreview {
|
|
9
|
+
title: string | null;
|
|
10
|
+
description: string | null;
|
|
11
|
+
image: string | null;
|
|
12
|
+
favicon: string | null;
|
|
13
|
+
link: string;
|
|
14
|
+
}
|
|
15
|
+
@customElement("pubnub-message")
|
|
16
|
+
export class PubnubMessage extends LitElement {
|
|
17
|
+
static styles = [pubnubChatStyles];
|
|
18
|
+
|
|
19
|
+
@property({ attribute: true })
|
|
20
|
+
message: ChatMessage | undefined;
|
|
21
|
+
|
|
22
|
+
@property({ attribute: true })
|
|
23
|
+
myPubnub: MyPubnub | undefined;
|
|
24
|
+
|
|
25
|
+
@property({ attribute: true })
|
|
26
|
+
onMount: () => void = () => ({});
|
|
27
|
+
|
|
28
|
+
@state()
|
|
29
|
+
loadingPreviews = true;
|
|
30
|
+
|
|
31
|
+
@state()
|
|
32
|
+
parsedMessage: TemplateResult | undefined;
|
|
33
|
+
|
|
34
|
+
@state()
|
|
35
|
+
websitePreviewInfo: WebsitePreview[] = [];
|
|
36
|
+
|
|
37
|
+
firstUpdated(): void {
|
|
38
|
+
if (!this.message) return;
|
|
39
|
+
const { hyperlinks, markedText } = this.mapAndMarkHyperLinks(
|
|
40
|
+
this.message.message.text
|
|
41
|
+
);
|
|
42
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g; // Regular expression to match URLs
|
|
43
|
+
const imagesToDisplay: {
|
|
44
|
+
redirectTo: string;
|
|
45
|
+
source: string;
|
|
46
|
+
}[] = [];
|
|
47
|
+
|
|
48
|
+
const loadingWebsitePreviews: Promise<WebsitePreview>[] = [];
|
|
49
|
+
this.parsedMessage = html`${markedText.split("\n").map((line) => {
|
|
50
|
+
return html`${line.split(" ").map((word) => {
|
|
51
|
+
if (hyperlinks.find((obj) => obj.id === word)) {
|
|
52
|
+
// EliseAI made hyperlink
|
|
53
|
+
const link = hyperlinks.find((obj) => obj.id === word);
|
|
54
|
+
if (!link) return;
|
|
55
|
+
imagesToDisplay.push({
|
|
56
|
+
redirectTo: link?.link,
|
|
57
|
+
source: link?.link,
|
|
58
|
+
});
|
|
59
|
+
return html`<a
|
|
60
|
+
class="redirect-link"
|
|
61
|
+
href="${link?.link}"
|
|
62
|
+
target="_blank"
|
|
63
|
+
rel="noopener noreferrer"
|
|
64
|
+
>${link?.hyperlink}
|
|
65
|
+
</a>`;
|
|
66
|
+
}
|
|
67
|
+
if (urlRegex.test(word)) {
|
|
68
|
+
// general url
|
|
69
|
+
loadingWebsitePreviews.push(this.getLinkData(word));
|
|
70
|
+
return html`<a
|
|
71
|
+
class="redirect-link"
|
|
72
|
+
href="${word}"
|
|
73
|
+
target="_blank"
|
|
74
|
+
rel="noopener noreferrer"
|
|
75
|
+
>${word}
|
|
76
|
+
</a>`;
|
|
77
|
+
} else {
|
|
78
|
+
return html`${word} `;
|
|
79
|
+
}
|
|
80
|
+
})}<br />`;
|
|
81
|
+
})}
|
|
82
|
+
${imagesToDisplay.map((image) => {
|
|
83
|
+
return html`<a
|
|
84
|
+
href="${image.redirectTo}"
|
|
85
|
+
target="_blank"
|
|
86
|
+
rel="noopener noreferrer"
|
|
87
|
+
><img
|
|
88
|
+
class="displayed-message-image"
|
|
89
|
+
src=${image.source}
|
|
90
|
+
alt="displayed message image"
|
|
91
|
+
/></a>`;
|
|
92
|
+
})}`;
|
|
93
|
+
Promise.all(loadingWebsitePreviews).then((results) => {
|
|
94
|
+
this.websitePreviewInfo = results;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private mapAndMarkHyperLinks(text: string): {
|
|
99
|
+
hyperlinks: {
|
|
100
|
+
link: string;
|
|
101
|
+
hyperlink: string;
|
|
102
|
+
mark: string;
|
|
103
|
+
id: string;
|
|
104
|
+
}[];
|
|
105
|
+
markedText: string;
|
|
106
|
+
} {
|
|
107
|
+
const matches = Array.from(text.matchAll(/<https?:\/\/[^\s]+\|[^>]+>/g));
|
|
108
|
+
const listHyperlinks: {
|
|
109
|
+
link: string;
|
|
110
|
+
hyperlink: string;
|
|
111
|
+
mark: string;
|
|
112
|
+
id: string;
|
|
113
|
+
}[] = [];
|
|
114
|
+
matches.forEach((match) => {
|
|
115
|
+
try {
|
|
116
|
+
const link = match[0].split("|")[0].replace("<", "");
|
|
117
|
+
const hyperlink = match[0].split("|")[1].replace(">", "");
|
|
118
|
+
if (link && hyperlink) {
|
|
119
|
+
const uniqueKeyMark = uuid();
|
|
120
|
+
listHyperlinks.push({
|
|
121
|
+
link,
|
|
122
|
+
hyperlink,
|
|
123
|
+
mark: match[0],
|
|
124
|
+
id: uniqueKeyMark,
|
|
125
|
+
});
|
|
126
|
+
text = text.replace(match[0], uniqueKeyMark);
|
|
127
|
+
}
|
|
128
|
+
} catch (_) {
|
|
129
|
+
// pass
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return { hyperlinks: listHyperlinks, markedText: text };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async getLinkData(url: string): Promise<WebsitePreview> {
|
|
137
|
+
try {
|
|
138
|
+
// Getting link data is unlikely to work unless the site has CORS enabled and/or url is on the same domain
|
|
139
|
+
// For dev testing, can visit https://cors-anywhere.herokuapp.com/ and prefix url with it to bypass CORS
|
|
140
|
+
const response = await axios.get(url);
|
|
141
|
+
const html = response.data;
|
|
142
|
+
const parser = new DOMParser(); // creates a temp dom to parse HTML
|
|
143
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
144
|
+
if (!doc)
|
|
145
|
+
return {
|
|
146
|
+
title: null,
|
|
147
|
+
description: null,
|
|
148
|
+
image: null,
|
|
149
|
+
favicon: null,
|
|
150
|
+
link: url,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
title: doc.querySelector("title")?.textContent ?? null,
|
|
155
|
+
description:
|
|
156
|
+
doc
|
|
157
|
+
.querySelector('meta[name="description"]')
|
|
158
|
+
?.getAttribute("content") || null,
|
|
159
|
+
image:
|
|
160
|
+
doc
|
|
161
|
+
.querySelector('meta[property="og:image"]')
|
|
162
|
+
?.getAttribute("content") || null,
|
|
163
|
+
favicon:
|
|
164
|
+
doc
|
|
165
|
+
.querySelector('link[rel="shortcut icon"]')
|
|
166
|
+
?.getAttribute("href") ||
|
|
167
|
+
doc.querySelector('link[rel="icon"]')?.getAttribute("href") ||
|
|
168
|
+
null,
|
|
169
|
+
link: url,
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.error("Error fetching website details:", error);
|
|
174
|
+
return {
|
|
175
|
+
title: null,
|
|
176
|
+
description: null,
|
|
177
|
+
image: null,
|
|
178
|
+
favicon: null,
|
|
179
|
+
link: url,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
updated(): void {
|
|
185
|
+
this.onMount();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
render(): TemplateResult {
|
|
189
|
+
if (!this.message) return html``;
|
|
190
|
+
return html`<div class="message-inner-body">
|
|
191
|
+
<p class="message-text">${this.parsedMessage}</p>
|
|
192
|
+
${this.websitePreviewInfo.length > 0 &&
|
|
193
|
+
this.websitePreviewInfo.some(
|
|
194
|
+
(preview) => preview.title && (preview.image || preview.favicon)
|
|
195
|
+
)
|
|
196
|
+
? html`<br />`
|
|
197
|
+
: ""}
|
|
198
|
+
${this.websitePreviewInfo.map((preview) => {
|
|
199
|
+
if (!preview.title || !(preview.image || preview.favicon)) return;
|
|
200
|
+
return html` <a
|
|
201
|
+
class="redirect-link-preview"
|
|
202
|
+
href=${preview.link}
|
|
203
|
+
target="_blank"
|
|
204
|
+
rel="noopener noreferrer"
|
|
205
|
+
>
|
|
206
|
+
<div class="website-preview">
|
|
207
|
+
<img
|
|
208
|
+
src=${preview.image ?? preview.favicon}
|
|
209
|
+
class="website-preview-image"
|
|
210
|
+
alt="website preview image"
|
|
211
|
+
width="100%"
|
|
212
|
+
height="100%"
|
|
213
|
+
/>
|
|
214
|
+
<div class="website-preview-body">
|
|
215
|
+
<p class="message-text website-preview-title">${preview.title}</p>
|
|
216
|
+
<p class="message-text website-preview-description">
|
|
217
|
+
${preview.description}
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</a></div> `;
|
|
222
|
+
})}
|
|
223
|
+
</div>`;
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/analytics.ts
CHANGED
|
@@ -19,11 +19,18 @@ export default class Analytics {
|
|
|
19
19
|
private building: string;
|
|
20
20
|
private featureFlags?: Record<string, boolean>;
|
|
21
21
|
public chatId: string;
|
|
22
|
+
private incomingProcessedLeadSource: string | null;
|
|
22
23
|
|
|
23
|
-
constructor(
|
|
24
|
+
constructor(
|
|
25
|
+
org: string,
|
|
26
|
+
building: string,
|
|
27
|
+
chatId: string,
|
|
28
|
+
incomingProcessedLeadSource: string | null
|
|
29
|
+
) {
|
|
24
30
|
this.org = org;
|
|
25
31
|
this.building = building;
|
|
26
32
|
this.chatId = chatId;
|
|
33
|
+
this.incomingProcessedLeadSource = incomingProcessedLeadSource;
|
|
27
34
|
this.featureFlags = {};
|
|
28
35
|
}
|
|
29
36
|
|
|
@@ -57,6 +64,7 @@ export default class Analytics {
|
|
|
57
64
|
referrer: document.referrer,
|
|
58
65
|
featureFlags: this.featureFlags,
|
|
59
66
|
campaignSources: getCampaignSources(),
|
|
67
|
+
incomingProcessedLeadSource: this.incomingProcessedLeadSource, // if we already know the lead source, send it along
|
|
60
68
|
}),
|
|
61
69
|
}
|
|
62
70
|
);
|