@primexperts.co/pulse-agent 5.8.3 → 6.0.1
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/README.md +26 -449
- package/dist/browser/pulse-agent.global.js +167 -237
- package/dist/widget/pulse-agent.d.ts +1 -17
- package/dist/widget/pulse-agent.d.ts.map +1 -1
- package/dist/widget/pulse-agent.js +167 -237
- package/dist/widget/pulse-agent.js.map +1 -1
- package/docs/CHANGELOG.md +6 -0
- package/docs/DEVELOPER.md +19 -1
- package/docs/IMPLEMENTATION-GUIDE.md +18 -333
- package/package.json +2 -3
|
@@ -9,7 +9,7 @@ function createPulseAgent(options) {
|
|
|
9
9
|
const mountRoot = resolveMount(state.options.mount);
|
|
10
10
|
const root = document.createElement('section');
|
|
11
11
|
root.className = 'pulse-agent-root';
|
|
12
|
-
const title = state.options.title ? `${state.options.title}
|
|
12
|
+
const title = state.options.title ? `${state.options.title} · Help` : 'How can we help?';
|
|
13
13
|
root.innerHTML = template(title);
|
|
14
14
|
mountRoot.appendChild(root);
|
|
15
15
|
const fabs = root.querySelector('[data-role="fabs"]');
|
|
@@ -27,12 +27,49 @@ function createPulseAgent(options) {
|
|
|
27
27
|
const convoIdLabel = root.querySelector('[data-role="conversation-id"]');
|
|
28
28
|
const nameInput = root.querySelector('[data-role="name"]');
|
|
29
29
|
const emailInput = root.querySelector('[data-role="email"]');
|
|
30
|
+
const phoneInput = root.querySelector('[data-role="phone"]');
|
|
31
|
+
const privacyLinks = root.querySelectorAll('[data-role="privacy-link"]');
|
|
32
|
+
updatePrivacyLinks(privacyLinks, state);
|
|
33
|
+
phoneInput.value = state.profile?.phone || '';
|
|
30
34
|
const continueBtn = root.querySelector('[data-role="continue"]');
|
|
31
35
|
const textarea = root.querySelector('[data-role="message"]');
|
|
36
|
+
const requestTypeSelect = root.querySelector('[data-role="request-type"]');
|
|
37
|
+
const requestHint = root.querySelector('[data-role="request-hint"]');
|
|
38
|
+
const requestReferenceWrap = root.querySelector('[data-role="request-reference-wrap"]');
|
|
39
|
+
const requestReference = root.querySelector('[data-role="request-reference"]');
|
|
40
|
+
const intake = root.querySelector('[data-role="intake"]');
|
|
41
|
+
const review = root.querySelector('[data-role="review"]');
|
|
42
|
+
const reviewDetails = root.querySelector('[data-role="review-details"]');
|
|
43
|
+
const reviewConfirm = root.querySelector('[data-role="review-confirm"]');
|
|
44
|
+
const reviewEdit = root.querySelector('[data-role="review-edit"]');
|
|
45
|
+
let reviewSnapshot = '';
|
|
32
46
|
const sendBtn = root.querySelector('[data-role="send"]');
|
|
33
47
|
const newConversationBtn = root.querySelector('[data-role="new-conversation"]');
|
|
34
48
|
const copyConversationBtn = root.querySelector('[data-role="copy-conversation"]');
|
|
35
49
|
const emailHistoryBtn = root.querySelector('[data-role="email-history"]');
|
|
50
|
+
reviewConfirm.addEventListener('click', () => sendBtn.click());
|
|
51
|
+
reviewEdit.addEventListener('click', () => { review.classList.add('hidden'); reviewSnapshot = ''; textarea.focus(); });
|
|
52
|
+
const refreshIntake = () => {
|
|
53
|
+
intake.classList.toggle('hidden', !!state.conversationId || state.requestTypes.length === 0);
|
|
54
|
+
const selected = state.requestTypes.find(type => type.code === requestTypeSelect.value);
|
|
55
|
+
requestHint.textContent = selected?.description || 'Choose what you need so the team can route your request.';
|
|
56
|
+
textarea.placeholder = selected?.question || 'How can we help?';
|
|
57
|
+
requestReferenceWrap.classList.toggle('hidden', !selected?.referenceLabel);
|
|
58
|
+
requestReferenceWrap.firstChild.textContent = selected?.referenceLabel || '';
|
|
59
|
+
if (!selected?.referenceLabel)
|
|
60
|
+
requestReference.value = '';
|
|
61
|
+
};
|
|
62
|
+
const syncIntakeOptions = () => {
|
|
63
|
+
const selected = requestTypeSelect.value;
|
|
64
|
+
requestTypeSelect.replaceChildren(new Option('Choose what you need', ''));
|
|
65
|
+
for (const type of state.requestTypes)
|
|
66
|
+
requestTypeSelect.add(new Option(type.label, type.code));
|
|
67
|
+
if (state.requestTypes.some(type => type.code === selected))
|
|
68
|
+
requestTypeSelect.value = selected;
|
|
69
|
+
refreshIntake();
|
|
70
|
+
};
|
|
71
|
+
requestTypeSelect.addEventListener('change', refreshIntake);
|
|
72
|
+
refreshIntake();
|
|
36
73
|
syncProfileGate(state, prechat, chatUi, nameInput, emailInput);
|
|
37
74
|
if (state.messages.length > 0) {
|
|
38
75
|
renderMessages(log, state.messages);
|
|
@@ -47,7 +84,6 @@ function createPulseAgent(options) {
|
|
|
47
84
|
state.unreadBadge = unreadBadge;
|
|
48
85
|
state.launcherUnreadBadge = launcherUnreadBadge;
|
|
49
86
|
refreshUnreadIndicator(state);
|
|
50
|
-
installErrorMonitoring(state);
|
|
51
87
|
launcherBtn.addEventListener('click', () => toggleFabActions(fabs, launcherBtn));
|
|
52
88
|
button.addEventListener('click', () => {
|
|
53
89
|
closeFabActions(fabs, launcherBtn);
|
|
@@ -62,12 +98,34 @@ function createPulseAgent(options) {
|
|
|
62
98
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
63
99
|
});
|
|
64
100
|
closeBtn.addEventListener('click', () => close(panel, state));
|
|
101
|
+
const dismissOutside = (event) => {
|
|
102
|
+
if (!event.composedPath().includes(root)) {
|
|
103
|
+
if (state.opened)
|
|
104
|
+
close(panel, state);
|
|
105
|
+
closeFabActions(fabs, launcherBtn);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const dismissOnEscape = (event) => {
|
|
109
|
+
if (event.key !== 'Escape' || event.defaultPrevented || !state.opened)
|
|
110
|
+
return;
|
|
111
|
+
const restoreFocus = root.contains(document.activeElement);
|
|
112
|
+
close(panel, state);
|
|
113
|
+
closeFabActions(fabs, launcherBtn);
|
|
114
|
+
if (restoreFocus)
|
|
115
|
+
(launcherBtn.hidden ? button : launcherBtn).focus();
|
|
116
|
+
};
|
|
117
|
+
document.addEventListener('pointerdown', dismissOutside, true);
|
|
118
|
+
document.addEventListener('keydown', dismissOnEscape);
|
|
65
119
|
continueBtn.addEventListener('click', () => {
|
|
66
|
-
const profile = readVisitorProfile(nameInput.value, emailInput.value);
|
|
120
|
+
const profile = readVisitorProfile(nameInput.value, emailInput.value, phoneInput.value);
|
|
67
121
|
if (!profile) {
|
|
68
122
|
pushMessage(state, log, { role: 'system', content: 'Name and email are required before chat starts.' });
|
|
69
123
|
return;
|
|
70
124
|
}
|
|
125
|
+
if (profile.phone && !/^\+[1-9][0-9]{7,14}$/.test(profile.phone.replace(/[ ()-]/g, ''))) {
|
|
126
|
+
pushMessage(state, log, { role: 'system', content: 'For a callback, include the country code, for example +27821234567.' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
71
129
|
state.profile = profile;
|
|
72
130
|
persistSession(state);
|
|
73
131
|
syncProfileGate(state, prechat, chatUi, nameInput, emailInput);
|
|
@@ -75,6 +133,11 @@ function createPulseAgent(options) {
|
|
|
75
133
|
newConversationBtn.addEventListener('click', () => {
|
|
76
134
|
resetConversation(state, log);
|
|
77
135
|
refreshConversationMeta(state, convoMeta, convoIdLabel);
|
|
136
|
+
requestTypeSelect.value = '';
|
|
137
|
+
requestReference.value = '';
|
|
138
|
+
refreshIntake();
|
|
139
|
+
review.classList.add('hidden');
|
|
140
|
+
reviewSnapshot = '';
|
|
78
141
|
});
|
|
79
142
|
copyConversationBtn.addEventListener('click', async () => {
|
|
80
143
|
if (!state.conversationId) {
|
|
@@ -137,15 +200,30 @@ function createPulseAgent(options) {
|
|
|
137
200
|
return;
|
|
138
201
|
}
|
|
139
202
|
if (!state.widgetToken) {
|
|
140
|
-
await resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn);
|
|
203
|
+
await resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn, privacyLinks);
|
|
204
|
+
syncIntakeOptions();
|
|
141
205
|
if (!state.widgetToken) {
|
|
142
206
|
return;
|
|
143
207
|
}
|
|
144
208
|
}
|
|
209
|
+
const selectedType = state.conversationId ? null : state.requestTypes.find(type => type.code === requestTypeSelect.value);
|
|
210
|
+
if (!state.conversationId && state.requestTypes.length && !selectedType) {
|
|
211
|
+
pushMessage(state, log, { role: 'system', content: 'Choose what you need before sending your first message.' });
|
|
212
|
+
requestTypeSelect.focus();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!state.conversationId && selectedType) {
|
|
216
|
+
const snapshot = JSON.stringify([selectedType.code, requestReference.value.trim(), text]);
|
|
217
|
+
if (reviewSnapshot !== snapshot) {
|
|
218
|
+
reviewSnapshot = snapshot;
|
|
219
|
+
reviewDetails.textContent = `${selectedType.label}${requestReference.value.trim() ? ' · ' + selectedType.referenceLabel + ': ' + requestReference.value.trim() : ''}\n${text}`;
|
|
220
|
+
review.classList.remove('hidden');
|
|
221
|
+
reviewConfirm.focus();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
145
225
|
state.sending = true;
|
|
146
226
|
sendBtn.disabled = true;
|
|
147
|
-
pushMessage(state, log, { role: 'visitor', content: text });
|
|
148
|
-
textarea.value = '';
|
|
149
227
|
try {
|
|
150
228
|
const clientSessionId = ensureClientSessionId(state);
|
|
151
229
|
const payload = state.profile
|
|
@@ -156,9 +234,11 @@ function createPulseAgent(options) {
|
|
|
156
234
|
phone: state.profile.phone,
|
|
157
235
|
pulseUserId: state.profile.id,
|
|
158
236
|
conversationId: state.conversationId,
|
|
159
|
-
clientSessionId
|
|
237
|
+
clientSessionId,
|
|
238
|
+
...(!state.conversationId && selectedType ? { requestTypeCode: selectedType.code, customerReference: requestReference.value.trim() } : {})
|
|
160
239
|
}
|
|
161
|
-
: { message: text, conversationId: state.conversationId, clientSessionId
|
|
240
|
+
: { message: text, conversationId: state.conversationId, clientSessionId,
|
|
241
|
+
...(!state.conversationId && selectedType ? { requestTypeCode: selectedType.code, customerReference: requestReference.value.trim() } : {}) };
|
|
162
242
|
const res = await fetch(`${trimSlash(state.options.apiBaseUrl)}/api/pulse/public/webchat/${state.widgetToken}/message`, {
|
|
163
243
|
method: 'POST',
|
|
164
244
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -168,18 +248,26 @@ function createPulseAgent(options) {
|
|
|
168
248
|
throw new Error(`Failed to send message (${res.status})`);
|
|
169
249
|
}
|
|
170
250
|
const body = (await res.json());
|
|
251
|
+
pushMessage(state, log, { role: 'visitor', content: text });
|
|
252
|
+
textarea.value = '';
|
|
171
253
|
const conversationId = body.conversationId ?? body.id;
|
|
172
254
|
if (!conversationId) {
|
|
173
255
|
pushMessage(state, log, { role: 'system', content: 'Message sent but conversation ID was missing.' });
|
|
174
256
|
}
|
|
175
257
|
else if (!state.conversationId) {
|
|
176
258
|
state.conversationId = conversationId;
|
|
259
|
+
review.classList.add('hidden');
|
|
260
|
+
reviewSnapshot = '';
|
|
261
|
+
refreshIntake();
|
|
177
262
|
persistSession(state);
|
|
178
263
|
connectStream(state, log);
|
|
179
264
|
refreshConversationMeta(state, convoMeta, convoIdLabel);
|
|
180
265
|
}
|
|
181
266
|
else if (state.conversationId !== conversationId) {
|
|
182
267
|
state.conversationId = conversationId;
|
|
268
|
+
review.classList.add('hidden');
|
|
269
|
+
reviewSnapshot = '';
|
|
270
|
+
refreshIntake();
|
|
183
271
|
pushMessage(state, log, { role: 'system', content: 'Previous conversation was closed. Started a new conversation.' });
|
|
184
272
|
persistSession(state);
|
|
185
273
|
connectStream(state, log);
|
|
@@ -196,13 +284,13 @@ function createPulseAgent(options) {
|
|
|
196
284
|
sendBtn.disabled = false;
|
|
197
285
|
}
|
|
198
286
|
});
|
|
199
|
-
resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn).then(async () => {
|
|
287
|
+
resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn, privacyLinks).then(async () => {
|
|
288
|
+
syncIntakeOptions();
|
|
200
289
|
if (state.conversationId) {
|
|
201
290
|
await syncConversationHistory(state, log);
|
|
202
291
|
connectStream(state, log);
|
|
203
292
|
refreshConversationMeta(state, convoMeta, convoIdLabel);
|
|
204
293
|
}
|
|
205
|
-
flushPendingErrors(state);
|
|
206
294
|
if (state.options.autoOpen) {
|
|
207
295
|
open(panel, state);
|
|
208
296
|
}
|
|
@@ -214,6 +302,8 @@ function createPulseAgent(options) {
|
|
|
214
302
|
open: () => open(panel, state),
|
|
215
303
|
close: () => close(panel, state),
|
|
216
304
|
destroy: () => {
|
|
305
|
+
document.removeEventListener('pointerdown', dismissOutside, true);
|
|
306
|
+
document.removeEventListener('keydown', dismissOnEscape);
|
|
217
307
|
if (state.stream) {
|
|
218
308
|
state.stream.close();
|
|
219
309
|
}
|
|
@@ -225,23 +315,26 @@ function createPulseAgent(options) {
|
|
|
225
315
|
window.clearTimeout(state.reconnectTimer);
|
|
226
316
|
state.reconnectTimer = null;
|
|
227
317
|
}
|
|
228
|
-
uninstallErrorMonitoring(state);
|
|
229
318
|
root.remove();
|
|
230
319
|
},
|
|
231
320
|
resetConversation: () => {
|
|
232
321
|
resetConversation(state, log);
|
|
322
|
+
requestTypeSelect.value = '';
|
|
323
|
+
requestReference.value = '';
|
|
324
|
+
refreshIntake();
|
|
325
|
+
review.classList.add('hidden');
|
|
326
|
+
reviewSnapshot = '';
|
|
233
327
|
},
|
|
234
328
|
identify: (profile) => {
|
|
235
329
|
state.profile = normalizeVisitorProfile(profile) ?? null;
|
|
330
|
+
phoneInput.value = state.profile?.phone || '';
|
|
236
331
|
persistSession(state);
|
|
237
332
|
syncProfileGate(state, prechat, chatUi, nameInput, emailInput);
|
|
238
333
|
},
|
|
239
|
-
captureException: (error, context) => {
|
|
240
|
-
captureException(state, error, context);
|
|
241
|
-
},
|
|
242
|
-
getErrorReportCount: (days = 7) => getRecentErrorReportCount(state, days),
|
|
243
334
|
update: (partial) => {
|
|
244
335
|
state.options = normalizeOptions({ ...state.options, ...partial });
|
|
336
|
+
updatePrivacyLinks(privacyLinks, state);
|
|
337
|
+
refreshIntake();
|
|
245
338
|
if (partial.widgetToken) {
|
|
246
339
|
state.widgetToken = partial.widgetToken.trim();
|
|
247
340
|
persistSession(state);
|
|
@@ -302,12 +395,21 @@ function template(title) {
|
|
|
302
395
|
<div class="pulse-agent-log" data-role="log"></div>
|
|
303
396
|
<div class="pulse-agent-compose">
|
|
304
397
|
<div class="pulse-agent-prechat" data-role="prechat">
|
|
305
|
-
<
|
|
306
|
-
<input data-role="
|
|
398
|
+
<p class="pulse-agent-intro">Share a few details so the team can reply.</p>
|
|
399
|
+
<label>Name<input data-role="name" type="text" autocomplete="name" placeholder="Your name" /></label>
|
|
400
|
+
<label>Email<input data-role="email" type="email" autocomplete="email" placeholder="you@example.com" /></label>
|
|
401
|
+
<label>Phone <span class="pulse-agent-optional">(optional, for a callback)</span><input data-role="phone" type="tel" inputmode="tel" autocomplete="tel" maxlength="32" placeholder="+27821234567" /></label>
|
|
402
|
+
<p class="pulse-agent-privacy">Your details and messages go to this team. Phone is optional for a callback. <a data-role="privacy-link" target="_blank" rel="noopener noreferrer">Privacy notice</a></p>
|
|
307
403
|
<button data-role="continue">Continue</button>
|
|
308
404
|
</div>
|
|
309
405
|
<div class="pulse-agent-chat-ui" data-role="chat-ui">
|
|
406
|
+
<p class="pulse-agent-privacy">Messages go to this team. <a data-role="privacy-link" target="_blank" rel="noopener noreferrer">Privacy notice</a></p>
|
|
310
407
|
<button class="pulse-agent-new-conversation" data-role="new-conversation" type="button">Start new conversation</button>
|
|
408
|
+
<div class="pulse-agent-intake" data-role="intake">
|
|
409
|
+
<label>What do you need?<select data-role="request-type"><option value="">Choose what you need</option></select></label>
|
|
410
|
+
<p data-role="request-hint">Choose what you need so the team can route your request.</p>
|
|
411
|
+
<label data-role="request-reference-wrap" class="hidden">Reference<input data-role="request-reference" type="text" maxlength="100" autocomplete="off" /></label>
|
|
412
|
+
</div>
|
|
311
413
|
<div class="pulse-agent-input-wrap">
|
|
312
414
|
<textarea data-role="message" rows="3" placeholder="How can we help?"></textarea>
|
|
313
415
|
<button class="pulse-agent-send-icon" data-role="send" type="button" aria-label="Send message">
|
|
@@ -316,6 +418,7 @@ function template(title) {
|
|
|
316
418
|
</svg>
|
|
317
419
|
</button>
|
|
318
420
|
</div>
|
|
421
|
+
<div class="pulse-agent-review hidden" data-role="review"><strong>Check your request before sending</strong><p data-role="review-details"></p><div><button type="button" data-role="review-edit">Edit</button><button type="button" data-role="review-confirm">Confirm and send</button></div></div>
|
|
319
422
|
</div>
|
|
320
423
|
</div>
|
|
321
424
|
</div>`;
|
|
@@ -443,6 +546,23 @@ function ensureStyle() {
|
|
|
443
546
|
linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
|
444
547
|
}
|
|
445
548
|
.pulse-agent-prechat { display: grid; gap: 6px; }
|
|
549
|
+
.pulse-agent-prechat label { display: block; color: #344054; font-size: 12px; font-weight: 700; }
|
|
550
|
+
.pulse-agent-prechat label input { display: block; margin-top: 4px; }
|
|
551
|
+
.pulse-agent-optional { color: #667085; font-weight: 400; }
|
|
552
|
+
.pulse-agent-intro, .pulse-agent-privacy { margin: 0; color: #526679; font-size: 12px; line-height: 1.45; }
|
|
553
|
+
.pulse-agent-intake { display: grid; gap: 8px; padding: 10px; border: 1px solid #cbdcf7; border-radius: 8px; background: #f8fbff; }
|
|
554
|
+
.pulse-agent-intake label { display: grid; gap: 5px; font-size: 12px; font-weight: 600; color: #0b1f3a; }
|
|
555
|
+
.pulse-agent-intake select, .pulse-agent-intake input { width: 100%; box-sizing: border-box; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; font: inherit; }
|
|
556
|
+
.pulse-agent-intake p { margin: 0; color: #526679; font-size: 12px; line-height: 1.45; }
|
|
557
|
+
.pulse-agent-intake.hidden, .pulse-agent-intake .hidden { display: none; }
|
|
558
|
+
.pulse-agent-review { display: grid; gap: 8px; padding: 10px; border: 1px solid #8ab4f8; border-radius: 8px; background: #f4f8ff; font-size: 12px; }
|
|
559
|
+
.pulse-agent-review.hidden { display: none; }
|
|
560
|
+
.pulse-agent-review p { white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; max-height: 140px; overflow: auto; }
|
|
561
|
+
.pulse-agent-review div { display: flex; gap: 8px; justify-content: end; }
|
|
562
|
+
.pulse-agent-review button { border: 1px solid #005bbb; border-radius: 6px; padding: 7px 9px; background: white; color: #005bbb; cursor: pointer; }
|
|
563
|
+
.pulse-agent-review button:last-child { background: #005bbb; color: white; }
|
|
564
|
+
.pulse-agent-privacy { padding: 8px 10px; border: 1px solid #cbdcf7; border-radius: 8px; background: #f4f8ff; }
|
|
565
|
+
.pulse-agent-privacy a { color: #0047d9; font-weight: 700; }
|
|
446
566
|
.pulse-agent-prechat.hidden { display: none; }
|
|
447
567
|
.pulse-agent-chat-ui.hidden { display: none; }
|
|
448
568
|
.pulse-agent-compose textarea, .pulse-agent-prechat input {
|
|
@@ -540,6 +660,8 @@ function createState(options) {
|
|
|
540
660
|
options: normalized,
|
|
541
661
|
widgetToken: null,
|
|
542
662
|
resolved: false,
|
|
663
|
+
resolvedPrivacyNoticeUrl: null,
|
|
664
|
+
requestTypes: [],
|
|
543
665
|
whatsappAllowed: false,
|
|
544
666
|
profile: normalized.visitorProfile !== undefined ? normalized.visitorProfile : persisted.profile ?? null,
|
|
545
667
|
conversationId: persisted.conversationId?.trim() || null,
|
|
@@ -554,11 +676,7 @@ function createState(options) {
|
|
|
554
676
|
toggleButton: null,
|
|
555
677
|
launcherButton: null,
|
|
556
678
|
unreadBadge: null,
|
|
557
|
-
launcherUnreadBadge: null
|
|
558
|
-
errorHandler: null,
|
|
559
|
-
rejectionHandler: null,
|
|
560
|
-
originalFetch: null,
|
|
561
|
-
pendingErrors: []
|
|
679
|
+
launcherUnreadBadge: null
|
|
562
680
|
};
|
|
563
681
|
}
|
|
564
682
|
function normalizeOptions(options) {
|
|
@@ -574,18 +692,30 @@ function normalizeOptions(options) {
|
|
|
574
692
|
followUpEmailTo: options.followUpEmailTo?.trim() || '',
|
|
575
693
|
whatsappUrl: '',
|
|
576
694
|
dashboardUrl: options.dashboardUrl?.trim() || DEFAULT_DASHBOARD_URL,
|
|
695
|
+
privacyPolicyUrl: options.privacyPolicyUrl?.trim() || '',
|
|
577
696
|
dashboardSetupPath: options.dashboardSetupPath?.trim() || DEFAULT_DASHBOARD_SETUP_PATH,
|
|
578
697
|
autoOpen: options.autoOpen === true,
|
|
579
698
|
requireProfile: options.requireProfile !== false,
|
|
580
|
-
visitorProfile: options.visitorProfile === undefined ? undefined : normalizeVisitorProfile(options.visitorProfile)
|
|
581
|
-
errorMonitoring: options.errorMonitoring === true,
|
|
582
|
-
captureUnhandledErrors: options.captureUnhandledErrors !== false,
|
|
583
|
-
captureUnhandledRejections: options.captureUnhandledRejections !== false,
|
|
584
|
-
captureFetchErrors: options.captureFetchErrors !== false,
|
|
585
|
-
environment: options.environment?.trim() || '',
|
|
586
|
-
release: options.release?.trim() || ''
|
|
699
|
+
visitorProfile: options.visitorProfile === undefined ? undefined : normalizeVisitorProfile(options.visitorProfile)
|
|
587
700
|
};
|
|
588
701
|
}
|
|
702
|
+
function privacyNoticeUrl(state) {
|
|
703
|
+
const fallback = `${trimSlash(state.options.dashboardUrl || DEFAULT_DASHBOARD_URL)}/privacy`;
|
|
704
|
+
try {
|
|
705
|
+
const url = new URL(state.options.privacyPolicyUrl || state.resolvedPrivacyNoticeUrl || fallback);
|
|
706
|
+
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : fallback;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return fallback;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function updatePrivacyLinks(links, state) {
|
|
713
|
+
const organizationNotice = !!(state.options.privacyPolicyUrl || state.resolvedPrivacyNoticeUrl);
|
|
714
|
+
links.forEach(link => {
|
|
715
|
+
link.href = privacyNoticeUrl(state);
|
|
716
|
+
link.textContent = organizationNotice ? 'Privacy notice' : 'Pulse privacy notice';
|
|
717
|
+
});
|
|
718
|
+
}
|
|
589
719
|
function resolveMount(mount) {
|
|
590
720
|
if (!mount) {
|
|
591
721
|
return document.body;
|
|
@@ -630,7 +760,7 @@ function normalizeWhatsAppUrl(value) {
|
|
|
630
760
|
}
|
|
631
761
|
return '';
|
|
632
762
|
}
|
|
633
|
-
async function resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn) {
|
|
763
|
+
async function resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn, privacyLinks) {
|
|
634
764
|
if (state.resolved && state.widgetToken) {
|
|
635
765
|
return;
|
|
636
766
|
}
|
|
@@ -654,6 +784,10 @@ async function resolveWidgetToken(state, log, whatsappBtn, fabs, launcherBtn) {
|
|
|
654
784
|
throw new Error(`Failed to resolve widget keys (${res.status}).`);
|
|
655
785
|
}
|
|
656
786
|
const body = (await res.json());
|
|
787
|
+
state.resolvedPrivacyNoticeUrl = body.privacyNoticeUrl || null;
|
|
788
|
+
state.requestTypes = Array.isArray(body.requestTypes) ? body.requestTypes.filter(type => type.enabled) : [];
|
|
789
|
+
if (privacyLinks)
|
|
790
|
+
updatePrivacyLinks(privacyLinks, state);
|
|
657
791
|
state.widgetToken = body.widgetToken;
|
|
658
792
|
state.resolved = true;
|
|
659
793
|
const resolvedWhatsApp = body.whatsappEnabled === true
|
|
@@ -838,211 +972,6 @@ function renderMessageContent(container, content) {
|
|
|
838
972
|
if (cursor < content.length)
|
|
839
973
|
container.appendChild(document.createTextNode(content.slice(cursor)));
|
|
840
974
|
}
|
|
841
|
-
function installErrorMonitoring(state) {
|
|
842
|
-
if (!state.options.errorMonitoring) {
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
if (state.options.captureUnhandledErrors && !state.errorHandler) {
|
|
846
|
-
state.errorHandler = (event) => {
|
|
847
|
-
captureException(state, event.error ?? event.message, {
|
|
848
|
-
message: event.message,
|
|
849
|
-
errorType: 'unhandled-error',
|
|
850
|
-
pageUrl: event.filename || window.location.href,
|
|
851
|
-
source: 'window.onerror'
|
|
852
|
-
});
|
|
853
|
-
};
|
|
854
|
-
window.addEventListener('error', state.errorHandler);
|
|
855
|
-
}
|
|
856
|
-
if (state.options.captureFetchErrors && !state.originalFetch && typeof window.fetch === 'function') {
|
|
857
|
-
state.originalFetch = window.fetch.bind(window);
|
|
858
|
-
window.fetch = async (input, init) => {
|
|
859
|
-
const endpoint = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
860
|
-
try {
|
|
861
|
-
const response = await state.originalFetch(input, init);
|
|
862
|
-
if (!response.ok && shouldReportEndpoint(endpoint)) {
|
|
863
|
-
captureException(state, { message: ('HTTP ' + response.status + ' ' + response.statusText).trim(), status: response.status, url: endpoint }, {
|
|
864
|
-
errorType: 'http-error',
|
|
865
|
-
endpoint,
|
|
866
|
-
source: 'fetch'
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
|
-
return response;
|
|
870
|
-
}
|
|
871
|
-
catch (error) {
|
|
872
|
-
if (shouldReportEndpoint(endpoint)) {
|
|
873
|
-
captureException(state, error, {
|
|
874
|
-
errorType: 'fetch-error',
|
|
875
|
-
endpoint,
|
|
876
|
-
source: 'fetch'
|
|
877
|
-
});
|
|
878
|
-
}
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
if (state.options.captureUnhandledRejections && !state.rejectionHandler) {
|
|
884
|
-
state.rejectionHandler = (event) => {
|
|
885
|
-
captureException(state, event.reason, {
|
|
886
|
-
errorType: 'unhandled-rejection',
|
|
887
|
-
source: 'unhandledrejection'
|
|
888
|
-
});
|
|
889
|
-
};
|
|
890
|
-
window.addEventListener('unhandledrejection', state.rejectionHandler);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
function uninstallErrorMonitoring(state) {
|
|
894
|
-
if (state.errorHandler) {
|
|
895
|
-
window.removeEventListener('error', state.errorHandler);
|
|
896
|
-
state.errorHandler = null;
|
|
897
|
-
}
|
|
898
|
-
if (state.rejectionHandler) {
|
|
899
|
-
window.removeEventListener('unhandledrejection', state.rejectionHandler);
|
|
900
|
-
state.rejectionHandler = null;
|
|
901
|
-
}
|
|
902
|
-
if (state.originalFetch) {
|
|
903
|
-
window.fetch = state.originalFetch;
|
|
904
|
-
state.originalFetch = null;
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
function shouldReportEndpoint(endpoint) {
|
|
908
|
-
return !endpoint.includes('/api/pulse/public/webchat/') || !endpoint.includes('/errors');
|
|
909
|
-
}
|
|
910
|
-
function captureException(state, error, context = {}) {
|
|
911
|
-
if (!state.options.errorMonitoring) {
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
const normalized = normalizeError(error, context);
|
|
915
|
-
if (!normalized.message) {
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
if (!state.widgetToken) {
|
|
919
|
-
state.pendingErrors.push({ error, context });
|
|
920
|
-
if (state.pendingErrors.length > 50) {
|
|
921
|
-
state.pendingErrors = state.pendingErrors.slice(-50);
|
|
922
|
-
}
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
void sendErrorEvent(state, normalized);
|
|
926
|
-
}
|
|
927
|
-
function flushPendingErrors(state) {
|
|
928
|
-
if (!state.widgetToken || state.pendingErrors.length === 0) {
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
const pending = state.pendingErrors.splice(0);
|
|
932
|
-
for (const entry of pending) {
|
|
933
|
-
const normalized = normalizeError(entry.error, entry.context);
|
|
934
|
-
if (!normalized.message) {
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
void sendErrorEvent(state, normalized);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
async function sendErrorEvent(state, event) {
|
|
941
|
-
if (!state.widgetToken) {
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
const endpoint = `${trimSlash(state.options.apiBaseUrl)}/api/pulse/public/webchat/${state.widgetToken}/errors`;
|
|
945
|
-
if (event.endpoint && event.endpoint.includes('/api/pulse/public/webchat/') && event.endpoint.includes('/errors')) {
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
try {
|
|
949
|
-
const res = await fetch(endpoint, {
|
|
950
|
-
method: 'POST',
|
|
951
|
-
headers: { 'Content-Type': 'application/json' },
|
|
952
|
-
body: JSON.stringify({
|
|
953
|
-
message: event.message,
|
|
954
|
-
errorType: event.errorType,
|
|
955
|
-
statusCode: event.statusCode,
|
|
956
|
-
endpoint: event.endpoint,
|
|
957
|
-
pageUrl: event.pageUrl || window.location.href,
|
|
958
|
-
source: event.source,
|
|
959
|
-
environment: state.options.environment || undefined,
|
|
960
|
-
release: state.options.release || undefined,
|
|
961
|
-
userAgent: navigator.userAgent,
|
|
962
|
-
clientSessionId: ensureClientSessionId(state),
|
|
963
|
-
stack: event.stack
|
|
964
|
-
})
|
|
965
|
-
});
|
|
966
|
-
if (res.ok) {
|
|
967
|
-
rememberErrorReport(state);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
catch {
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
function normalizeError(error, context) {
|
|
975
|
-
const fallbackMessage = context.message || 'Unhandled website error';
|
|
976
|
-
if (error instanceof Error) {
|
|
977
|
-
return {
|
|
978
|
-
message: context.message || error.message || fallbackMessage,
|
|
979
|
-
errorType: context.errorType || error.name,
|
|
980
|
-
statusCode: context.statusCode,
|
|
981
|
-
endpoint: context.endpoint,
|
|
982
|
-
pageUrl: context.pageUrl,
|
|
983
|
-
source: context.source,
|
|
984
|
-
stack: context.stack || error.stack
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
if (typeof error === 'string') {
|
|
988
|
-
return { ...context, message: context.message || error };
|
|
989
|
-
}
|
|
990
|
-
if (error && typeof error === 'object') {
|
|
991
|
-
const payload = error;
|
|
992
|
-
return {
|
|
993
|
-
message: context.message || asErrorString(payload.message) || fallbackMessage,
|
|
994
|
-
errorType: context.errorType || asErrorString(payload.name),
|
|
995
|
-
statusCode: context.statusCode || asErrorNumber(payload.statusCode) || asErrorNumber(payload.status),
|
|
996
|
-
endpoint: context.endpoint || asErrorString(payload.url),
|
|
997
|
-
pageUrl: context.pageUrl,
|
|
998
|
-
source: context.source,
|
|
999
|
-
stack: context.stack || asErrorString(payload.stack)
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
return { ...context, message: fallbackMessage };
|
|
1003
|
-
}
|
|
1004
|
-
function asErrorString(value) {
|
|
1005
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
1006
|
-
}
|
|
1007
|
-
function asErrorNumber(value) {
|
|
1008
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1009
|
-
}
|
|
1010
|
-
function rememberErrorReport(state) {
|
|
1011
|
-
try {
|
|
1012
|
-
if (typeof localStorage === 'undefined') {
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
const key = getErrorReportStorageKey(getAgentScopeKey(state.options));
|
|
1016
|
-
const rows = loadErrorReportTimes(state).concat(Date.now()).slice(-200);
|
|
1017
|
-
localStorage.setItem(key, JSON.stringify(rows));
|
|
1018
|
-
}
|
|
1019
|
-
catch {
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
function getRecentErrorReportCount(state, days) {
|
|
1024
|
-
const cutoff = Date.now() - Math.max(1, days) * 24 * 60 * 60 * 1000;
|
|
1025
|
-
return loadErrorReportTimes(state).filter((timestamp) => timestamp >= cutoff).length;
|
|
1026
|
-
}
|
|
1027
|
-
function loadErrorReportTimes(state) {
|
|
1028
|
-
try {
|
|
1029
|
-
if (typeof localStorage === 'undefined') {
|
|
1030
|
-
return [];
|
|
1031
|
-
}
|
|
1032
|
-
const raw = localStorage.getItem(getErrorReportStorageKey(getAgentScopeKey(state.options)));
|
|
1033
|
-
if (!raw) {
|
|
1034
|
-
return [];
|
|
1035
|
-
}
|
|
1036
|
-
const parsed = JSON.parse(raw);
|
|
1037
|
-
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === 'number') : [];
|
|
1038
|
-
}
|
|
1039
|
-
catch {
|
|
1040
|
-
return [];
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
function getErrorReportStorageKey(slug) {
|
|
1044
|
-
return `pulse-agent-errors:${slug.toLowerCase()}`;
|
|
1045
|
-
}
|
|
1046
975
|
function getAgentScopeKey(options) {
|
|
1047
976
|
const app = options.appKey || options.appSlug || 'default';
|
|
1048
977
|
return `${options.slug}:${app}`;
|
|
@@ -1166,13 +1095,14 @@ function resetConversation(state, log) {
|
|
|
1166
1095
|
state.reconnectTimer = null;
|
|
1167
1096
|
}
|
|
1168
1097
|
state.conversationId = null;
|
|
1098
|
+
state.clientSessionId = null;
|
|
1169
1099
|
state.messages = [];
|
|
1170
1100
|
log.innerHTML = '';
|
|
1171
1101
|
pushMessage(state, log, { role: 'system', content: 'Started a new conversation. Send your message when ready.' });
|
|
1172
1102
|
persistSession(state);
|
|
1173
1103
|
}
|
|
1174
|
-
function readVisitorProfile(name, email) {
|
|
1175
|
-
const profile = normalizeVisitorProfile({ name, email });
|
|
1104
|
+
function readVisitorProfile(name, email, phone) {
|
|
1105
|
+
const profile = normalizeVisitorProfile({ name, email, phone });
|
|
1176
1106
|
return profile ?? null;
|
|
1177
1107
|
}
|
|
1178
1108
|
function normalizeVisitorProfile(profile) {
|
|
@@ -5,6 +5,7 @@ export interface PulseAgentOptions {
|
|
|
5
5
|
apiBaseUrl?: string;
|
|
6
6
|
followUpEmailTo?: string;
|
|
7
7
|
dashboardUrl?: string;
|
|
8
|
+
privacyPolicyUrl?: string;
|
|
8
9
|
dashboardSetupPath?: string;
|
|
9
10
|
mount?: HTMLElement | string;
|
|
10
11
|
autoOpen?: boolean;
|
|
@@ -13,12 +14,6 @@ export interface PulseAgentOptions {
|
|
|
13
14
|
widgetToken?: string;
|
|
14
15
|
title?: string;
|
|
15
16
|
whatsappUrl?: string;
|
|
16
|
-
errorMonitoring?: boolean;
|
|
17
|
-
captureUnhandledErrors?: boolean;
|
|
18
|
-
captureUnhandledRejections?: boolean;
|
|
19
|
-
captureFetchErrors?: boolean;
|
|
20
|
-
environment?: string;
|
|
21
|
-
release?: string;
|
|
22
17
|
}
|
|
23
18
|
export interface PulseAgentHandle {
|
|
24
19
|
open(): void;
|
|
@@ -27,17 +22,6 @@ export interface PulseAgentHandle {
|
|
|
27
22
|
resetConversation(): void;
|
|
28
23
|
identify(profile: VisitorProfile | null): void;
|
|
29
24
|
update(partial: Partial<PulseAgentOptions>): void;
|
|
30
|
-
captureException(error: unknown, context?: PulseErrorContext): void;
|
|
31
|
-
getErrorReportCount(days?: number): number;
|
|
32
|
-
}
|
|
33
|
-
export interface PulseErrorContext {
|
|
34
|
-
message?: string;
|
|
35
|
-
errorType?: string;
|
|
36
|
-
statusCode?: number;
|
|
37
|
-
endpoint?: string;
|
|
38
|
-
pageUrl?: string;
|
|
39
|
-
source?: string;
|
|
40
|
-
stack?: string;
|
|
41
25
|
}
|
|
42
26
|
export interface VisitorProfile {
|
|
43
27
|
id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pulse-agent.d.ts","sourceRoot":"","sources":["../../src/widget/pulse-agent.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"pulse-agent.d.ts","sourceRoot":"","sources":["../../src/widget/pulse-agent.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,cAAc,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,OAAO,IAAI,IAAI,CAAC;IAChB,iBAAiB,IAAI,IAAI,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/C,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAAC;CACnD;AA4CD,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAmW7E"}
|