embeddedaichatux 2.1.1 → 2.2.0

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.
Files changed (2) hide show
  1. package/EmbeddedChat.js +160 -70
  2. package/package.json +1 -1
package/EmbeddedChat.js CHANGED
@@ -1,5 +1,10 @@
1
1
  class EmbeddedChat {
2
+ static instanceCounter = 0;
3
+
2
4
  constructor(containerDiv, chatId, options) {
5
+ EmbeddedChat.instanceCounter++;
6
+ this.instanceId = `ec_${EmbeddedChat.instanceCounter}_${Date.now()}`;
7
+
3
8
  if (!containerDiv) {
4
9
  containerDiv = this.createChatContainer();
5
10
  }
@@ -25,19 +30,25 @@
25
30
  // Determine transition styles
26
31
  const transitionStyle = this.enableAnimation ? 'transition: height 0.3s ease, opacity 0.3s ease;' : '';
27
32
 
28
- this.positionStyle = this.containerDiv.dataset.position === 'in-place' ?
29
- `width:100%; height:${this.height}px; ${transitionStyle}` :
33
+ this.position = this.options.position || this.containerDiv.dataset.position || 'fixed';
34
+ this.positionStyle = this.position === 'in-place' ?
35
+ `position: relative; width:100%; height:${this.height}px; ${transitionStyle}` :
30
36
  `position: fixed; right: 1em; bottom: 0; width: ${this.width}px; z-index: 100000; max-width:90vw; max-height: 90vh; ${transitionStyle}`;
31
37
  this.mode = options.mode || 'Chat'; // default to 'chat'
32
38
  this.minimizeOnScroll = false; // Default to false
33
- if (this.mode === 'ContactForm') {
34
- // Adjust position style for contact form if mode is 'ContactForm'
35
- this.positionStyle = `width:100%; height:${this.height}px; ${transitionStyle}`;
39
+ if (this.mode === 'ContactForm' || this.mode === 'PartnerOffers') {
40
+ // Adjust position style for contact form/offers - force relative/absolute to occupy container
41
+ this.positionStyle = `position: relative; width:100%; height:${this.height}px; ${transitionStyle}`;
42
+ this.containerDiv.style.position = 'relative';
36
43
  this.containerDiv.style.height = `${this.height}px`;
37
44
  }
38
45
 
39
46
  this.conversationId = this.getStoredConversationId();
40
47
  this.sessionInfo = options.sessionInfo || null;
48
+
49
+ // Initialize window event listeners only once per instance
50
+ this.addWindowEventListeners();
51
+
41
52
  this.init();
42
53
  }
43
54
 
@@ -82,31 +93,35 @@
82
93
  ? this.serverUrl.slice(0, -1)
83
94
  : this.serverUrl;
84
95
  const baseIframeUrl = `${cleanedServerUrl}/ChatUX/${this.chatId}`;
85
- const minimizedPositionStyle = `
86
- position: fixed; right: 0; bottom: 0;
87
- width: 150px;
88
- height: ${this.minimizedHeight};
89
- z-index: 100000;
90
- ${this.enableAnimation ? 'transition: height 0.3s ease, opacity 0.3s ease;' : ''}
91
- `;
96
+ const minimizedPositionStyle = this.position === 'in-place' ?
97
+ `position: relative; width: 100%; height: ${this.minimizedHeight}; ${this.enableAnimation ? 'transition: height 0.3s ease, opacity 0.3s ease;' : ''}` :
98
+ `position: fixed; right: 0; bottom: 0; width: 150px; height: ${this.minimizedHeight}; z-index: 100000; ${this.enableAnimation ? 'transition: height 0.3s ease, opacity 0.3s ease;' : ''}`;
92
99
 
93
100
  const params = {
94
101
  isPreview: this.isPreview,
95
102
  mode: this.mode,
96
103
  locale: this.locale,
97
- conversationId: this.conversationId
104
+ conversationId: this.conversationId,
105
+ instanceId: this.instanceId,
106
+ customStyle: this.options.customStyle,
107
+ customJs: this.options.customJs
98
108
  };
99
109
 
110
+ const iframeId = `ec-iframe-${this.instanceId}`;
111
+ const minimizedIframeId = `ec-minimized-${this.instanceId}`;
112
+
100
113
  const iframeHtml = `
101
114
  <iframe
102
- id="embedded-chat"
115
+ id="${iframeId}"
116
+ title="AI chatbot"
103
117
  style="${this.positionStyle}; display: none; border:none; overflow:hidden;"
104
118
  frameborder="0"
105
119
  sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-top-navigation-by-user-activation"
106
120
  src="${this.buildIframeUrl(baseIframeUrl, params)}"
107
121
  ></iframe>
108
122
  <iframe
109
- id="embedded-chat-minimized"
123
+ id="${minimizedIframeId}"
124
+ title="AI chatbot"
110
125
  style="${minimizedPositionStyle}; display: none; border:none; overflow:hidden;"
111
126
  frameborder="0"
112
127
  sandbox="allow-same-origin allow-scripts allow-popups allow-forms"
@@ -116,27 +131,27 @@
116
131
 
117
132
  // inject and capture references
118
133
  this.containerDiv.innerHTML = iframeHtml;
119
- this.iframe = this.containerDiv.querySelector("#embedded-chat");
120
- this.minimizedIframe = this.containerDiv.querySelector("#embedded-chat-minimized");
134
+ this.iframe = this.containerDiv.querySelector(`#${iframeId}`);
135
+ this.minimizedIframe = this.containerDiv.querySelector(`#${minimizedIframeId}`);
121
136
 
122
137
  // Wait for main iframe to load
123
138
  if (this.iframe) {
124
139
  await this.waitForIframeLoad(this.iframe);
125
140
  }
126
141
 
127
- this.addEventListeners();
142
+ this.addIframeEventListeners();
128
143
 
129
144
  // Send auth to iframes after load
130
145
  await this.postAuthToIframes();
131
146
 
132
147
  // restore your ContactForm logic
133
- if (this.mode === 'ContactForm') {
148
+ if (this.mode === 'ContactForm' || this.mode === 'PartnerOffers') {
134
149
  this.iframe.style.maxWidth = '100%';
135
150
  this.iframe.style.display = 'block';
136
151
  }
137
152
  }
138
153
 
139
- addEventListeners() {
154
+ addIframeEventListeners() {
140
155
  if (!this.iframe || !this.minimizedIframe) {
141
156
  console.error('EmbeddedChat: iframe or minimizedIframe is not defined when trying to add event listeners.', this.iframe, this.minimizedIframe);
142
157
  return;
@@ -145,6 +160,12 @@
145
160
  this.iframe.addEventListener("mouseleave", () => { this.mouseInsideChat = false; });
146
161
  this.minimizedIframe.addEventListener("mouseenter", () => { this.mouseInsideChat = true; });
147
162
  this.minimizedIframe.addEventListener("mouseleave", () => { this.mouseInsideChat = false; });
163
+ }
164
+
165
+ addWindowEventListeners() {
166
+
167
+ // Use a flag to ensure window listeners aren't duplicated for this specific instance if setMode is called (though it shouldn't be)
168
+ if (this.windowListenersAdded) return;
148
169
 
149
170
  window.addEventListener("message", (e) => {
150
171
  // Only trust messages from our server (when targetOrigin is not "*")
@@ -158,13 +179,18 @@
158
179
  }
159
180
  });
160
181
 
161
- window.addEventListener("load", () => {
182
+ const handleResizeTrigger = () => {
162
183
  this.postResizeMessage();
163
- });
184
+ };
164
185
 
165
- window.addEventListener("resize", () => {
166
- this.postResizeMessage();
167
- });
186
+ if (document.readyState === 'complete') {
187
+ handleResizeTrigger();
188
+ } else {
189
+ window.addEventListener("load", handleResizeTrigger);
190
+ }
191
+
192
+ window.addEventListener("resize", handleResizeTrigger);
193
+ this.windowListenersAdded = true;
168
194
  }
169
195
 
170
196
  postResizeMessage() {
@@ -172,7 +198,8 @@
172
198
  this.iframe.contentWindow.postMessage({
173
199
  message: "resize",
174
200
  width: window.innerWidth, // Ensure correct size
175
- height: window.innerHeight
201
+ height: window.innerHeight,
202
+ instanceId: this.instanceId
176
203
  }, this.targetOrigin);
177
204
  }
178
205
  }
@@ -205,9 +232,13 @@
205
232
  const msg = (data.message || data.type || "").toString();
206
233
  const incomingChatId = (data.chatId || "").toString().toLowerCase();
207
234
  const targetChatId = (this.chatId || "").toString().toLowerCase();
235
+ const incomingInstanceId = data.instanceId;
236
+
237
+ // Priority 1: Handle instance-specific routing
238
+ if (incomingInstanceId && incomingInstanceId !== this.instanceId) return;
208
239
 
209
- // Bail out if chatId doesn't match and is provided
210
- if (incomingChatId && targetChatId && incomingChatId !== targetChatId) return;
240
+ // Priority 2: Bail out if chatId doesn't match and is provided (legacy/fallback)
241
+ if (!incomingInstanceId && incomingChatId && targetChatId && incomingChatId !== targetChatId) return;
211
242
 
212
243
  // Allow the iframe to request a fresh token
213
244
  if (data.type === "requestAuth") {
@@ -254,7 +285,9 @@
254
285
 
255
286
  // Handle maximize/minimize/navigate
256
287
  if (msg === "minimize") {
257
- if (this.mode !== 'ContactForm') {
288
+ if (typeof this.options.onMinimize === 'function') {
289
+ this.options.onMinimize();
290
+ } else if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers') {
258
291
  this.animateMinimize();
259
292
  }
260
293
  } else if (msg === "show" || msg === "maximize") {
@@ -262,6 +295,18 @@
262
295
  this.showMaximized(animate);
263
296
  } else if (msg === "navigate" && data.url) {
264
297
  window.location.href = data.url;
298
+ } else if (data.type === "switchToChat" || msg === "switchToChat") {
299
+ if (typeof this.options.onSwitchToChat === 'function') {
300
+ this.options.onSwitchToChat(data.partner);
301
+ } else {
302
+ this.setMode('Chat', { ...data, initialState: 'Maximized' });
303
+ }
304
+ } else if (data.type === "switchToContactForm" || msg === "switchToContactForm") {
305
+ if (typeof this.options.onSwitchToContactForm === 'function') {
306
+ this.options.onSwitchToContactForm(data.partner);
307
+ } else {
308
+ this.setMode('ContactForm', { partner: data.partner, initialState: 'Maximized' });
309
+ }
265
310
  }
266
311
 
267
312
  // Handle conversationId update
@@ -271,7 +316,7 @@
271
316
  }
272
317
 
273
318
  animateMinimize() {
274
- if (this.mode !== 'ContactForm') {
319
+ if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers') {
275
320
  this.iframe.style.height = this.minimizedHeight;
276
321
  this.iframe.style.opacity = '0';
277
322
  setTimeout(() => {
@@ -283,14 +328,14 @@
283
328
 
284
329
  // Request a height update after showing
285
330
  if (this.minimizedIframe.contentWindow) {
286
- this.minimizedIframe.contentWindow.postMessage({ message: "requestHeightUpdate" }, this.targetOrigin);
331
+ this.minimizedIframe.contentWindow.postMessage({ message: "requestHeightUpdate", instanceId: this.instanceId }, this.targetOrigin);
287
332
  }
288
333
  }, this.enableAnimation ? 300 : 0);
289
334
  }
290
335
  }
291
336
 
292
337
  updateMinimizedSize(contentHeight, contentWidth) {
293
- if (this.mode === 'ContactForm') return;
338
+ if (this.mode === 'ContactForm' || this.mode === 'PartnerOffers') return;
294
339
 
295
340
  this.hasMeasuredSize = true;
296
341
 
@@ -312,7 +357,7 @@
312
357
  }
313
358
 
314
359
  animateMaximize() {
315
- if (this.mode !== 'ContactForm') {
360
+ if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers') {
316
361
  // Start the animation for minimizing iframe
317
362
  this.minimizedIframe.style.height = `${this.height}px`;
318
363
  this.minimizedIframe.style.opacity = '0';
@@ -349,25 +394,23 @@
349
394
  this.width = parsedWidth;
350
395
  console.log(`Width updated to: ${this.width}px`);
351
396
 
352
- // Apply width to container
353
- this.containerDiv.style.width = `${this.width}px`;
354
-
355
- // Apply width to the main iframe
356
- if (this.iframe) {
357
- this.iframe.style.width = `${this.width}px`;
397
+ // Apply width only if not in-place
398
+ if (this.position !== 'in-place') {
399
+ this.containerDiv.style.width = `${this.width}px`;
400
+ if (this.iframe) {
401
+ this.iframe.style.width = `${this.width}px`;
402
+ }
403
+ } else {
404
+ this.containerDiv.style.width = '100%';
405
+ if (this.iframe) {
406
+ this.iframe.style.width = '100%';
407
+ }
358
408
  }
359
409
  }
360
410
 
361
411
  // Apply scale if provided
362
412
  if (typeof scale === 'number' && scale > 0) {
363
- if (this.iframe) {
364
- this.iframe.style.transform = `scale(${scale})`;
365
- this.iframe.style.transformOrigin = "bottom right";
366
- }
367
- if (this.minimizedIframe) {
368
- this.minimizedIframe.style.transform = `scale(${scale})`;
369
- this.minimizedIframe.style.transformOrigin = "bottom right";
370
- }
413
+ this.applyScale(scale);
371
414
  console.log(`Scale applied: ${scale}`);
372
415
  }
373
416
 
@@ -403,15 +446,25 @@
403
446
  console.log(`Height updated to: ${this.height}px (clamped to ${effectiveHeight}px for display), marginBottom: ${this.marginBottom}px`);
404
447
 
405
448
  // Apply the updated height/marginBottom
406
- if (this.mode !== 'ContactForm') {
407
- if (this.containerDiv.dataset.position === 'in-place') {
449
+ if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers') {
450
+ if (this.position === 'in-place') {
408
451
  // "in-place" uses full width, sets containerDiv height
452
+ this.containerDiv.style.position = 'relative';
453
+ this.containerDiv.style.width = '100%';
409
454
  this.containerDiv.style.height = `${effectiveHeight}px`;
410
455
  if (this.iframe) {
456
+ this.iframe.style.position = 'relative';
457
+ this.iframe.style.width = '100%';
411
458
  this.iframe.style.height = `${effectiveHeight}px`;
459
+ this.iframe.style.bottom = 'unset';
460
+ this.iframe.style.right = 'unset';
412
461
  }
413
462
  if (this.minimizedIframe) {
463
+ this.minimizedIframe.style.position = 'relative';
464
+ this.minimizedIframe.style.width = '100%';
414
465
  this.minimizedIframe.style.height = this.minimizedHeight;
466
+ this.minimizedIframe.style.bottom = 'unset';
467
+ this.minimizedIframe.style.right = 'unset';
415
468
  }
416
469
  } else {
417
470
  // position: fixed (typical floating chat)
@@ -440,7 +493,8 @@
440
493
  if (this.iframe.contentWindow) {
441
494
  this.iframe.contentWindow.postMessage({
442
495
  message: "show",
443
- sessionInfo: this.sessionInfo || null
496
+ sessionInfo: this.sessionInfo || null,
497
+ instanceId: this.instanceId
444
498
  }, this.targetOrigin);
445
499
  } else {
446
500
  // Schedule a single retry with a delay
@@ -448,7 +502,8 @@
448
502
  if (this.iframe.contentWindow) {
449
503
  this.iframe.contentWindow.postMessage({
450
504
  message: "show",
451
- sessionInfo: this.sessionInfo || null
505
+ sessionInfo: this.sessionInfo || null,
506
+ instanceId: this.instanceId
452
507
  }, this.targetOrigin);
453
508
  } else {
454
509
  console.warn("iframe contentWindow is not available.");
@@ -473,16 +528,21 @@
473
528
  }
474
529
 
475
530
  applyScale(scale) {
476
- if (this.mode !== 'ContactForm') {
477
- this.iframe.style.transform = `scale(${scale})`;
478
- this.iframe.style.transformOrigin = "bottom right";
479
- this.minimizedIframe.style.transform = `scale(${scale})`;
480
- this.minimizedIframe.style.transformOrigin = "bottom right";
531
+ if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers') {
532
+ const origin = this.position === 'in-place' ? 'top center' : 'bottom right';
533
+ if (this.iframe) {
534
+ this.iframe.style.transform = `scale(${scale})`;
535
+ this.iframe.style.transformOrigin = origin;
536
+ }
537
+ if (this.minimizedIframe) {
538
+ this.minimizedIframe.style.transform = `scale(${scale})`;
539
+ this.minimizedIframe.style.transformOrigin = origin;
540
+ }
481
541
  }
482
542
  }
483
543
 
484
544
  minimizeOnScrollAction() {
485
- if (this.mode !== 'ContactForm' && this.iframe.style.display !== "none") {
545
+ if (this.mode !== 'ContactForm' && this.mode !== 'PartnerOffers' && this.iframe.style.display !== "none") {
486
546
  this.animateMinimize();
487
547
  }
488
548
  }
@@ -493,7 +553,7 @@
493
553
 
494
554
  // Send session info immediately if the chat is already maximized
495
555
  if (this.iframe?.contentWindow && this.iframe.style.display !== "none") {
496
- this.iframe.contentWindow.postMessage({ type: 'setSessionInfo', sessionInfo: this.sessionInfo }, this.targetOrigin);
556
+ this.iframe.contentWindow.postMessage({ type: 'setSessionInfo', sessionInfo: this.sessionInfo, instanceId: this.instanceId }, this.targetOrigin);
497
557
  }
498
558
  }
499
559
 
@@ -512,8 +572,8 @@
512
572
  urlParams.set('isPreview', 'true');
513
573
  }
514
574
 
515
- if (params.mode === 'ContactForm') {
516
- urlParams.set('mode', 'ContactForm');
575
+ if (params.mode === 'ContactForm' || params.mode === 'PartnerOffers' || params.mode === 'Chat') {
576
+ urlParams.set('mode', params.mode);
517
577
  }
518
578
 
519
579
  if (params.locale) {
@@ -533,17 +593,12 @@
533
593
  return `${baseIframeUrl}?${urlParams.toString()}`;
534
594
  }
535
595
 
536
- waitForIframeLoad(iframe, timeout = 5000) {
537
- return new Promise((resolve, reject) => {
538
- // Check if the iframe is already loaded
539
- if (iframe.contentDocument || iframe.contentWindow) {
540
- resolve();
541
- return;
542
- }
543
-
596
+ waitForIframeLoad(iframe, timeout = 10000) {
597
+ return new Promise((resolve) => {
544
598
  // Set up a timeout to avoid waiting indefinitely
545
599
  const timeoutId = setTimeout(() => {
546
- reject(new Error('Iframe did not load within the timeout period.'));
600
+ console.warn('EmbeddedChat: Iframe load timeout, proceeding with initialization.');
601
+ resolve();
547
602
  }, timeout);
548
603
 
549
604
  // Add the load event listener
@@ -574,7 +629,8 @@
574
629
  this.iframe.contentWindow.postMessage({
575
630
  type: 'auth',
576
631
  userToken: userToken,
577
- chatId: this.chatId
632
+ chatId: this.chatId,
633
+ instanceId: this.instanceId
578
634
  }, this.targetOrigin);
579
635
  console.log('[EmbeddedChat] Auth message sent successfully');
580
636
  } else {
@@ -585,6 +641,33 @@
585
641
  console.error('[EmbeddedChat] Failed to get user token:', err);
586
642
  }
587
643
  }
644
+
645
+ setMode(newMode, options = {}) {
646
+ if (this.mode === newMode && (!options.chatId || options.chatId === this.chatId)) return;
647
+
648
+ console.log(`[EmbeddedChat] Setting mode to ${newMode}`, options);
649
+ this.mode = newMode;
650
+ if (options.chatId) {
651
+ this.chatId = options.chatId;
652
+ }
653
+ this.options = { ...this.options, ...options };
654
+ const transitionStyle = this.enableAnimation ? 'transition: height 0.3s ease, opacity 0.3s ease;' : '';
655
+ if (this.mode === 'ContactForm' || this.mode === 'PartnerOffers') {
656
+ this.positionStyle = `position: relative; width:100%; height:${this.height}px; ${transitionStyle}`;
657
+ this.containerDiv.style.position = 'relative';
658
+ this.containerDiv.style.height = `${this.height}px`;
659
+ } else {
660
+ this.positionStyle = this.position === 'in-place' ?
661
+ `position: relative; width:100%; height:${this.height}px; ${transitionStyle}` :
662
+ `position: fixed; right: 1em; bottom: 0; width: ${this.width}px; z-index: 100000; max-width:90vw; max-height: 90vh; ${transitionStyle}`;
663
+ if (this.position === 'in-place') {
664
+ this.containerDiv.style.position = 'relative';
665
+ this.containerDiv.style.width = '100%';
666
+ this.containerDiv.style.height = `${this.height}px`;
667
+ }
668
+ }
669
+ this.updateIframes();
670
+ }
588
671
  }
589
672
 
590
673
  export function initEmbeddedChat(containerDiv, chatId, options) {
@@ -595,3 +678,10 @@ export function initContactForm(containerDiv, chatId, options) {
595
678
  const clonedOptions = { ...options, mode: 'ContactForm' };
596
679
  return new EmbeddedChat(containerDiv, chatId, clonedOptions);
597
680
  }
681
+
682
+ export function initOffers(options = {}) {
683
+ const chatId = options.chatConfigId;
684
+ const mergedOptions = { ...options, mode: 'PartnerOffers', height: options.height || 500 };
685
+ return new EmbeddedChat(options.container, chatId, mergedOptions);
686
+ }
687
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "embeddedaichatux",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "A lightweight and customizable embedded AI chat UI component that seamlessly integrates into web applications, offering minimized and expanded views, with iframe-based content rendering.",
5
5
  "main": "EmbeddedChat.js",
6
6
  "scripts": {