@wabbit-dashboard/embed 1.0.10 → 1.0.12

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 CHANGED
@@ -33,12 +33,14 @@ import { Wabbit } from '@wabbit-dashboard/embed';
33
33
 
34
34
  ### Using CDN
35
35
 
36
- **Production:**
36
+ **Production (deployed website):**
37
37
  ```html
38
38
  <script src="https://unpkg.com/@wabbit-dashboard/embed@1/dist/wabbit-embed.umd.min.js"></script>
39
39
  <script>
40
40
  Wabbit.init({
41
41
  apiKey: 'pk_live_xxx',
42
+ apiUrl: 'https://platform.insourcedata.ai',
43
+ wsUrl: 'wss://chat.insourcedata.ai/ws/chat',
42
44
  chat: {
43
45
  enabled: true,
44
46
  collectionId: 'abc123',
@@ -48,7 +50,9 @@ import { Wabbit } from '@wabbit-dashboard/embed';
48
50
  </script>
49
51
  ```
50
52
 
51
- **Local Development:**
53
+ > **Note**: The `apiUrl` and `wsUrl` are required when your website runs on localhost but connects to production services. For fully deployed production websites, these can be omitted as the SDK auto-detects the correct URLs.
54
+
55
+ **Local Development (local services):**
52
56
  ```html
53
57
  <script src="http://localhost:3000/sdk/wabbit-embed.umd.js"></script>
54
58
  <script>
@@ -23,6 +23,10 @@ function validateConfig(config) {
23
23
  if (!config.chat.collectionId) {
24
24
  throw new Error('[Wabbit] collectionId is required when chat is enabled');
25
25
  }
26
+ // Validate inline mode requires a container
27
+ if (config.chat.mode === 'inline' && !config.chat.container) {
28
+ throw new Error('[Wabbit] container is required when chat mode is "inline"');
29
+ }
26
30
  }
27
31
  // Validate forms config if enabled
28
32
  if (config.forms?.enabled) {
@@ -90,11 +94,14 @@ function mergeConfig(config) {
90
94
  chat: config.chat
91
95
  ? {
92
96
  ...config.chat,
97
+ mode: config.chat.mode || 'widget',
93
98
  position: config.chat.position || 'bottom-right',
94
99
  triggerType: config.chat.triggerType || 'button',
95
100
  theme: config.chat.theme || 'auto',
96
101
  primaryColor: config.chat.primaryColor || '#6366f1',
97
- placeholder: config.chat.placeholder || 'Type your message...'
102
+ placeholder: config.chat.placeholder || 'Type your message...',
103
+ showHeader: config.chat.showHeader ?? true,
104
+ headerTitle: config.chat.headerTitle || 'AI Assistant'
98
105
  }
99
106
  : undefined,
100
107
  forms: config.forms
@@ -157,6 +164,11 @@ class Wabbit {
157
164
  if (mergedConfig.chat?.enabled && mergedConfig.chat) {
158
165
  // Import ChatWidget dynamically to avoid circular dependencies
159
166
  Promise.resolve().then(function () { return ChatWidget$1; }).then(({ ChatWidget }) => {
167
+ // Check if instance was destroyed during async import (e.g., React StrictMode)
168
+ if (!Wabbit.instance) {
169
+ console.warn('[Wabbit] Instance was destroyed before chat widget could initialize');
170
+ return;
171
+ }
160
172
  const chat = mergedConfig.chat;
161
173
  const chatConfig = {
162
174
  enabled: chat.enabled,
@@ -165,13 +177,23 @@ class Wabbit {
165
177
  apiKey: chat.apiKey || mergedConfig.apiKey,
166
178
  apiUrl: chat.apiUrl || mergedConfig.apiUrl,
167
179
  wsUrl: mergedConfig.wsUrl || chat.wsUrl, // Use global wsUrl or chat-specific wsUrl
180
+ // Embedding mode
181
+ mode: chat.mode,
182
+ container: chat.container,
183
+ // Widget mode options
168
184
  position: chat.position,
169
185
  triggerType: chat.triggerType,
170
186
  triggerDelay: chat.triggerDelay,
187
+ // Appearance options
171
188
  theme: chat.theme,
172
189
  primaryColor: chat.primaryColor,
173
190
  welcomeMessage: chat.welcomeMessage,
174
- placeholder: chat.placeholder
191
+ placeholder: chat.placeholder,
192
+ // Header customization
193
+ showHeader: chat.showHeader,
194
+ headerTitle: chat.headerTitle,
195
+ // Callbacks - prefer chat-specific, fallback to global
196
+ onChatReady: chat.onChatReady || config.onChatReady
175
197
  };
176
198
  const chatWidget = new ChatWidget(chatConfig);
177
199
  chatWidget.init();
@@ -181,6 +203,11 @@ class Wabbit {
181
203
  if (mergedConfig.forms?.enabled && mergedConfig.forms) {
182
204
  // Import FormWidget dynamically to avoid circular dependencies
183
205
  Promise.resolve().then(function () { return FormWidget$1; }).then(({ FormWidget }) => {
206
+ // Check if instance was destroyed during async import
207
+ if (!Wabbit.instance) {
208
+ console.warn('[Wabbit] Instance was destroyed before form widget could initialize');
209
+ return;
210
+ }
184
211
  const forms = mergedConfig.forms;
185
212
  const formConfig = {
186
213
  enabled: forms.enabled,
@@ -205,6 +232,11 @@ class Wabbit {
205
232
  if (mergedConfig.emailCapture?.enabled) {
206
233
  const emailCapture = mergedConfig.emailCapture;
207
234
  Promise.resolve().then(function () { return EmailCaptureWidget$1; }).then(({ EmailCaptureWidget }) => {
235
+ // Check if instance was destroyed during async import
236
+ if (!Wabbit.instance) {
237
+ console.warn('[Wabbit] Instance was destroyed before email capture widget could initialize');
238
+ return;
239
+ }
208
240
  const emailCaptureConfig = {
209
241
  enabled: emailCapture.enabled,
210
242
  triggerAfterMessages: emailCapture.triggerAfterMessages,
@@ -219,7 +251,7 @@ class Wabbit {
219
251
  // Connect to chat widget after it's initialized
220
252
  // Use a small delay to ensure chat widget is ready
221
253
  setTimeout(() => {
222
- if (Wabbit.instance.chatWidget) {
254
+ if (Wabbit.instance?.chatWidget) {
223
255
  const chatWidget = Wabbit.instance.chatWidget;
224
256
  // Set WebSocket client
225
257
  if (chatWidget.wsClient) {
@@ -239,7 +271,9 @@ class Wabbit {
239
271
  });
240
272
  }
241
273
  // Auto-initialize forms with data-wabbit-form-id (backward compatibility)
242
- Wabbit.instance.initLegacyForms(config);
274
+ if (Wabbit.instance) {
275
+ Wabbit.instance.initLegacyForms(config);
276
+ }
243
277
  // Call onReady callback if provided
244
278
  if (config.onReady) {
245
279
  // Wait for DOM to be ready
@@ -293,6 +327,60 @@ class Wabbit {
293
327
  Wabbit.instance = null;
294
328
  }
295
329
  }
330
+ /**
331
+ * Get the URL for a dedicated chat page
332
+ *
333
+ * @param collectionId - The collection ID
334
+ * @param options - Optional parameters
335
+ * @returns Full URL to the chat page
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const url = Wabbit.getChatPageUrl('abc123', {
340
+ * initialMessage: 'How do I get started?',
341
+ * theme: 'dark'
342
+ * });
343
+ * // Returns: https://platform.insourcedata.ai/c/abc123?q=How%20do%20I%20get%20started%3F&theme=dark
344
+ * ```
345
+ */
346
+ static getChatPageUrl(collectionId, options) {
347
+ // Determine base URL
348
+ const baseUrl = options?.baseUrl ||
349
+ (typeof window !== 'undefined' && window.WABBIT_BASE_URL) ||
350
+ (typeof window !== 'undefined' &&
351
+ (window.location.hostname === 'localhost' ||
352
+ window.location.hostname === '127.0.0.1')
353
+ ? 'http://localhost:3000'
354
+ : 'https://platform.insourcedata.ai');
355
+ const url = new URL(`/c/${collectionId}`, baseUrl);
356
+ // Add query parameters
357
+ if (options?.initialMessage) {
358
+ url.searchParams.set('q', options.initialMessage);
359
+ }
360
+ if (options?.theme && options.theme !== 'auto') {
361
+ url.searchParams.set('theme', options.theme);
362
+ }
363
+ if (options?.primaryColor) {
364
+ url.searchParams.set('color', options.primaryColor.replace('#', ''));
365
+ }
366
+ return url.toString();
367
+ }
368
+ /**
369
+ * Open chat in a new tab/window
370
+ *
371
+ * @param collectionId - The collection ID
372
+ * @param options - Optional parameters (same as getChatPageUrl)
373
+ *
374
+ * @example
375
+ * ```typescript
376
+ * // Open chat page with initial message
377
+ * Wabbit.openChatPage('abc123', { initialMessage: 'Hello!' });
378
+ * ```
379
+ */
380
+ static openChatPage(collectionId, options) {
381
+ const url = Wabbit.getChatPageUrl(collectionId, options);
382
+ window.open(url, '_blank', 'noopener,noreferrer');
383
+ }
296
384
  /**
297
385
  * Get current configuration
298
386
  */
@@ -812,6 +900,8 @@ class ChatPanel {
812
900
  this.sendButton = null;
813
901
  this.messages = [];
814
902
  this.isWaitingForResponse = false;
903
+ this.closeButton = null;
904
+ this.eventCleanup = [];
815
905
  this.options = options;
816
906
  }
817
907
  /**
@@ -821,36 +911,83 @@ class ChatPanel {
821
911
  if (this.element) {
822
912
  return this.element;
823
913
  }
824
- // Main panel
914
+ const isInline = this.options.mode === 'inline';
915
+ // Main panel - use different class for inline mode
825
916
  this.element = createElement('div', {
826
- class: `wabbit-chat-panel ${this.options.position}`
917
+ class: isInline
918
+ ? 'wabbit-chat-panel wabbit-chat-panel-inline'
919
+ : `wabbit-chat-panel ${this.options.position}`
827
920
  });
828
- // Header
921
+ // Header (conditionally shown based on showHeader option, default: true)
922
+ if (this.options.showHeader !== false) {
923
+ const header = this.createHeader(isInline);
924
+ this.element.appendChild(header);
925
+ }
926
+ // Messages area
927
+ this.messagesContainer = createElement('div', { class: 'wabbit-chat-messages' });
928
+ // Input area
929
+ const inputArea = this.createInputArea();
930
+ // Assemble panel
931
+ this.element.appendChild(this.messagesContainer);
932
+ this.element.appendChild(inputArea);
933
+ // Append to container (inline) or document.body (widget)
934
+ if (isInline && this.options.container) {
935
+ this.options.container.appendChild(this.element);
936
+ // In inline mode, always visible
937
+ this.element.style.display = 'flex';
938
+ }
939
+ else {
940
+ document.body.appendChild(this.element);
941
+ // In widget mode, initially hidden until opened
942
+ this.element.style.display = 'none';
943
+ }
944
+ // Show welcome message if provided
945
+ if (this.options.welcomeMessage) {
946
+ this.addSystemMessage(this.options.welcomeMessage);
947
+ }
948
+ // Ensure scroll to bottom after DOM settles (for initial load and history)
949
+ setTimeout(() => this.scrollToBottom(), 100);
950
+ return this.element;
951
+ }
952
+ /**
953
+ * Create the header element
954
+ */
955
+ createHeader(isInline) {
829
956
  const header = createElement('div', { class: 'wabbit-chat-panel-header' });
830
957
  const headerTitle = createElement('div', { class: 'wabbit-chat-panel-header-title' });
831
958
  const headerIcon = createElement('div', { class: 'wabbit-chat-panel-header-icon' });
832
959
  headerIcon.textContent = 'AI';
833
960
  const headerText = createElement('div', { class: 'wabbit-chat-panel-header-text' });
834
961
  const headerH3 = createElement('h3');
835
- headerH3.textContent = 'AI Assistant';
962
+ headerH3.textContent = this.options.headerTitle || 'AI Assistant';
836
963
  const headerP = createElement('p');
837
964
  headerP.textContent = 'Powered by Wabbit';
838
965
  headerText.appendChild(headerH3);
839
966
  headerText.appendChild(headerP);
840
967
  headerTitle.appendChild(headerIcon);
841
968
  headerTitle.appendChild(headerText);
842
- const closeButton = createElement('button', {
843
- class: 'wabbit-chat-panel-close',
844
- 'aria-label': 'Close chat',
845
- type: 'button'
846
- });
847
- closeButton.innerHTML = '×';
848
- closeButton.addEventListener('click', this.options.onClose);
849
969
  header.appendChild(headerTitle);
850
- header.appendChild(closeButton);
851
- // Messages area
852
- this.messagesContainer = createElement('div', { class: 'wabbit-chat-messages' });
853
- // Input area
970
+ // Only add close button for widget mode (not inline)
971
+ if (!isInline) {
972
+ this.closeButton = createElement('button', {
973
+ class: 'wabbit-chat-panel-close',
974
+ 'aria-label': 'Close chat',
975
+ type: 'button'
976
+ });
977
+ this.closeButton.innerHTML = '×';
978
+ const closeHandler = this.options.onClose;
979
+ this.closeButton.addEventListener('click', closeHandler);
980
+ this.eventCleanup.push(() => {
981
+ this.closeButton?.removeEventListener('click', closeHandler);
982
+ });
983
+ header.appendChild(this.closeButton);
984
+ }
985
+ return header;
986
+ }
987
+ /**
988
+ * Create the input area element
989
+ */
990
+ createInputArea() {
854
991
  const inputArea = createElement('div', { class: 'wabbit-chat-input-area' });
855
992
  const inputWrapper = createElement('div', { class: 'wabbit-chat-input-wrapper' });
856
993
  this.inputElement = document.createElement('textarea');
@@ -858,17 +995,26 @@ class ChatPanel {
858
995
  this.inputElement.placeholder = this.options.placeholder || 'Type your message...';
859
996
  this.inputElement.rows = 1;
860
997
  this.inputElement.disabled = this.options.disabled || false;
861
- // Auto-resize textarea
862
- this.inputElement.addEventListener('input', () => {
998
+ // Auto-resize textarea and update send button state on input
999
+ const inputHandler = () => {
863
1000
  this.inputElement.style.height = 'auto';
864
1001
  this.inputElement.style.height = `${Math.min(this.inputElement.scrollHeight, 120)}px`;
1002
+ this.updateSendButtonState();
1003
+ };
1004
+ this.inputElement.addEventListener('input', inputHandler);
1005
+ this.eventCleanup.push(() => {
1006
+ this.inputElement?.removeEventListener('input', inputHandler);
865
1007
  });
866
1008
  // Send on Enter (Shift+Enter for new line)
867
- this.inputElement.addEventListener('keydown', (e) => {
1009
+ const keydownHandler = (e) => {
868
1010
  if (e.key === 'Enter' && !e.shiftKey) {
869
1011
  e.preventDefault();
870
1012
  this.handleSend();
871
1013
  }
1014
+ };
1015
+ this.inputElement.addEventListener('keydown', keydownHandler);
1016
+ this.eventCleanup.push(() => {
1017
+ this.inputElement?.removeEventListener('keydown', keydownHandler);
872
1018
  });
873
1019
  this.sendButton = createElement('button', {
874
1020
  class: 'wabbit-chat-send-button',
@@ -881,23 +1027,16 @@ class ChatPanel {
881
1027
  <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
882
1028
  </svg>
883
1029
  `;
884
- this.sendButton.addEventListener('click', () => this.handleSend());
1030
+ const sendClickHandler = () => this.handleSend();
1031
+ this.sendButton.addEventListener('click', sendClickHandler);
1032
+ this.eventCleanup.push(() => {
1033
+ this.sendButton?.removeEventListener('click', sendClickHandler);
1034
+ });
885
1035
  this.updateSendButtonState();
886
1036
  inputWrapper.appendChild(this.inputElement);
887
1037
  inputWrapper.appendChild(this.sendButton);
888
1038
  inputArea.appendChild(inputWrapper);
889
- // Assemble panel
890
- this.element.appendChild(header);
891
- this.element.appendChild(this.messagesContainer);
892
- this.element.appendChild(inputArea);
893
- document.body.appendChild(this.element);
894
- // Initially hide the panel (it will be shown when opened)
895
- this.element.style.display = 'none';
896
- // Show welcome message if provided
897
- if (this.options.welcomeMessage) {
898
- this.addSystemMessage(this.options.welcomeMessage);
899
- }
900
- return this.element;
1039
+ return inputArea;
901
1040
  }
902
1041
  /**
903
1042
  * Add a message to the panel
@@ -1020,7 +1159,12 @@ class ChatPanel {
1020
1159
  return;
1021
1160
  const hasText = this.inputElement.value.trim().length > 0;
1022
1161
  const disabled = this.options.disabled || this.isWaitingForResponse || !hasText;
1023
- this.sendButton.setAttribute('disabled', disabled ? 'true' : 'false');
1162
+ if (disabled) {
1163
+ this.sendButton.setAttribute('disabled', 'true');
1164
+ }
1165
+ else {
1166
+ this.sendButton.removeAttribute('disabled');
1167
+ }
1024
1168
  if (this.sendButton instanceof HTMLButtonElement) {
1025
1169
  this.sendButton.disabled = disabled;
1026
1170
  }
@@ -1039,8 +1183,9 @@ class ChatPanel {
1039
1183
  show() {
1040
1184
  if (this.element) {
1041
1185
  this.element.style.display = 'flex';
1042
- // Focus input after a brief delay
1186
+ // Focus input and scroll to bottom after a brief delay
1043
1187
  setTimeout(() => {
1188
+ this.scrollToBottom();
1044
1189
  if (this.inputElement) {
1045
1190
  this.inputElement.focus();
1046
1191
  }
@@ -1056,15 +1201,19 @@ class ChatPanel {
1056
1201
  }
1057
1202
  }
1058
1203
  /**
1059
- * Remove the panel from DOM
1204
+ * Remove the panel from DOM and cleanup event listeners
1060
1205
  */
1061
1206
  destroy() {
1207
+ // Run all event cleanup functions
1208
+ this.eventCleanup.forEach((cleanup) => cleanup());
1209
+ this.eventCleanup = [];
1062
1210
  if (this.element) {
1063
1211
  this.element.remove();
1064
1212
  this.element = null;
1065
1213
  this.messagesContainer = null;
1066
1214
  this.inputElement = null;
1067
1215
  this.sendButton = null;
1216
+ this.closeButton = null;
1068
1217
  }
1069
1218
  }
1070
1219
  }
@@ -1528,6 +1677,47 @@ function injectChatStyles(primaryColor = '#6366f1', theme) {
1528
1677
  .wabbit-chat-message {
1529
1678
  animation: wabbit-fade-in 0.2s ease-out;
1530
1679
  }
1680
+
1681
+ /* ========================================
1682
+ * INLINE MODE STYLES
1683
+ * ======================================== */
1684
+
1685
+ /* Inline Chat Panel - renders inside container instead of fixed position */
1686
+ .wabbit-chat-panel.wabbit-chat-panel-inline {
1687
+ position: relative !important;
1688
+ width: 100%;
1689
+ height: 100%;
1690
+ min-height: 400px;
1691
+ max-height: none;
1692
+ max-width: none;
1693
+ bottom: auto !important;
1694
+ right: auto !important;
1695
+ left: auto !important;
1696
+ border-radius: 12px;
1697
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1698
+ animation: none; /* Disable slide-in animation for inline */
1699
+ }
1700
+
1701
+ /* Inline mode messages area fills available space */
1702
+ .wabbit-chat-panel-inline .wabbit-chat-messages {
1703
+ flex: 1;
1704
+ min-height: 200px;
1705
+ }
1706
+
1707
+ /* Full-height variant for inline mode (used in standalone chat pages) */
1708
+ .wabbit-chat-panel-inline.wabbit-chat-panel-fullheight {
1709
+ height: 100vh;
1710
+ max-height: 100vh;
1711
+ border-radius: 0;
1712
+ }
1713
+
1714
+ /* Mobile responsive for inline mode */
1715
+ @media (max-width: 480px) {
1716
+ .wabbit-chat-panel.wabbit-chat-panel-inline {
1717
+ border-radius: 0;
1718
+ min-height: 300px;
1719
+ }
1720
+ }
1531
1721
  `;
1532
1722
  const style = document.createElement('style');
1533
1723
  style.id = styleId;
@@ -1561,7 +1751,10 @@ class ChatWidget {
1561
1751
  this.isOpen = false;
1562
1752
  this.cleanup = [];
1563
1753
  this.themeCleanup = null;
1754
+ this.onChatReadyCallback = null;
1755
+ this.chatReadyFired = false;
1564
1756
  this.config = config;
1757
+ this.onChatReadyCallback = config.onChatReady || null;
1565
1758
  }
1566
1759
  /**
1567
1760
  * Initialize the chat widget
@@ -1573,6 +1766,16 @@ class ChatWidget {
1573
1766
  resolve();
1574
1767
  });
1575
1768
  });
1769
+ const isInlineMode = this.config.mode === 'inline';
1770
+ // Resolve container for inline mode
1771
+ let containerElement = null;
1772
+ if (isInlineMode) {
1773
+ containerElement = this.resolveContainer();
1774
+ if (!containerElement) {
1775
+ console.error(`[Wabbit] Container not found: ${this.config.container}`);
1776
+ return;
1777
+ }
1778
+ }
1576
1779
  // Inject styles with theme configuration
1577
1780
  injectChatStyles(this.config.primaryColor || '#6366f1', this.config.theme);
1578
1781
  // Setup theme watcher if theme is 'auto'
@@ -1583,37 +1786,63 @@ class ChatWidget {
1583
1786
  );
1584
1787
  // Set up event handlers
1585
1788
  this.setupWebSocketHandlers();
1586
- // Create bubble
1587
- this.bubble = new ChatBubble({
1588
- position: this.config.position || 'bottom-right',
1589
- onClick: () => this.toggle()
1590
- });
1591
- // Create panel
1789
+ // Only create bubble for widget mode (not inline)
1790
+ if (!isInlineMode) {
1791
+ this.bubble = new ChatBubble({
1792
+ position: this.config.position || 'bottom-right',
1793
+ onClick: () => this.toggle()
1794
+ });
1795
+ }
1796
+ // Create panel with mode-specific options
1592
1797
  this.panel = new ChatPanel({
1593
1798
  position: this.config.position || 'bottom-right',
1594
1799
  welcomeMessage: this.config.welcomeMessage,
1595
1800
  placeholder: this.config.placeholder,
1596
1801
  onSendMessage: (content) => this.handleSendMessage(content),
1597
1802
  onClose: () => this.close(),
1598
- disabled: true
1803
+ disabled: true,
1804
+ mode: this.config.mode || 'widget',
1805
+ container: containerElement || undefined,
1806
+ showHeader: this.config.showHeader,
1807
+ headerTitle: this.config.headerTitle
1599
1808
  });
1600
1809
  // Render components
1601
- this.bubble.render();
1810
+ if (this.bubble) {
1811
+ this.bubble.render();
1812
+ }
1602
1813
  this.panel.render();
1603
- // Ensure initial state: panel hidden, bubble visible
1604
- // This prevents state mismatch where panel might be visible but isOpen is false
1605
- if (this.panel) {
1606
- this.panel.hide();
1814
+ // Set initial state based on mode
1815
+ if (isInlineMode) {
1816
+ // Inline mode: always visible, mark as open
1817
+ this.isOpen = true;
1607
1818
  }
1608
- if (this.bubble) {
1609
- this.bubble.show();
1819
+ else {
1820
+ // Widget mode: panel hidden, bubble visible
1821
+ if (this.panel) {
1822
+ this.panel.hide();
1823
+ }
1824
+ if (this.bubble) {
1825
+ this.bubble.show();
1826
+ }
1827
+ this.isOpen = false;
1828
+ // Handle trigger types (only for widget mode)
1829
+ this.handleTriggerType();
1610
1830
  }
1611
- this.isOpen = false; // Ensure state is consistent
1612
- // Handle trigger types
1613
- this.handleTriggerType();
1614
1831
  // Connect WebSocket
1615
1832
  await this.wsClient.connect();
1616
1833
  }
1834
+ /**
1835
+ * Resolve container element for inline mode
1836
+ */
1837
+ resolveContainer() {
1838
+ if (!this.config.container)
1839
+ return null;
1840
+ // Try as ID first (with or without # prefix), then as CSS selector
1841
+ const selector = this.config.container;
1842
+ const idWithoutHash = selector.startsWith('#') ? selector.slice(1) : selector;
1843
+ return document.getElementById(idWithoutHash) ||
1844
+ document.querySelector(selector);
1845
+ }
1617
1846
  /**
1618
1847
  * Setup WebSocket event handlers
1619
1848
  */
@@ -1628,6 +1857,18 @@ class ChatWidget {
1628
1857
  this.panel.addSystemMessage(message);
1629
1858
  }
1630
1859
  }
1860
+ // Fire onChatReady callback once when chat is ready
1861
+ // Use requestAnimationFrame to ensure the panel DOM is fully painted
1862
+ // before the callback runs (important for sending initial messages)
1863
+ if (!this.chatReadyFired && this.onChatReadyCallback) {
1864
+ this.chatReadyFired = true;
1865
+ requestAnimationFrame(() => {
1866
+ console.log('[Wabbit] Chat ready, firing onChatReady callback');
1867
+ if (this.onChatReadyCallback) {
1868
+ this.onChatReadyCallback();
1869
+ }
1870
+ });
1871
+ }
1631
1872
  };
1632
1873
  this.wsClient.onMessageHistory = (messages) => {
1633
1874
  console.log('[Wabbit] Loaded message history:', messages.length);
@@ -1726,9 +1967,12 @@ class ChatWidget {
1726
1967
  this.wsClient.sendMessage(content);
1727
1968
  }
1728
1969
  /**
1729
- * Open the chat panel
1970
+ * Open the chat panel (no-op in inline mode)
1730
1971
  */
1731
1972
  open() {
1973
+ // Inline mode is always open
1974
+ if (this.config.mode === 'inline')
1975
+ return;
1732
1976
  if (this.isOpen)
1733
1977
  return;
1734
1978
  this.isOpen = true;
@@ -1740,9 +1984,12 @@ class ChatWidget {
1740
1984
  }
1741
1985
  }
1742
1986
  /**
1743
- * Close the chat panel
1987
+ * Close the chat panel (no-op in inline mode)
1744
1988
  */
1745
1989
  close() {
1990
+ // Inline mode cannot be closed
1991
+ if (this.config.mode === 'inline')
1992
+ return;
1746
1993
  if (!this.isOpen)
1747
1994
  return;
1748
1995
  this.isOpen = false;
@@ -1754,9 +2001,12 @@ class ChatWidget {
1754
2001
  }
1755
2002
  }
1756
2003
  /**
1757
- * Toggle chat panel
2004
+ * Toggle chat panel (no-op in inline mode)
1758
2005
  */
1759
2006
  toggle() {
2007
+ // Inline mode cannot be toggled
2008
+ if (this.config.mode === 'inline')
2009
+ return;
1760
2010
  if (this.isOpen) {
1761
2011
  this.close();
1762
2012
  }
@@ -3562,12 +3812,43 @@ function getInstance() {
3562
3812
  function destroy() {
3563
3813
  return Wabbit.destroy();
3564
3814
  }
3815
+ /**
3816
+ * Get the URL for a dedicated chat page
3817
+ *
3818
+ * @param collectionId - The collection ID
3819
+ * @param options - Optional parameters
3820
+ * @returns Full URL to the chat page
3821
+ *
3822
+ * @example
3823
+ * ```typescript
3824
+ * const url = Wabbit.getChatPageUrl('abc123', { initialMessage: 'Hello!' });
3825
+ * ```
3826
+ */
3827
+ function getChatPageUrl(collectionId, options) {
3828
+ return Wabbit.getChatPageUrl(collectionId, options);
3829
+ }
3830
+ /**
3831
+ * Open chat in a new tab/window
3832
+ *
3833
+ * @param collectionId - The collection ID
3834
+ * @param options - Optional parameters
3835
+ *
3836
+ * @example
3837
+ * ```typescript
3838
+ * Wabbit.openChatPage('abc123', { initialMessage: 'Hello!' });
3839
+ * ```
3840
+ */
3841
+ function openChatPage(collectionId, options) {
3842
+ return Wabbit.openChatPage(collectionId, options);
3843
+ }
3565
3844
  // Create an object as default export (for ESM/CJS: import Wabbit from '@wabbit-dashboard/embed')
3566
3845
  // Note: For UMD, we don't use default export, but directly use named exports
3567
3846
  const WabbitSDK = {
3568
3847
  init,
3569
3848
  getInstance,
3570
- destroy
3849
+ destroy,
3850
+ getChatPageUrl,
3851
+ openChatPage
3571
3852
  };
3572
3853
 
3573
3854
  exports.ApiClient = ApiClient;
@@ -3588,10 +3869,12 @@ exports.default = WabbitSDK;
3588
3869
  exports.destroy = destroy;
3589
3870
  exports.detectTheme = detectTheme;
3590
3871
  exports.escapeHtml = escapeHtml;
3872
+ exports.getChatPageUrl = getChatPageUrl;
3591
3873
  exports.getInstance = getInstance;
3592
3874
  exports.init = init;
3593
3875
  exports.mergeConfig = mergeConfig;
3594
3876
  exports.onDOMReady = onDOMReady;
3877
+ exports.openChatPage = openChatPage;
3595
3878
  exports.storage = storage;
3596
3879
  exports.validateConfig = validateConfig;
3597
3880
  exports.watchTheme = watchTheme;