releasebird-javascript-sdk 1.0.92 → 1.0.94

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.
@@ -31,7 +31,8 @@ export class RbirdAutomationManager {
31
31
  */
32
32
  async init(apiKey) {
33
33
  this.apiKey = apiKey;
34
- this.firstSeenTimestamp = this.getFirstSeenTimestamp();
34
+ const { timestamp, isFirstVisit } = this.getFirstSeenTimestamp();
35
+ this.firstSeenTimestamp = timestamp;
35
36
 
36
37
  // Register event handlers
37
38
  this.registerEventHandlers();
@@ -42,12 +43,13 @@ export class RbirdAutomationManager {
42
43
  // Start command polling (reduced frequency - WebSocket is primary)
43
44
  this.startCommandPolling();
44
45
 
45
- // Send first_seen event
46
- await this.sendEvent('first_seen', {
47
- timeSinceFirstSeen: (Date.now() - this.firstSeenTimestamp) / 1000
48
- });
46
+ // Send first_seen event only if this is truly the first visit
47
+ if (isFirstVisit) {
48
+ await this.sendEvent('first_seen', {
49
+ timeSinceFirstSeen: 0
50
+ });
51
+ }
49
52
 
50
- console.log('[Rbird] Automation manager initialized');
51
53
  }
52
54
 
53
55
  /**
@@ -58,7 +60,6 @@ export class RbirdAutomationManager {
58
60
  window.addEventListener('message', (event) => {
59
61
  // Handle automation command notification from widget
60
62
  if (event.data === 'automationCommand') {
61
- console.log('[Rbird] Received WebSocket automation notification');
62
63
  // Immediately fetch and execute pending commands
63
64
  this.pollCommands();
64
65
  }
@@ -67,15 +68,16 @@ export class RbirdAutomationManager {
67
68
 
68
69
  /**
69
70
  * Get or set the first_seen timestamp
71
+ * @returns {{ timestamp: number, isFirstVisit: boolean }}
70
72
  */
71
73
  getFirstSeenTimestamp() {
72
74
  const stored = localStorage.getItem('rbird_first_seen');
73
75
  if (stored) {
74
- return parseInt(stored, 10);
76
+ return { timestamp: parseInt(stored, 10), isFirstVisit: false };
75
77
  }
76
78
  const now = Date.now();
77
79
  localStorage.setItem('rbird_first_seen', now.toString());
78
- return now;
80
+ return { timestamp: now, isFirstVisit: true };
79
81
  }
80
82
 
81
83
  /**
@@ -162,7 +164,6 @@ export class RbirdAutomationManager {
162
164
  if (!response.ok) {
163
165
  console.warn('[Rbird] Failed to send event:', response.status);
164
166
  } else {
165
- console.log('[Rbird] Event sent:', eventType);
166
167
  }
167
168
  } catch (error) {
168
169
  console.error('[Rbird] Error sending event:', error);
@@ -177,20 +178,37 @@ export class RbirdAutomationManager {
177
178
  }
178
179
 
179
180
  /**
180
- * Get the anonymous ID from session
181
+ * Get the anonymous ID from session.
182
+ * Uses the same anonymousIdentifier as RbirdSessionManager (stored in 'Rbird_I')
183
+ * to ensure consistency between automation and widget.
181
184
  */
182
185
  getAnonymousId() {
183
- return RbirdSessionManager.getInstance().getState()?.identify?.anonymousId ||
184
- localStorage.getItem('rbird_anonymous_id') ||
185
- this.generateAnonymousId();
186
+ // First check if there's an identified user with anonymousId
187
+ const identifyAnonymousId = RbirdSessionManager.getInstance().getState()?.identify?.anonymousId;
188
+ if (identifyAnonymousId) {
189
+ return identifyAnonymousId;
190
+ }
191
+
192
+ // Use the same anonymousIdentifier as RbirdSessionManager
193
+ const sessionAnonymousId = RbirdSessionManager.getInstance().anonymousIdentifier;
194
+ if (sessionAnonymousId) {
195
+ return sessionAnonymousId;
196
+ }
197
+
198
+ // Fallback: read from the same localStorage key as RbirdSessionManager
199
+ return localStorage.getItem('Rbird_I') || this.generateAnonymousId();
186
200
  }
187
201
 
188
202
  /**
189
- * Generate and store an anonymous ID
203
+ * Generate and store an anonymous ID (using same key as RbirdSessionManager)
190
204
  */
191
205
  generateAnonymousId() {
192
- const id = 'anon_' + Math.random().toString(36).substr(2, 9);
193
- localStorage.setItem('rbird_anonymous_id', id);
206
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
207
+ let id = '';
208
+ for (let i = 0; i < 60; i++) {
209
+ id += characters.charAt(Math.floor(Math.random() * characters.length));
210
+ }
211
+ localStorage.setItem('Rbird_I', id);
194
212
  return id;
195
213
  }
196
214
 
@@ -262,7 +280,6 @@ export class RbirdAutomationManager {
262
280
  * @param {Object} cmd - The command to execute
263
281
  */
264
282
  async executeCommand(cmd) {
265
- console.log('[Rbird] Executing command:', cmd.type);
266
283
 
267
284
  switch (cmd.type) {
268
285
  case 'show_banner':
@@ -277,6 +294,9 @@ export class RbirdAutomationManager {
277
294
  case 'send_chat':
278
295
  this.executeSendChat(cmd.message, cmd.config);
279
296
  break;
297
+ case 'send_booking':
298
+ this.executeSendBooking(cmd.targetId, cmd.message, cmd.config);
299
+ break;
280
300
  default:
281
301
  console.warn('[Rbird] Unknown command type:', cmd.type);
282
302
  }
@@ -302,25 +322,65 @@ export class RbirdAutomationManager {
302
322
 
303
323
  /**
304
324
  * Execute send_chat command
325
+ * @param {string} message - The chat message
326
+ * @param {Object} config - Config including chatId, openWidget flag, senderName, senderAvatar
305
327
  */
306
328
  executeSendChat(message, config) {
307
329
  if (!message) return;
308
330
 
309
- console.log('[Rbird] Chat message triggered:', message);
331
+ const widget = RbirdWebsiteWidget.getInstance();
332
+ const shouldOpenWidget = config?.openWidget !== false; // Default to true
333
+
334
+ if (shouldOpenWidget) {
335
+ // Open the widget and send the message
336
+ widget.openWebsiteWidget();
337
+
338
+ // Send message to iframe to display automation message
339
+ setTimeout(() => {
340
+ if (widget.iframe) {
341
+ widget.iframe.contentWindow?.postMessage({
342
+ type: 'automationMessage',
343
+ message: message,
344
+ chatId: config?.chatId
345
+ }, '*');
346
+ }
347
+ }, 500); // Small delay to ensure iframe is ready
348
+ } else {
349
+ // Just show a message bubble without opening the widget
350
+ widget.showMessageBubble(message, config?.chatId, null, config?.senderName, config?.senderAvatar);
351
+ }
352
+ }
310
353
 
311
- // Open the widget and send the message
354
+ /**
355
+ * Execute send_booking command - sends a booking request message
356
+ * @param {string} appointmentTypeId - The appointment type ID
357
+ * @param {string} message - Optional custom message
358
+ * @param {Object} config - Additional config (chatId, openWidget flag, senderName, senderAvatar)
359
+ */
360
+ executeSendBooking(appointmentTypeId, message, config) {
312
361
  const widget = RbirdWebsiteWidget.getInstance();
313
- widget.openWebsiteWidget();
314
-
315
- // Send message to iframe to display automation message
316
- setTimeout(() => {
317
- if (widget.iframe) {
318
- widget.iframe.contentWindow?.postMessage({
319
- type: 'automationMessage',
320
- message: message
321
- }, '*');
322
- }
323
- }, 500); // Small delay to ensure iframe is ready
362
+ const shouldOpenWidget = config?.openWidget !== false; // Default to true
363
+
364
+ if (shouldOpenWidget) {
365
+ // Open the widget and send the booking request
366
+ widget.openWebsiteWidget();
367
+
368
+ // Send message to iframe to display booking request
369
+ setTimeout(() => {
370
+ if (widget.iframe) {
371
+ widget.iframe.contentWindow?.postMessage({
372
+ type: 'automationBooking',
373
+ appointmentTypeId: appointmentTypeId,
374
+ message: message,
375
+ chatId: config?.chatId
376
+ }, '*');
377
+ }
378
+ }, 500); // Small delay to ensure iframe is ready
379
+ } else {
380
+ // Just show a message bubble without opening the widget
381
+ const displayMessage = message || 'Book an appointment';
382
+ widget.showMessageBubble(displayMessage, config?.chatId, appointmentTypeId, config?.senderName, config?.senderAvatar);
383
+ }
324
384
  }
325
385
 
326
386
  /**
@@ -9,6 +9,7 @@ export class RbirdBookingManager {
9
9
  constructor() {
10
10
  this.apiKey = null;
11
11
  this.isOpen = false;
12
+ this.embeddedInstances = new Map(); // Track embedded instances by target selector
12
13
  }
13
14
 
14
15
  static getInstance() {
@@ -27,6 +28,7 @@ export class RbirdBookingManager {
27
28
  /**
28
29
  * Show the booking modal
29
30
  * @param {Object} options - Optional configuration
31
+ * @param {string} options.appointmentTypeId - Pre-select a specific appointment type
30
32
  * @param {Function} options.onSuccess - Callback when booking is completed
31
33
  * @param {Function} options.onClose - Callback when modal is closed
32
34
  */
@@ -56,6 +58,9 @@ export class RbirdBookingManager {
56
58
  if (sessionManager.anonymousIdentifier) {
57
59
  iframeUrl += `&ai=${sessionManager.anonymousIdentifier}`;
58
60
  }
61
+ if (options.appointmentTypeId) {
62
+ iframeUrl += `&appointmentTypeId=${encodeURIComponent(options.appointmentTypeId)}`;
63
+ }
59
64
 
60
65
  this.createModal(iframeUrl, options);
61
66
  this.isOpen = true;
@@ -150,6 +155,133 @@ export class RbirdBookingManager {
150
155
  }
151
156
  }
152
157
 
158
+ /**
159
+ * Embed the booking form into a container element
160
+ * @param {Object} options - Configuration options
161
+ * @param {string} options.target - CSS selector for the container element (required)
162
+ * @param {string} options.appointmentTypeId - Pre-select a specific appointment type
163
+ * @param {string} options.height - Height of the embedded form (default: '600px')
164
+ * @param {Function} options.onSuccess - Callback when booking is completed
165
+ * @param {Function} options.onError - Callback when an error occurs
166
+ */
167
+ embedBooking(options = {}) {
168
+ if (typeof window === 'undefined') return;
169
+ if (!this.apiKey) {
170
+ console.error('[RbirdBookingManager] SDK not initialized. Call Rbird.initialize() first.');
171
+ return;
172
+ }
173
+ if (!options.target) {
174
+ console.error('[RbirdBookingManager] Target selector is required for embedBooking().');
175
+ return;
176
+ }
177
+
178
+ const container = document.querySelector(options.target);
179
+ if (!container) {
180
+ console.error(`[RbirdBookingManager] Container not found: ${options.target}`);
181
+ return;
182
+ }
183
+
184
+ // Check if already embedded in this container
185
+ if (this.embeddedInstances.has(options.target)) {
186
+ console.warn(`[RbirdBookingManager] Booking form already embedded in ${options.target}. Call destroyEmbeddedBooking() first to re-embed.`);
187
+ return;
188
+ }
189
+
190
+ const sessionManager = RbirdSessionManager.getInstance();
191
+ const state = sessionManager.getState();
192
+
193
+ // Build iframe URL with embedded mode
194
+ let iframeUrl = `${CONTENT_URL}/widget?apiKey=${this.apiKey}&view=BOOKING&embedded=true`;
195
+
196
+ if (state?.identify?.people) {
197
+ iframeUrl += `&people=${state.identify.people}`;
198
+ }
199
+ if (state?.identify?.hash) {
200
+ iframeUrl += `&hash=${state.identify.hash}`;
201
+ }
202
+ if (sessionManager.anonymousIdentifier) {
203
+ iframeUrl += `&ai=${sessionManager.anonymousIdentifier}`;
204
+ }
205
+ if (options.appointmentTypeId) {
206
+ iframeUrl += `&appointmentTypeId=${encodeURIComponent(options.appointmentTypeId)}`;
207
+ }
208
+
209
+ // Create wrapper div for styling
210
+ const wrapper = document.createElement('div');
211
+ wrapper.className = 'rbird-booking-embedded-wrapper';
212
+ wrapper.style.cssText = `
213
+ width: 100%;
214
+ height: ${options.height || '600px'};
215
+ border-radius: 12px;
216
+ overflow: hidden;
217
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
218
+ `;
219
+
220
+ // Create iframe
221
+ const iframe = document.createElement('iframe');
222
+ iframe.className = 'rbird-booking-embedded-iframe';
223
+ iframe.src = iframeUrl;
224
+ iframe.allow = 'clipboard-write';
225
+ iframe.style.cssText = `
226
+ width: 100%;
227
+ height: 100%;
228
+ border: none;
229
+ `;
230
+
231
+ wrapper.appendChild(iframe);
232
+ container.appendChild(wrapper);
233
+
234
+ // Handle messages from iframe
235
+ const messageHandler = (e) => {
236
+ if (e.data === 'bookingSuccess') {
237
+ if (options.onSuccess) {
238
+ options.onSuccess();
239
+ }
240
+ }
241
+ if (e.data === 'bookingError') {
242
+ if (options.onError) {
243
+ options.onError(e.data.error);
244
+ }
245
+ }
246
+ };
247
+ window.addEventListener('message', messageHandler);
248
+
249
+ // Store instance info for cleanup
250
+ this.embeddedInstances.set(options.target, {
251
+ wrapper,
252
+ messageHandler
253
+ });
254
+
255
+ return {
256
+ destroy: () => this.destroyEmbeddedBooking(options.target)
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Remove an embedded booking form
262
+ * @param {string} target - CSS selector for the container element
263
+ */
264
+ destroyEmbeddedBooking(target) {
265
+ if (!this.embeddedInstances.has(target)) {
266
+ console.warn(`[RbirdBookingManager] No embedded booking found for ${target}`);
267
+ return;
268
+ }
269
+
270
+ const instance = this.embeddedInstances.get(target);
271
+
272
+ // Remove event listener
273
+ if (instance.messageHandler) {
274
+ window.removeEventListener('message', instance.messageHandler);
275
+ }
276
+
277
+ // Remove DOM element
278
+ if (instance.wrapper && instance.wrapper.parentNode) {
279
+ instance.wrapper.parentNode.removeChild(instance.wrapper);
280
+ }
281
+
282
+ this.embeddedInstances.delete(target);
283
+ }
284
+
153
285
  injectStyles() {
154
286
  if (typeof window === 'undefined' || document.getElementById('rbird-booking-styles')) return;
155
287
 
@@ -107,8 +107,6 @@ export default class RbirdScreenshotManager {
107
107
 
108
108
  // Wait a moment for the loader to render, then hide our UI elements
109
109
  setTimeout(() => {
110
- console.log('[Screenshot] Starting HTML export method...');
111
-
112
110
  // Hide the close button and toolbar
113
111
  const closeButton = document.getElementById('screenshotCloseButton');
114
112
  const toolbar = document.querySelector('.menu'); // SVGEditor toolbar
@@ -131,15 +129,12 @@ export default class RbirdScreenshotManager {
131
129
  // Export HTML with inline styles (now async!)
132
130
  const htmlData = await that.exportHTML();
133
131
 
134
- console.log('[Screenshot] HTML exported, sending to backend...');
135
-
136
132
  // Restore loader visibility while waiting for backend
137
133
  loader.style.visibility = 'visible';
138
134
 
139
135
  // Send HTML to backend for rendering
140
136
  that.renderScreenshotOnBackend(htmlData)
141
137
  .then((screenshotDataUrl) => {
142
- console.log('[Screenshot] Screenshot received from backend!');
143
138
  screenshotCompleted = true;
144
139
  clearTimeout(timeoutId);
145
140
 
@@ -207,7 +202,6 @@ export default class RbirdScreenshotManager {
207
202
  * Returns complete HTML document with minimal modifications
208
203
  */
209
204
  async exportHTML() {
210
- console.log('[Screenshot] Exporting HTML with inline resources...');
211
205
 
212
206
  // Capture scroll positions BEFORE cloning
213
207
  this.captureScrollPositions();
@@ -246,7 +240,6 @@ export default class RbirdScreenshotManager {
246
240
  const doctype = '<!DOCTYPE html>';
247
241
  const html = doctype + clone.outerHTML;
248
242
 
249
- console.log(`[Screenshot] Exported HTML: ${html.length} bytes`);
250
243
  return html;
251
244
  }
252
245
 
@@ -255,7 +248,6 @@ export default class RbirdScreenshotManager {
255
248
  * This allows the backend to restore scroll positions before taking the screenshot
256
249
  */
257
250
  captureScrollPositions() {
258
- console.log('[Screenshot] Capturing scroll positions...');
259
251
  let scrollableCount = 0;
260
252
 
261
253
  // Find all elements that might be scrollable
@@ -289,7 +281,6 @@ export default class RbirdScreenshotManager {
289
281
  }
290
282
 
291
283
  scrollableCount++;
292
- console.log(`[Screenshot] Captured scroll position for element: scrollLeft=${scrollLeft}, scrollTop=${scrollTop}`);
293
284
  }
294
285
  });
295
286
 
@@ -304,7 +295,6 @@ export default class RbirdScreenshotManager {
304
295
  document.body.setAttribute('data-rbird-scroll-top', docScrollTop.toString());
305
296
  }
306
297
 
307
- console.log(`[Screenshot] Captured scroll positions for ${scrollableCount} elements`);
308
298
  }
309
299
 
310
300
  /**
@@ -346,7 +336,6 @@ export default class RbirdScreenshotManager {
346
336
 
347
337
  if (totalOriginalSize > 0) {
348
338
  const reduction = Math.round((1 - totalResizedSize / totalOriginalSize) * 100);
349
- console.log(`[Screenshot] Image optimization: ${totalOriginalSize} -> ${totalResizedSize} bytes (${reduction}% reduction)`);
350
339
  }
351
340
  }
352
341
 
@@ -454,7 +443,6 @@ export default class RbirdScreenshotManager {
454
443
  * Download all CSS and inline with data URLs
455
444
  */
456
445
  async downloadAllCSS(clone) {
457
- console.log(`[Screenshot] Processing ${document.styleSheets.length} stylesheets...`);
458
446
  const processedStylesheets = [];
459
447
  const googleFontsLinks = [];
460
448
 
@@ -491,7 +479,6 @@ export default class RbirdScreenshotManager {
491
479
  styleSheet.href.includes('icon.css') ||
492
480
  styleSheet.href.includes('icons.css')
493
481
  )) {
494
- console.log(`[Screenshot] Skipping font/icon service stylesheet: ${styleSheet.href}`);
495
482
  googleFontsLinks.push(styleSheet.href);
496
483
  continue;
497
484
  }
@@ -504,16 +491,13 @@ export default class RbirdScreenshotManager {
504
491
  for (let j = 0; j < styleSheet.cssRules.length; j++) {
505
492
  cssText += styleSheet.cssRules[j].cssText + '\n';
506
493
  }
507
- console.log(`[Screenshot] Extracted ${styleSheet.cssRules.length} rules from stylesheet ${i}`);
508
494
  }
509
495
  } catch (corsError) {
510
496
  // CORS blocked - try to fetch the stylesheet if it has an href
511
497
  if (styleSheet.href) {
512
- console.log(`[Screenshot] CORS blocked stylesheet ${i}, fetching: ${styleSheet.href}`);
513
498
  try {
514
499
  const response = await fetch(styleSheet.href);
515
500
  cssText = await response.text();
516
- console.log(`[Screenshot] Fetched ${cssText.length} bytes from ${styleSheet.href}`);
517
501
  } catch (fetchError) {
518
502
  console.warn('[Screenshot] Failed to fetch CORS-blocked stylesheet:', styleSheet.href, fetchError);
519
503
  }
@@ -544,11 +528,8 @@ export default class RbirdScreenshotManager {
544
528
  }
545
529
  }
546
530
 
547
- console.log(`[Screenshot] Successfully processed ${processedStylesheets.length} stylesheets`);
548
-
549
531
  // Remove original link tags except Google Fonts and icon fonts
550
532
  const links = clone.querySelectorAll('link[rel="stylesheet"]');
551
- console.log(`[Screenshot] Removing ${links.length} original link tags (keeping font/icon services)`);
552
533
  links.forEach(link => {
553
534
  const href = link.getAttribute('href');
554
535
  // Keep Google Fonts, icon fonts, and other font service links
@@ -627,15 +608,12 @@ export default class RbirdScreenshotManager {
627
608
  fullURL = new URL(url, basePath + '/').href;
628
609
  }
629
610
 
630
- console.log(`[Screenshot] Processing CSS resource: ${fullURL} (font: ${isFont}, image: ${isImage})`);
631
-
632
611
  const dataURL = await this.fetchResourceAsDataURL(fullURL);
633
612
 
634
613
  if (dataURL) {
635
614
  if (isFont) {
636
615
  // Don't resize fonts - use as-is
637
616
  cssText = cssText.split(match[0]).join(`url(${dataURL})`);
638
- console.log(`[Screenshot] Embedded font: ${url}`);
639
617
  } else if (isImage) {
640
618
  // Resize images to reduce payload
641
619
  try {
@@ -735,7 +713,6 @@ export default class RbirdScreenshotManager {
735
713
 
736
714
  // Convert to base64 in chunks to avoid call stack issues
737
715
  const base64 = this.arrayBufferToBase64(compressed);
738
- console.log(`[Screenshot] Compression: ${data.length} -> ${compressed.length} bytes (${Math.round(compressed.length / data.length * 100)}%)`);
739
716
  return base64;
740
717
  } else {
741
718
  // Fallback: no compression available
@@ -773,17 +750,11 @@ export default class RbirdScreenshotManager {
773
750
  */
774
751
  async renderScreenshotOnBackend(html) {
775
752
  const htmlSizeBytes = html.length;
776
- const htmlSizeMB = (htmlSizeBytes / (1024 * 1024)).toFixed(2);
777
-
778
- console.log('[Screenshot] Sending HTML to backend...');
779
- console.log(`[Screenshot] Original HTML size: ${htmlSizeBytes} bytes (${htmlSizeMB} MB)`);
780
753
 
781
754
  // If HTML > 800KB, use S3 upload to bypass ALB 1MB limit
782
755
  if (htmlSizeBytes > 800 * 1024) {
783
- console.log('[Screenshot] HTML > 800KB, using S3 upload...');
784
756
  return this.renderViaS3(html);
785
757
  } else {
786
- console.log('[Screenshot] HTML < 800KB, using direct upload...');
787
758
  return this.renderDirect(html);
788
759
  }
789
760
  }
@@ -794,8 +765,6 @@ export default class RbirdScreenshotManager {
794
765
  async renderViaS3(html) {
795
766
  return new Promise(async (resolve, reject) => {
796
767
  try {
797
- console.log('[Screenshot] Step 1: Requesting presigned URL...');
798
-
799
768
  const widgetInstance = this.getRbirdWebsiteWidget().getInstance();
800
769
 
801
770
  // Create abort controller for timeout
@@ -824,7 +793,6 @@ export default class RbirdScreenshotManager {
824
793
  }
825
794
 
826
795
  const { uploadUrl, key } = await urlResponse.json();
827
- console.log(`[Screenshot] Step 2: Uploading HTML to S3 (key: ${key})...`);
828
796
 
829
797
  // 2. Upload HTML directly to S3
830
798
  const controller2 = new AbortController();
@@ -850,8 +818,6 @@ export default class RbirdScreenshotManager {
850
818
  throw new Error(`Failed to upload to S3: ${s3Response.status}`);
851
819
  }
852
820
 
853
- console.log('[Screenshot] Step 3: Requesting screenshot render from backend...');
854
-
855
821
  // 3. Request screenshot render via backend
856
822
  const controller3 = new AbortController();
857
823
  const timeout3 = setTimeout(() => controller3.abort(), 20000); // 20s timeout
@@ -884,7 +850,6 @@ export default class RbirdScreenshotManager {
884
850
  }
885
851
 
886
852
  const result = await renderResponse.json();
887
- console.log('[Screenshot] Screenshot received successfully via S3!');
888
853
  resolve(result.screenshot);
889
854
 
890
855
  } catch (error) {
@@ -956,7 +921,6 @@ export default class RbirdScreenshotManager {
956
921
  }
957
922
 
958
923
  const payload = JSON.stringify(requestData);
959
- console.log(`[Screenshot] Final payload size: ${payload.length} bytes`);
960
924
 
961
925
  http.send(payload);
962
926
  });
@@ -767,6 +767,78 @@ export default class RbirdWebsiteWidget {
767
767
  }
768
768
  }
769
769
 
770
+ /**
771
+ * Show a single message bubble for automation messages (without opening the widget)
772
+ * @param {string} message - The message text to display
773
+ * @param {string} chatId - The chat ID to open when clicked
774
+ * @param {string} appointmentTypeId - Optional appointment type ID for booking messages
775
+ * @param {string} senderName - Optional sender name (from selected user)
776
+ * @param {string} senderAvatar - Optional sender avatar URL (from selected user)
777
+ */
778
+ showMessageBubble(message, chatId, appointmentTypeId = null, senderName = null, senderAvatar = null) {
779
+
780
+ if (!message) {
781
+ return;
782
+ }
783
+
784
+ // Ensure widget button exists for positioning
785
+ if (!this.websiteWidget) {
786
+ // Retry after widget is initialized
787
+ setTimeout(() => this.showMessageBubble(message, chatId, appointmentTypeId, senderName, senderAvatar), 500);
788
+ return;
789
+ }
790
+
791
+ // Create message object in the format expected by createMessageBubble
792
+ // Use provided sender info, or fall back to 'Support' with no avatar
793
+ const msg = {
794
+ chatId: chatId || 'automation-' + Date.now(),
795
+ text: message,
796
+ senderName: senderName || 'Support',
797
+ senderAvatar: senderAvatar || null,
798
+ timestamp: new Date().toISOString(),
799
+ appointmentTypeId: appointmentTypeId
800
+ };
801
+
802
+ // Ensure bubble container exists
803
+ if (!this.messageBubblesContainer) {
804
+ this.messageBubblesContainer = document.createElement('div');
805
+ this.messageBubblesContainer.className = 'rbird-message-bubbles-container';
806
+ // Set critical styles directly to ensure visibility
807
+ this.messageBubblesContainer.style.position = 'fixed';
808
+ this.messageBubblesContainer.style.zIndex = '9999999';
809
+ this.messageBubblesContainer.style.display = 'flex';
810
+ this.messageBubblesContainer.style.flexDirection = 'column';
811
+ this.messageBubblesContainer.style.gap = '10px';
812
+ this.messageBubblesContainer.style.maxWidth = '320px';
813
+ document.body.appendChild(this.messageBubblesContainer);
814
+ }
815
+
816
+ // Create and add the bubble
817
+ const bubble = this.createMessageBubble(msg);
818
+
819
+ // Override click handler for automation bubbles - open widget and show the chat
820
+ bubble.onclick = () => {
821
+ this.hideMessageBubbles();
822
+ this.openWebsiteWidget();
823
+ // Send message to iframe to open the specific chat
824
+ if (this.iframe && chatId) {
825
+ this.iframe.contentWindow?.postMessage({
826
+ type: 'openChat',
827
+ chatId: chatId
828
+ }, '*');
829
+ }
830
+ };
831
+
832
+ this.messageBubblesContainer.appendChild(bubble);
833
+ this.messageBubblesContainer.style.display = 'flex';
834
+
835
+ // Position bubbles
836
+ this.positionMessageBubbles();
837
+
838
+ // Play notification sound
839
+ this.playNotificationSound();
840
+ }
841
+
770
842
  /**
771
843
  * Create the drag handle element (6-dot grip pattern)
772
844
  */