@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.
@@ -9,7 +9,7 @@ export 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} Support Agent` : 'Pulse Support Agent';
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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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 @@ export 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
- <input data-role="name" type="text" placeholder="Names" />
306
- <input data-role="email" type="email" placeholder="Email" />
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) {