@convai/web-sdk 1.8.0-beta.6 → 1.8.0-beta.7

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 (86) hide show
  1. package/README.md +285 -1
  2. package/dist/core/ConvaiClient.d.ts +59 -2
  3. package/dist/core/ConvaiClient.d.ts.map +1 -1
  4. package/dist/core/ConvaiClient.js +457 -6
  5. package/dist/core/ConvaiClient.js.map +1 -1
  6. package/dist/core/MessageHandler.d.ts +11 -0
  7. package/dist/core/MessageHandler.d.ts.map +1 -1
  8. package/dist/core/MessageHandler.js +325 -30
  9. package/dist/core/MessageHandler.js.map +1 -1
  10. package/dist/core/SSESession.js +2 -2
  11. package/dist/core/SSESession.js.map +1 -1
  12. package/dist/core/connectRequest.d.ts +38 -3
  13. package/dist/core/connectRequest.d.ts.map +1 -1
  14. package/dist/core/connectRequest.js +57 -11
  15. package/dist/core/connectRequest.js.map +1 -1
  16. package/dist/core/index.d.ts +3 -1
  17. package/dist/core/index.d.ts.map +1 -1
  18. package/dist/core/index.js +2 -1
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/logicalTurn.d.ts +4 -1
  21. package/dist/core/logicalTurn.d.ts.map +1 -1
  22. package/dist/core/logicalTurn.js +35 -4
  23. package/dist/core/logicalTurn.js.map +1 -1
  24. package/dist/core/modelOutput.d.ts +40 -0
  25. package/dist/core/modelOutput.d.ts.map +1 -0
  26. package/dist/core/modelOutput.js +187 -0
  27. package/dist/core/modelOutput.js.map +1 -0
  28. package/dist/core/publishedChat.d.ts +9 -0
  29. package/dist/core/publishedChat.d.ts.map +1 -0
  30. package/dist/core/publishedChat.js +13 -0
  31. package/dist/core/publishedChat.js.map +1 -0
  32. package/dist/core/types.d.ts +225 -25
  33. package/dist/core/types.d.ts.map +1 -1
  34. package/dist/core/types.js.map +1 -1
  35. package/dist/embed/ConvaiChatElement.d.ts +27 -0
  36. package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
  37. package/dist/embed/ConvaiChatElement.js +300 -0
  38. package/dist/embed/ConvaiChatElement.js.map +1 -0
  39. package/dist/embed/browser.d.ts +3 -0
  40. package/dist/embed/browser.d.ts.map +1 -0
  41. package/dist/embed/browser.js +5 -0
  42. package/dist/embed/browser.js.map +1 -0
  43. package/dist/embed/chat-embed-v1.js +100 -0
  44. package/dist/embed/chat-embed-v1.js.map +7 -0
  45. package/dist/embed/index.d.ts +9 -0
  46. package/dist/embed/index.d.ts.map +1 -0
  47. package/dist/embed/index.js +9 -0
  48. package/dist/embed/index.js.map +1 -0
  49. package/dist/embed/publicationApi.d.ts +23 -0
  50. package/dist/embed/publicationApi.d.ts.map +1 -0
  51. package/dist/embed/publicationApi.js +141 -0
  52. package/dist/embed/publicationApi.js.map +1 -0
  53. package/dist/react/components/ConvaiWidget.d.ts +11 -1
  54. package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
  55. package/dist/react/components/ConvaiWidget.js +67 -11
  56. package/dist/react/components/ConvaiWidget.js.map +1 -1
  57. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
  58. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
  59. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
  60. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
  61. package/dist/react/hooks/useConvaiClient.d.ts +2 -0
  62. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  63. package/dist/react/hooks/useConvaiClient.js +8 -0
  64. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  65. package/dist/react/index.d.ts +1 -1
  66. package/dist/react/index.d.ts.map +1 -1
  67. package/dist/react/index.js +1 -1
  68. package/dist/react/index.js.map +1 -1
  69. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  70. package/dist/vanilla/ConvaiWidget.js +153 -32
  71. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  72. package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
  73. package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
  74. package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
  75. package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
  76. package/dist/vanilla/index.d.ts +1 -1
  77. package/dist/vanilla/index.d.ts.map +1 -1
  78. package/dist/vanilla/index.js +1 -1
  79. package/dist/vanilla/index.js.map +1 -1
  80. package/dist/vanilla/types.d.ts +8 -1
  81. package/dist/vanilla/types.d.ts.map +1 -1
  82. package/dist/vanilla/types.js.map +1 -1
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/dist/version.js.map +1 -1
  86. package/package.json +19 -6
@@ -5,8 +5,10 @@
5
5
  import { AudioRenderer } from "./AudioRenderer.js";
6
6
  import { aeroTheme, injectGlobalStyles, injectKeyframes, } from "./styles.js";
7
7
  import { Icons } from "./icons.js";
8
+ import { WidgetEventSubscriptions } from "./WidgetEventSubscriptions.js";
8
9
  import { ConvaiClient } from "../core/ConvaiClient.js";
9
10
  import { parseInlineMarkdown } from "../utils/inlineMarkdown.js";
11
+ import { freshPublicationGrantOptions } from "../core/publishedChat.js";
10
12
  /**
11
13
  * Positioning declarations for the widget root, keyed by `placement`. The
12
14
  * `bottom-right` entry must stay byte-identical to the literals it replaces --
@@ -171,7 +173,7 @@ export function createConvaiWidget(container, options) {
171
173
  blendshapeConfig: options.blendshapeConfig,
172
174
  });
173
175
  })();
174
- const { showVideo = true, showScreenShare = true, defaultVoiceMode = true, onConnect, onDisconnect, onMessage, } = options;
176
+ const { showVideo = true, showScreenShare = true, defaultVoiceMode = true, requestPublicationGrant, publishedChatConnectOptions, publishedChatDescriptor, onConnect, onDisconnect, onMessage, } = options;
175
177
  // Unknown/typo'd values fall back to the default rather than throwing --
176
178
  // a bad `placement` attribute on a customer's page must not blank the
177
179
  // widget.
@@ -183,12 +185,14 @@ export function createConvaiWidget(container, options) {
183
185
  // State
184
186
  let isOpen = false;
185
187
  let isSettingsOpen = false;
186
- let isVoiceMode = defaultVoiceMode;
188
+ let isVoiceMode = defaultVoiceMode &&
189
+ !requestPublicationGrant &&
190
+ client.connectionType !== "text";
187
191
  let isMuted = false;
188
192
  let isVideoVisible = false;
189
193
  let inputValue = "";
190
- let characterName = "Character";
191
- let characterImage = "";
194
+ let characterName = publishedChatDescriptor?.display_name || "Character";
195
+ let characterImage = publishedChatDescriptor?.image_url || "";
192
196
  let audioRenderer = null;
193
197
  let hasEnteredDefaultVoiceMode = false;
194
198
  /** True after we have connected at least once; used to call reconnect() instead of connect() when opening after disconnect */
@@ -209,6 +213,9 @@ export function createConvaiWidget(container, options) {
209
213
  * so a cancellation during the await is honoured.
210
214
  */
211
215
  let pendingOpen = false;
216
+ /** Prevent an in-flight publication grant request from connecting after teardown. */
217
+ let isDestroyed = false;
218
+ const eventSubscriptions = new WidgetEventSubscriptions(() => isDestroyed);
212
219
  // DOM elements (will be created below)
213
220
  let rootElement;
214
221
  let morphingContainer;
@@ -237,6 +244,8 @@ export function createConvaiWidget(container, options) {
237
244
  let source = null;
238
245
  // Fetch character info - matches React useCharacterInfo hook
239
246
  const fetchCharacterInfo = async () => {
247
+ if (publishedChatDescriptor)
248
+ return;
240
249
  // Prefer authToken over apiKey, matching the precedence ConvaiClient
241
250
  // uses when building /connect headers -- a client carrying both must
242
251
  // behave identically in both places.
@@ -1204,7 +1213,8 @@ export function createConvaiWidget(container, options) {
1204
1213
  if (inputValue.length > 0) {
1205
1214
  handleSend();
1206
1215
  }
1207
- else {
1216
+ else if (!requestPublicationGrant &&
1217
+ client.connectionType !== "text") {
1208
1218
  // Toggle Voice Mode
1209
1219
  client.sendInterruptMessage();
1210
1220
  try {
@@ -1319,7 +1329,9 @@ export function createConvaiWidget(container, options) {
1319
1329
  const resetIcon = Icons.Redo("md");
1320
1330
  resetIcon.style.width = "18px";
1321
1331
  resetIcon.style.height = "18px";
1322
- settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
1332
+ if (!client.isPublishedChatSession || requestPublicationGrant) {
1333
+ settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
1334
+ }
1323
1335
  // Video - only show if connection type is video
1324
1336
  const connectionType = client.connectionType;
1325
1337
  if (connectionType === "video" && showVideo) {
@@ -1350,7 +1362,9 @@ export function createConvaiWidget(container, options) {
1350
1362
  const disconnectIcon = document.createElement("span");
1351
1363
  disconnectIcon.innerHTML = "⏻";
1352
1364
  disconnectIcon.style.fontSize = "18px";
1353
- settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
1365
+ if (!client.isPublishedChatSession || requestPublicationGrant) {
1366
+ settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
1367
+ }
1354
1368
  tray.appendChild(settingsRow);
1355
1369
  return tray;
1356
1370
  };
@@ -1419,7 +1433,9 @@ export function createConvaiWidget(container, options) {
1419
1433
  const resetIcon = Icons.Redo("md");
1420
1434
  resetIcon.style.width = "18px";
1421
1435
  resetIcon.style.height = "18px";
1422
- settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
1436
+ if (!client.isPublishedChatSession || requestPublicationGrant) {
1437
+ settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
1438
+ }
1423
1439
  // Video - Always show if showVideo is true and connection type is video
1424
1440
  const connectionType = client.connectionType;
1425
1441
  if (connectionType === "video" && showVideo) {
@@ -1450,7 +1466,9 @@ export function createConvaiWidget(container, options) {
1450
1466
  const disconnectIcon = document.createElement("span");
1451
1467
  disconnectIcon.innerHTML = "⏻";
1452
1468
  disconnectIcon.style.fontSize = "18px";
1453
- settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
1469
+ if (!client.isPublishedChatSession || requestPublicationGrant) {
1470
+ settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
1471
+ }
1454
1472
  };
1455
1473
  // Create floating video - Matches React FloatingVideo component
1456
1474
  const createFloatingVideo = () => {
@@ -1643,6 +1661,8 @@ export function createConvaiWidget(container, options) {
1643
1661
  };
1644
1662
  // Event handlers
1645
1663
  const handleToggle = async () => {
1664
+ if (isDestroyed)
1665
+ return;
1646
1666
  if (isOpen || pendingOpen) {
1647
1667
  // Already open, or an open is already in flight: nothing to do. The
1648
1668
  // `pendingOpen` half is what keeps a repeat call from falling through
@@ -1654,24 +1674,54 @@ export function createConvaiWidget(container, options) {
1654
1674
  if (!client.state.isConnected && !client.state.isConnecting) {
1655
1675
  pendingOpen = true;
1656
1676
  try {
1657
- // Use reconnect when opening again after a disconnect (reuses stored config)
1658
- if (hasConnectedBefore && typeof client.reconnect === "function") {
1677
+ if (requestPublicationGrant || client.isPublishedChatSession) {
1678
+ if (!requestPublicationGrant) {
1679
+ throw new Error("Published chat widgets require requestPublicationGrant to start a new session");
1680
+ }
1681
+ if (!client.connectWithPublicationGrant) {
1682
+ throw new Error("This Convai client does not support published chat connections");
1683
+ }
1684
+ const launchToken = await requestPublicationGrant();
1685
+ // destroy() can run while the grant endpoint is in flight. A one-time
1686
+ // grant must not start a session for a widget which no longer exists.
1687
+ if (isDestroyed) {
1688
+ return;
1689
+ }
1690
+ await client.connectWithPublicationGrant(launchToken, freshPublicationGrantOptions(publishedChatConnectOptions));
1691
+ }
1692
+ else if (hasConnectedBefore && typeof client.reconnect === "function") {
1659
1693
  await client.reconnect();
1660
1694
  }
1661
1695
  else {
1662
1696
  await client.connect();
1663
1697
  }
1698
+ if (isDestroyed) {
1699
+ // destroy() can also run after the grant is exchanged but before
1700
+ // Core or LiveKit finishes connecting. Disconnect that late session
1701
+ // instead of leaving an invisible room alive.
1702
+ await client.disconnect();
1703
+ return;
1704
+ }
1664
1705
  // Re-checked *after* the await: a close()/toggle()/launcher click
1665
1706
  // during the connect clears the flag, and that cancellation has to
1666
1707
  // win. The client stays connected either way -- only the panel is
1667
1708
  // held back -- so a later open() takes the already-connected branch
1668
1709
  // and opens instantly.
1710
+ if (isDestroyed) {
1711
+ // destroy() can also run after the grant is exchanged but before
1712
+ // Core or LiveKit finishes connecting. Disconnect that late session
1713
+ // instead of leaving an invisible room alive.
1714
+ await client.disconnect();
1715
+ return;
1716
+ }
1669
1717
  if (!pendingOpen)
1670
1718
  return;
1671
1719
  setIsOpen(true);
1672
1720
  }
1673
1721
  catch (error) {
1674
- console.error("Failed to connect:", error);
1722
+ if (!isDestroyed) {
1723
+ console.error("Failed to connect:", error);
1724
+ }
1675
1725
  }
1676
1726
  finally {
1677
1727
  pendingOpen = false;
@@ -1697,8 +1747,14 @@ export function createConvaiWidget(container, options) {
1697
1747
  pendingOpen = false;
1698
1748
  setIsOpen(false);
1699
1749
  };
1750
+ // Published sessions must wait for Core's bot-ready handshake. A normal
1751
+ // authenticated text session still gets the text-only UI, but can send as
1752
+ // soon as it connects.
1753
+ const requiresBotReadyForText = () => Boolean(requestPublicationGrant) || Boolean(client.isPublishedChatSession);
1754
+ const isTextInputReady = () => client.state.isConnected &&
1755
+ (!requiresBotReadyForText() || client.isBotReady);
1700
1756
  const handleSend = () => {
1701
- if (inputValue.trim() && client.state.isConnected) {
1757
+ if (inputValue.trim() && isTextInputReady()) {
1702
1758
  client.sendUserTextMessage(inputValue);
1703
1759
  inputValue = "";
1704
1760
  inputElement.value = "";
@@ -1805,6 +1861,8 @@ export function createConvaiWidget(container, options) {
1805
1861
  chatContent.style.pointerEvents = "auto";
1806
1862
  // Auto-enter voice mode if defaultVoiceMode is true and not already entered
1807
1863
  if (defaultVoiceMode &&
1864
+ !requestPublicationGrant &&
1865
+ client.connectionType !== "text" &&
1808
1866
  !hasEnteredDefaultVoiceMode &&
1809
1867
  client.state.isConnected) {
1810
1868
  setTimeout(async () => {
@@ -1848,7 +1906,15 @@ export function createConvaiWidget(container, options) {
1848
1906
  return;
1849
1907
  // Clear previous content
1850
1908
  sendButton.innerHTML = "";
1851
- if (inputValue.length > 0) {
1909
+ // A published session must wait for Core's bot-ready handshake before
1910
+ // accepting text. Preserve the established interactive-widget behavior
1911
+ // for authenticated voice/video sessions.
1912
+ const isPublishedTextChat = requiresBotReadyForText();
1913
+ const textInputReady = isTextInputReady();
1914
+ inputElement.disabled = isPublishedTextChat && !textInputReady;
1915
+ if (inputValue.length > 0 ||
1916
+ requestPublicationGrant ||
1917
+ client.connectionType === "text") {
1852
1918
  // Show Send Button
1853
1919
  sendButton.appendChild(Icons.Send("md"));
1854
1920
  sendButton.style.background = `var(--convai-accent, ${aeroTheme.colors.text.primary})`;
@@ -1861,7 +1927,20 @@ export function createConvaiWidget(container, options) {
1861
1927
  // must contrast with each other, since this is the one place they meet.
1862
1928
  sendButton.style.color = "var(--convai-panel-bg, white)";
1863
1929
  sendButton.style.border = "none";
1864
- sendButton.title = "Send";
1930
+ sendButton.disabled =
1931
+ isPublishedTextChat && (inputValue.length === 0 || !textInputReady);
1932
+ if (isPublishedTextChat) {
1933
+ sendButton.style.opacity =
1934
+ inputValue.length > 0 && textInputReady ? "1" : "0.5";
1935
+ sendButton.title = textInputReady
1936
+ ? inputValue.length > 0
1937
+ ? "Send"
1938
+ : "Type a message"
1939
+ : "Waiting for character";
1940
+ }
1941
+ else {
1942
+ sendButton.title = "Send";
1943
+ }
1865
1944
  }
1866
1945
  else {
1867
1946
  // Show Voice Mode Toggle
@@ -1869,6 +1948,8 @@ export function createConvaiWidget(container, options) {
1869
1948
  sendButton.style.background = "transparent";
1870
1949
  sendButton.style.color = `var(--convai-panel-fg, ${aeroTheme.colors.text.primary})`;
1871
1950
  sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
1951
+ sendButton.style.opacity = "1";
1952
+ sendButton.disabled = false;
1872
1953
  sendButton.title = "Voice Mode";
1873
1954
  }
1874
1955
  };
@@ -2270,7 +2351,17 @@ export function createConvaiWidget(container, options) {
2270
2351
  };
2271
2352
  // Setup event listeners for client state changes
2272
2353
  const setupClientListeners = () => {
2273
- client.on("stateChange", () => {
2354
+ eventSubscriptions.register((listener) => client.on("stateChange", listener), () => {
2355
+ // Published chat (connectionType "text") has no microphone: never let
2356
+ // a voice-mode transition leave the panel in a mode it cannot honour.
2357
+ if (client.connectionType === "text") {
2358
+ if (isVoiceMode) {
2359
+ isVoiceMode = false;
2360
+ hasEnteredDefaultVoiceMode = true;
2361
+ void updateVoiceMode();
2362
+ }
2363
+ updateSendButton();
2364
+ }
2274
2365
  updateHeader();
2275
2366
  // Update pulse animation based on connecting state
2276
2367
  if (client.state.isConnecting) {
@@ -2328,7 +2419,7 @@ export function createConvaiWidget(container, options) {
2328
2419
  });
2329
2420
  // Audio state changes now only affect voice mode (microphone button removed)
2330
2421
  // Video state change listener
2331
- client.videoControls.on("videoStateChange", (videoState) => {
2422
+ eventSubscriptions.register((listener) => client.videoControls.on("videoStateChange", listener), (videoState) => {
2332
2423
  if (videoState.isVideoEnabled !== undefined) {
2333
2424
  isVideoVisible = videoState.isVideoEnabled;
2334
2425
  applyVoiceMode(); // synchronous listener; see applyVoiceMode()
@@ -2381,19 +2472,15 @@ export function createConvaiWidget(container, options) {
2381
2472
  }
2382
2473
  }
2383
2474
  });
2384
- client.on("messagesChange", () => {
2475
+ eventSubscriptions.register((listener) => client.on("messagesChange", listener), () => {
2385
2476
  updateMessageList();
2386
- // Call onMessage callback if provided
2387
- if (onMessage && client.chatMessages.length > 0) {
2388
- const lastMessage = client.chatMessages[client.chatMessages.length - 1];
2389
- onMessage(lastMessage);
2390
- }
2391
2477
  });
2392
2478
  // Bot ready listener - updates UI when bot becomes ready
2393
- client.on("botReady", () => {
2479
+ eventSubscriptions.register((listener) => client.on("botReady", listener), () => {
2394
2480
  updateHeader(); // Update header to show green status
2481
+ updateSendButton();
2395
2482
  });
2396
- client.on("connect", () => {
2483
+ eventSubscriptions.register((listener) => client.on("connect", listener), () => {
2397
2484
  hasConnectedBefore = true;
2398
2485
  // Initialize audio renderer on connection
2399
2486
  // NOTE: the audio elements live under rootElement, so playback ends if
@@ -2411,7 +2498,7 @@ export function createConvaiWidget(container, options) {
2411
2498
  onConnect();
2412
2499
  }
2413
2500
  });
2414
- client.on("disconnect", () => {
2501
+ eventSubscriptions.register((listener) => client.on("disconnect", listener), () => {
2415
2502
  // Cleanup audio renderer
2416
2503
  if (audioRenderer) {
2417
2504
  audioRenderer.destroy();
@@ -2423,6 +2510,42 @@ export function createConvaiWidget(container, options) {
2423
2510
  onDisconnect();
2424
2511
  }
2425
2512
  });
2513
+ // `message` carries the row actually updated. That can differ from the
2514
+ // list tail when correlated v2 streams interleave, which is why onMessage
2515
+ // no longer reads the last element of chatMessages.
2516
+ eventSubscriptions.register((listener) => client.on("message", listener), (message) => {
2517
+ const row = message;
2518
+ if (onMessage && row) {
2519
+ onMessage(row);
2520
+ }
2521
+ });
2522
+ // Screen share state change listener
2523
+ eventSubscriptions.register((listener) => client.screenShareControls.on("screenShareStateChange", listener), (screenShareState) => {
2524
+ if (screenShareState.isScreenShareActive !== undefined) {
2525
+ const isSharing = screenShareState.isScreenShareActive;
2526
+ const shareBtn = document.getElementById("convai-settings-share-btn");
2527
+ if (shareBtn) {
2528
+ const isActive = isSharing;
2529
+ shareBtn.style.backgroundColor = isActive
2530
+ ? "rgba(16, 185, 129, 0.15)"
2531
+ : "transparent";
2532
+ shareBtn.style.color = isActive
2533
+ ? "#10b981"
2534
+ : aeroTheme.colors.text.primary;
2535
+ // Update Icon - first child is icon container
2536
+ const iconContainer = shareBtn.querySelector("div");
2537
+ if (iconContainer) {
2538
+ iconContainer.innerHTML = "";
2539
+ const icon = isActive
2540
+ ? Icons.StopScreenShare("md")
2541
+ : Icons.ScreenShare("md");
2542
+ icon.style.width = "18px";
2543
+ icon.style.height = "18px";
2544
+ iconContainer.appendChild(icon);
2545
+ }
2546
+ }
2547
+ }
2548
+ });
2426
2549
  };
2427
2550
  // Initialize
2428
2551
  createDOM();
@@ -2509,6 +2632,7 @@ export function createConvaiWidget(container, options) {
2509
2632
  updateMessageList();
2510
2633
  },
2511
2634
  destroy: () => {
2635
+ isDestroyed = true;
2512
2636
  // Cleanup audio renderer
2513
2637
  if (audioRenderer) {
2514
2638
  audioRenderer.destroy();
@@ -2523,12 +2647,9 @@ export function createConvaiWidget(container, options) {
2523
2647
  if (floatingVideo.parentElement) {
2524
2648
  floatingVideo.remove();
2525
2649
  }
2526
- // Unsubscribe from client events
2527
- client.off("stateChange", () => { });
2528
- client.off("messagesChange", () => { });
2529
- client.off("botReady", () => { });
2530
- client.off("connect", () => { });
2531
- client.off("disconnect", () => { });
2650
+ // Unsubscribe using the exact callbacks registered above. The guards
2651
+ // also suppress an event that was queued just before teardown.
2652
+ eventSubscriptions.unsubscribeAll();
2532
2653
  },
2533
2654
  };
2534
2655
  return widget;