@buni.ai/chatbot-core 1.0.19 → 1.0.21

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/dist/index.esm.js CHANGED
@@ -1,5 +1,7 @@
1
1
  // Default BuniAI platform URL
2
2
  const BUNI_PLATFORM_URL = "https://www.buni.ai";
3
+ const PARENT_HANDSHAKE_TYPE = "buni:parent_handshake";
4
+ const IFRAME_HANDSHAKE_ACK_TYPE = "buni:iframe_handshake_ack";
3
5
  // Core widget loader class
4
6
  class BuniChatWidget {
5
7
  constructor() {
@@ -15,6 +17,34 @@ class BuniChatWidget {
15
17
  this.chatIframe = null;
16
18
  this.customerData = null;
17
19
  this.sessionVariables = null;
20
+ this.chatTargetOrigin = null;
21
+ this.handshakeComplete = false;
22
+ this.outboundMessageQueue = [];
23
+ this.displayMode = "hidden";
24
+ this.serverBehavior = {};
25
+ }
26
+ isMinimalModeEnabled() {
27
+ var _a, _b;
28
+ if (typeof ((_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.enableMinimalMode) === "boolean") {
29
+ return this.options.config.enableMinimalMode;
30
+ }
31
+ return Boolean(this.serverBehavior.enableMinimalMode);
32
+ }
33
+ syncServerBehaviorFromData(data) {
34
+ if (!data || typeof data !== "object") {
35
+ return;
36
+ }
37
+ const behaviorCandidate = data.behavior && typeof data.behavior === "object"
38
+ ? data.behavior
39
+ : data;
40
+ if (typeof behaviorCandidate.enableMinimalMode === "boolean") {
41
+ this.serverBehavior.enableMinimalMode = behaviorCandidate.enableMinimalMode;
42
+ }
43
+ if (behaviorCandidate.defaultMode === "full" ||
44
+ behaviorCandidate.defaultMode === "minimal" ||
45
+ behaviorCandidate.defaultMode === "hidden") {
46
+ this.serverBehavior.defaultMode = behaviorCandidate.defaultMode;
47
+ }
18
48
  }
19
49
  async initialize(options) {
20
50
  this.options = options;
@@ -178,6 +208,15 @@ class BuniChatWidget {
178
208
  params.set("startButtonText", config.startButtonText);
179
209
  if (config.preChatFormFields)
180
210
  params.set("preChatFormFields", JSON.stringify(config.preChatFormFields));
211
+ if (config.enableMinimalMode !== undefined) {
212
+ params.set("enableMinimalMode", String(config.enableMinimalMode));
213
+ }
214
+ if (config.defaultMode) {
215
+ params.set("defaultMode", config.defaultMode);
216
+ }
217
+ if (config.autoMessages && Array.isArray(config.autoMessages)) {
218
+ params.set("autoMessages", JSON.stringify(config.autoMessages));
219
+ }
181
220
  chatIframe.src = `${this.getBaseUrl()}/embed/chat?${params.toString()}`;
182
221
  // Chat iframe styling - initially hidden
183
222
  chatIframe.style.cssText = `
@@ -201,52 +240,103 @@ class BuniChatWidget {
201
240
  chatIframe.onload = () => {
202
241
  // Set visibility hidden after iframe loads to allow content initialization
203
242
  chatIframe.style.visibility = "hidden";
243
+ // Compute the iframe origin once we know the final src.
244
+ try {
245
+ this.chatTargetOrigin = new URL(chatIframe.src).origin;
246
+ }
247
+ catch (_a) {
248
+ this.chatTargetOrigin = this.getBaseUrl();
249
+ }
250
+ // Reset handshake state for this iframe instance.
251
+ this.handshakeComplete = false;
252
+ this.outboundMessageQueue = [];
204
253
  this.setupPostMessageAPI(chatIframe);
254
+ this.sendParentHandshake();
205
255
  // Now that both iframes are loaded, resolve the promise
206
256
  this.widgetElement = container;
207
257
  this.triggerIframe = triggerIframe;
208
258
  this.chatIframe = chatIframe;
209
259
  this.state.isMinimized = true;
260
+ this.state.displayMode = "hidden";
261
+ this.displayMode = "hidden";
210
262
  this.state.isLoaded = true;
211
263
  resolve();
212
264
  };
213
265
  };
214
266
  // Listen for trigger and connection events
215
267
  window.addEventListener("message", (event) => {
216
- var _a;
217
- if (!event.data.type)
268
+ var _a, _b, _c, _d, _e, _f;
269
+ const payload = event.data;
270
+ if (!payload || typeof payload !== "object")
271
+ return;
272
+ if (typeof payload.type !== "string")
218
273
  return;
219
- switch (event.data.type) {
274
+ switch (payload.type) {
220
275
  case "connection_ready":
221
276
  // Check if message is from chat iframe
222
- if (event.source === ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow)) {
277
+ if (event.source === ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) &&
278
+ event.origin === this.getBaseUrl()) {
279
+ this.syncServerBehaviorFromData(payload.data);
223
280
  // Connection is ready, show the trigger button
224
281
  if (container && !config.hideDefaultTrigger) {
225
282
  container.style.display = "block";
226
283
  }
227
284
  // Emit connection_ready event for consumers
228
- this.emit("connection_ready", { timestamp: Date.now() });
285
+ this.emit("connection_ready", {
286
+ timestamp: Date.now(),
287
+ behavior: (_b = payload.data) === null || _b === void 0 ? void 0 : _b.behavior,
288
+ });
229
289
  if (this.options.onConnectionReady &&
230
290
  typeof this.options.onConnectionReady === "function") {
231
- this.options.onConnectionReady({ timestamp: Date.now() });
291
+ this.options.onConnectionReady({
292
+ timestamp: Date.now(),
293
+ behavior: (_c = payload.data) === null || _c === void 0 ? void 0 : _c.behavior,
294
+ });
295
+ }
296
+ }
297
+ break;
298
+ case "chatbot:close":
299
+ if (event.source === ((_d = this.chatIframe) === null || _d === void 0 ? void 0 : _d.contentWindow) &&
300
+ event.origin === this.getBaseUrl()) {
301
+ this.closeChat();
302
+ this.displayMode = "hidden";
303
+ this.state.displayMode = "hidden";
304
+ }
305
+ break;
306
+ case "chatbot:minimize":
307
+ if (event.source === ((_e = this.chatIframe) === null || _e === void 0 ? void 0 : _e.contentWindow) &&
308
+ event.origin === this.getBaseUrl()) {
309
+ if (this.isMinimalModeEnabled()) {
310
+ if (!this.state.isOpen) {
311
+ void this.openChat();
312
+ }
313
+ this.postMessageToWidget("minimize");
314
+ this.displayMode = "minimal";
315
+ this.state.displayMode = "minimal";
316
+ }
317
+ else {
318
+ this.closeChat();
319
+ this.displayMode = "hidden";
320
+ this.state.displayMode = "hidden";
232
321
  }
233
322
  }
234
323
  break;
235
324
  case "trigger_clicked":
236
- this.openChat();
325
+ if (event.source === ((_f = this.triggerIframe) === null || _f === void 0 ? void 0 : _f.contentWindow) &&
326
+ event.origin === window.location.origin) {
327
+ this.openChat();
328
+ }
237
329
  break;
238
330
  }
239
331
  });
240
332
  // Trigger the load event
241
333
  // if by 10 seconds the ready message is not received, we want to manually, set the container to visible
242
334
  setTimeout(() => {
243
- if (!this.state.isLoaded) {
244
- if (container &&
245
- !config.hideDefaultTrigger &&
246
- container.style.display === "none" &&
247
- this.state.isOpen === false) {
248
- container.style.display = "block";
249
- }
335
+ if (container &&
336
+ !config.hideDefaultTrigger &&
337
+ container.style.display === "none" &&
338
+ this.state.isOpen === false) {
339
+ container.style.display = "block";
250
340
  }
251
341
  }, 10000);
252
342
  triggerIframe.src = "about:blank";
@@ -348,11 +438,15 @@ class BuniChatWidget {
348
438
  </div>
349
439
  <script>
350
440
  function handleClick() {
351
- window.parent.postMessage({ type: 'trigger_clicked' }, '*');
441
+ // Trigger iframe is same-origin with parent (about:blank + document.write),
442
+ // so we can safely target the parent's origin.
443
+ window.parent.postMessage({ type: 'trigger_clicked' }, window.location.origin);
352
444
  }
353
445
 
354
446
  // Listen for unread count updates
355
447
  window.addEventListener('message', function(event) {
448
+ if (event.source !== window.parent) return;
449
+ if (event.origin !== window.location.origin) return;
356
450
  if (event.data.type === 'updateUnreadCount') {
357
451
  const badge = document.getElementById('badge');
358
452
  const count = event.data.count || 0;
@@ -419,6 +513,8 @@ class BuniChatWidget {
419
513
  this.chatIframe.style.visibility = "visible";
420
514
  this.state.isMinimized = false;
421
515
  this.state.isOpen = true;
516
+ this.displayMode = "full";
517
+ this.state.displayMode = "full";
422
518
  this.emit("maximized", { timestamp: Date.now() });
423
519
  }
424
520
  closeChat() {
@@ -451,6 +547,8 @@ class BuniChatWidget {
451
547
  this.triggerIframe.style.display = "block";
452
548
  this.state.isMinimized = true;
453
549
  this.state.isOpen = false;
550
+ this.displayMode = "hidden";
551
+ this.state.displayMode = "hidden";
454
552
  this.emit("minimized", { timestamp: Date.now() });
455
553
  }
456
554
  getPositionStyles(position, isMinimized = false) {
@@ -500,12 +598,24 @@ class BuniChatWidget {
500
598
  // Listen for messages from the iframe
501
599
  window.addEventListener("message", (event) => {
502
600
  var _a;
601
+ if (event.source !== iframe.contentWindow) {
602
+ return;
603
+ }
503
604
  // Verify origin for security
504
605
  if (event.origin !== this.getBaseUrl()) {
505
606
  return;
506
607
  }
507
- const { type, data } = event.data;
608
+ const payload = event.data;
609
+ if (!payload || typeof payload !== "object")
610
+ return;
611
+ const { type, data } = payload;
612
+ if (typeof type !== "string")
613
+ return;
508
614
  switch (type) {
615
+ case IFRAME_HANDSHAKE_ACK_TYPE:
616
+ this.handshakeComplete = true;
617
+ this.flushOutboundMessageQueue();
618
+ break;
509
619
  case "ready":
510
620
  this.state.isLoaded = true;
511
621
  this.emit("ready", data);
@@ -530,14 +640,24 @@ class BuniChatWidget {
530
640
  }
531
641
  break;
532
642
  case "minimized":
533
- // User clicked minimize in chat - close chat and show trigger
534
- this.closeChat();
643
+ this.syncServerBehaviorFromData(data);
644
+ if (this.isMinimalModeEnabled()) {
645
+ if (!this.state.isOpen) {
646
+ void this.openChat();
647
+ }
648
+ this.displayMode = "minimal";
649
+ this.state.displayMode = "minimal";
650
+ }
651
+ else {
652
+ // Backward-compatible behavior.
653
+ this.closeChat();
654
+ }
535
655
  break;
536
656
  case "new_unread_message":
537
657
  // Update unread count in trigger
538
658
  this.state.unreadCount++;
539
659
  if ((_a = this.triggerIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
540
- this.triggerIframe.contentWindow.postMessage({ type: "updateUnreadCount", count: this.state.unreadCount }, "*");
660
+ this.triggerIframe.contentWindow.postMessage({ type: "updateUnreadCount", count: this.state.unreadCount }, window.location.origin);
541
661
  }
542
662
  break;
543
663
  case "customer_data_updated":
@@ -557,17 +677,69 @@ class BuniChatWidget {
557
677
  }
558
678
  });
559
679
  }
680
+ sendParentHandshake() {
681
+ var _a, _b;
682
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
683
+ if (!iframeWindow)
684
+ return;
685
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
686
+ try {
687
+ iframeWindow.postMessage({
688
+ type: PARENT_HANDSHAKE_TYPE,
689
+ data: { origin: window.location.origin },
690
+ }, targetOrigin);
691
+ }
692
+ catch (_c) {
693
+ // Best-effort; iframe may fall back to document.referrer for origin discovery.
694
+ }
695
+ }
696
+ flushOutboundMessageQueue() {
697
+ var _a, _b;
698
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
699
+ if (!iframeWindow)
700
+ return;
701
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
702
+ const queued = this.outboundMessageQueue;
703
+ if (queued.length === 0)
704
+ return;
705
+ this.outboundMessageQueue = [];
706
+ for (const msg of queued) {
707
+ iframeWindow.postMessage({ type: msg.type, data: msg.data }, targetOrigin);
708
+ }
709
+ }
560
710
  postMessageToWidget(type, data) {
561
- var _a;
562
- if ((_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow) {
563
- this.chatIframe.contentWindow.postMessage({ type, data }, this.getBaseUrl());
711
+ var _a, _b;
712
+ const iframeWindow = (_a = this.chatIframe) === null || _a === void 0 ? void 0 : _a.contentWindow;
713
+ if (!iframeWindow)
714
+ return;
715
+ const targetOrigin = (_b = this.chatTargetOrigin) !== null && _b !== void 0 ? _b : this.getBaseUrl();
716
+ // Ensure handshake is sent before any other commands; queue until ack.
717
+ if (!this.handshakeComplete && type !== PARENT_HANDSHAKE_TYPE) {
718
+ const queue = this.outboundMessageQueue;
719
+ if (queue.length < 50) {
720
+ queue.push({ type, data });
721
+ }
722
+ else {
723
+ this.outboundMessageQueue = [...queue.slice(-49), { type, data }];
724
+ }
725
+ this.sendParentHandshake();
726
+ return;
564
727
  }
728
+ iframeWindow.postMessage({ type, data }, targetOrigin);
565
729
  }
566
730
  getBaseUrl() {
731
+ var _a, _b;
567
732
  // Return the base URL for the BuniAI platform
568
733
  // Priority: 1. config.baseUrl, 2. global BUNI_API_URL, 3. default platform URL
734
+ const configUrl = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.baseUrl;
569
735
  const globalUrl = globalThis.BUNI_API_URL;
570
- return globalUrl || BUNI_PLATFORM_URL;
736
+ const rawUrl = configUrl || globalUrl || BUNI_PLATFORM_URL;
737
+ try {
738
+ return new URL(rawUrl).origin;
739
+ }
740
+ catch (_c) {
741
+ return rawUrl;
742
+ }
571
743
  }
572
744
  destroy() {
573
745
  this.postMessageToWidget("destroy");
@@ -614,10 +786,21 @@ class BuniChatWidget {
614
786
  }
615
787
  }
616
788
  minimize() {
789
+ if (this.isMinimalModeEnabled()) {
790
+ if (!this.state.isOpen) {
791
+ void this.openChat();
792
+ }
793
+ this.postMessageToWidget("minimize");
794
+ this.displayMode = "minimal";
795
+ this.state.displayMode = "minimal";
796
+ return;
797
+ }
617
798
  this.closeChat();
618
799
  }
619
800
  maximize() {
620
- this.openChat();
801
+ void this.openChat();
802
+ this.displayMode = "full";
803
+ this.state.displayMode = "full";
621
804
  }
622
805
  setCustomerData(data) {
623
806
  this.customerData = { ...this.customerData, ...data };
@@ -658,6 +841,8 @@ class BuniChatWidget {
658
841
  }
659
842
  close() {
660
843
  this.closeChat();
844
+ this.displayMode = "hidden";
845
+ this.state.displayMode = "hidden";
661
846
  }
662
847
  on(event, callback) {
663
848
  if (!this.eventListeners.has(event)) {
@@ -693,7 +878,10 @@ class BuniChatWidget {
693
878
  }
694
879
  }
695
880
  getState() {
696
- return { ...this.state };
881
+ return {
882
+ ...this.state,
883
+ displayMode: this.displayMode,
884
+ };
697
885
  }
698
886
  isReady() {
699
887
  return this.state.isLoaded;