@convai/web-sdk 1.8.0-beta.5 → 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.
- package/README.md +333 -13
- package/dist/core/CharacterRoster.d.ts +84 -0
- package/dist/core/CharacterRoster.d.ts.map +1 -0
- package/dist/core/CharacterRoster.js +348 -0
- package/dist/core/CharacterRoster.js.map +1 -0
- package/dist/core/ConvaiClient.d.ts +152 -2
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +1036 -49
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/ConvaiRoomError.d.ts +48 -0
- package/dist/core/ConvaiRoomError.d.ts.map +1 -0
- package/dist/core/ConvaiRoomError.js +111 -0
- package/dist/core/ConvaiRoomError.js.map +1 -0
- package/dist/core/MessageHandler.d.ts +25 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +431 -30
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/SSESession.d.ts +15 -0
- package/dist/core/SSESession.d.ts.map +1 -1
- package/dist/core/SSESession.js +92 -3
- package/dist/core/SSESession.js.map +1 -1
- package/dist/core/connectRequest.d.ts +38 -2
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +57 -0
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +6 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logicalTurn.d.ts +4 -1
- package/dist/core/logicalTurn.d.ts.map +1 -1
- package/dist/core/logicalTurn.js +35 -4
- package/dist/core/logicalTurn.js.map +1 -1
- package/dist/core/modelOutput.d.ts +40 -0
- package/dist/core/modelOutput.d.ts.map +1 -0
- package/dist/core/modelOutput.js +187 -0
- package/dist/core/modelOutput.js.map +1 -0
- package/dist/core/publishedChat.d.ts +9 -0
- package/dist/core/publishedChat.d.ts.map +1 -0
- package/dist/core/publishedChat.js +13 -0
- package/dist/core/publishedChat.js.map +1 -0
- package/dist/core/rosterRequest.d.ts +57 -0
- package/dist/core/rosterRequest.d.ts.map +1 -0
- package/dist/core/rosterRequest.js +210 -0
- package/dist/core/rosterRequest.js.map +1 -0
- package/dist/core/types.d.ts +667 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/embed/ConvaiChatElement.d.ts +27 -0
- package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
- package/dist/embed/ConvaiChatElement.js +300 -0
- package/dist/embed/ConvaiChatElement.js.map +1 -0
- package/dist/embed/browser.d.ts +3 -0
- package/dist/embed/browser.d.ts.map +1 -0
- package/dist/embed/browser.js +5 -0
- package/dist/embed/browser.js.map +1 -0
- package/dist/embed/chat-embed-v1.js +100 -0
- package/dist/embed/chat-embed-v1.js.map +7 -0
- package/dist/embed/index.d.ts +9 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +9 -0
- package/dist/embed/index.js.map +1 -0
- package/dist/embed/publicationApi.d.ts +23 -0
- package/dist/embed/publicationApi.d.ts.map +1 -0
- package/dist/embed/publicationApi.js +141 -0
- package/dist/embed/publicationApi.js.map +1 -0
- package/dist/react/components/ConvaiWidget.d.ts +11 -1
- package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
- package/dist/react/components/ConvaiWidget.js +67 -11
- package/dist/react/components/ConvaiWidget.js.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +39 -44
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
- package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
- package/dist/react/hooks/useConvaiClient.d.ts +2 -0
- package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
- package/dist/react/hooks/useConvaiClient.js +16 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/utils/inlineMarkdown.d.ts +38 -0
- package/dist/utils/inlineMarkdown.d.ts.map +1 -0
- package/dist/utils/inlineMarkdown.js +106 -0
- package/dist/utils/inlineMarkdown.js.map +1 -0
- package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
- package/dist/vanilla/AudioRenderer.js +6 -0
- package/dist/vanilla/AudioRenderer.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +224 -89
- package/dist/vanilla/ConvaiWidget.js.map +1 -1
- package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
- package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
- package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
- package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
- package/dist/vanilla/index.d.ts +2 -1
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +2 -1
- package/dist/vanilla/index.js.map +1 -1
- package/dist/vanilla/types.d.ts +8 -1
- package/dist/vanilla/types.d.ts.map +1 -1
- package/dist/vanilla/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +19 -6
|
@@ -5,7 +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";
|
|
10
|
+
import { parseInlineMarkdown } from "../utils/inlineMarkdown.js";
|
|
11
|
+
import { freshPublicationGrantOptions } from "../core/publishedChat.js";
|
|
9
12
|
/**
|
|
10
13
|
* Positioning declarations for the widget root, keyed by `placement`. The
|
|
11
14
|
* `bottom-right` entry must stay byte-identical to the literals it replaces --
|
|
@@ -170,7 +173,7 @@ export function createConvaiWidget(container, options) {
|
|
|
170
173
|
blendshapeConfig: options.blendshapeConfig,
|
|
171
174
|
});
|
|
172
175
|
})();
|
|
173
|
-
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;
|
|
174
177
|
// Unknown/typo'd values fall back to the default rather than throwing --
|
|
175
178
|
// a bad `placement` attribute on a customer's page must not blank the
|
|
176
179
|
// widget.
|
|
@@ -182,12 +185,14 @@ export function createConvaiWidget(container, options) {
|
|
|
182
185
|
// State
|
|
183
186
|
let isOpen = false;
|
|
184
187
|
let isSettingsOpen = false;
|
|
185
|
-
let isVoiceMode = defaultVoiceMode
|
|
188
|
+
let isVoiceMode = defaultVoiceMode &&
|
|
189
|
+
!requestPublicationGrant &&
|
|
190
|
+
client.connectionType !== "text";
|
|
186
191
|
let isMuted = false;
|
|
187
192
|
let isVideoVisible = false;
|
|
188
193
|
let inputValue = "";
|
|
189
|
-
let characterName = "Character";
|
|
190
|
-
let characterImage = "";
|
|
194
|
+
let characterName = publishedChatDescriptor?.display_name || "Character";
|
|
195
|
+
let characterImage = publishedChatDescriptor?.image_url || "";
|
|
191
196
|
let audioRenderer = null;
|
|
192
197
|
let hasEnteredDefaultVoiceMode = false;
|
|
193
198
|
/** True after we have connected at least once; used to call reconnect() instead of connect() when opening after disconnect */
|
|
@@ -208,6 +213,9 @@ export function createConvaiWidget(container, options) {
|
|
|
208
213
|
* so a cancellation during the await is honoured.
|
|
209
214
|
*/
|
|
210
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);
|
|
211
219
|
// DOM elements (will be created below)
|
|
212
220
|
let rootElement;
|
|
213
221
|
let morphingContainer;
|
|
@@ -236,6 +244,8 @@ export function createConvaiWidget(container, options) {
|
|
|
236
244
|
let source = null;
|
|
237
245
|
// Fetch character info - matches React useCharacterInfo hook
|
|
238
246
|
const fetchCharacterInfo = async () => {
|
|
247
|
+
if (publishedChatDescriptor)
|
|
248
|
+
return;
|
|
239
249
|
// Prefer authToken over apiKey, matching the precedence ConvaiClient
|
|
240
250
|
// uses when building /connect headers -- a client carrying both must
|
|
241
251
|
// behave identically in both places.
|
|
@@ -1203,7 +1213,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1203
1213
|
if (inputValue.length > 0) {
|
|
1204
1214
|
handleSend();
|
|
1205
1215
|
}
|
|
1206
|
-
else
|
|
1216
|
+
else if (!requestPublicationGrant &&
|
|
1217
|
+
client.connectionType !== "text") {
|
|
1207
1218
|
// Toggle Voice Mode
|
|
1208
1219
|
client.sendInterruptMessage();
|
|
1209
1220
|
try {
|
|
@@ -1318,7 +1329,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1318
1329
|
const resetIcon = Icons.Redo("md");
|
|
1319
1330
|
resetIcon.style.width = "18px";
|
|
1320
1331
|
resetIcon.style.height = "18px";
|
|
1321
|
-
|
|
1332
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1333
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
1334
|
+
}
|
|
1322
1335
|
// Video - only show if connection type is video
|
|
1323
1336
|
const connectionType = client.connectionType;
|
|
1324
1337
|
if (connectionType === "video" && showVideo) {
|
|
@@ -1349,7 +1362,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1349
1362
|
const disconnectIcon = document.createElement("span");
|
|
1350
1363
|
disconnectIcon.innerHTML = "⏻";
|
|
1351
1364
|
disconnectIcon.style.fontSize = "18px";
|
|
1352
|
-
|
|
1365
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1366
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
1367
|
+
}
|
|
1353
1368
|
tray.appendChild(settingsRow);
|
|
1354
1369
|
return tray;
|
|
1355
1370
|
};
|
|
@@ -1418,7 +1433,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1418
1433
|
const resetIcon = Icons.Redo("md");
|
|
1419
1434
|
resetIcon.style.width = "18px";
|
|
1420
1435
|
resetIcon.style.height = "18px";
|
|
1421
|
-
|
|
1436
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1437
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
1438
|
+
}
|
|
1422
1439
|
// Video - Always show if showVideo is true and connection type is video
|
|
1423
1440
|
const connectionType = client.connectionType;
|
|
1424
1441
|
if (connectionType === "video" && showVideo) {
|
|
@@ -1449,7 +1466,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1449
1466
|
const disconnectIcon = document.createElement("span");
|
|
1450
1467
|
disconnectIcon.innerHTML = "⏻";
|
|
1451
1468
|
disconnectIcon.style.fontSize = "18px";
|
|
1452
|
-
|
|
1469
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1470
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
1471
|
+
}
|
|
1453
1472
|
};
|
|
1454
1473
|
// Create floating video - Matches React FloatingVideo component
|
|
1455
1474
|
const createFloatingVideo = () => {
|
|
@@ -1642,6 +1661,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1642
1661
|
};
|
|
1643
1662
|
// Event handlers
|
|
1644
1663
|
const handleToggle = async () => {
|
|
1664
|
+
if (isDestroyed)
|
|
1665
|
+
return;
|
|
1645
1666
|
if (isOpen || pendingOpen) {
|
|
1646
1667
|
// Already open, or an open is already in flight: nothing to do. The
|
|
1647
1668
|
// `pendingOpen` half is what keeps a repeat call from falling through
|
|
@@ -1653,24 +1674,54 @@ export function createConvaiWidget(container, options) {
|
|
|
1653
1674
|
if (!client.state.isConnected && !client.state.isConnecting) {
|
|
1654
1675
|
pendingOpen = true;
|
|
1655
1676
|
try {
|
|
1656
|
-
|
|
1657
|
-
|
|
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") {
|
|
1658
1693
|
await client.reconnect();
|
|
1659
1694
|
}
|
|
1660
1695
|
else {
|
|
1661
1696
|
await client.connect();
|
|
1662
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
|
+
}
|
|
1663
1705
|
// Re-checked *after* the await: a close()/toggle()/launcher click
|
|
1664
1706
|
// during the connect clears the flag, and that cancellation has to
|
|
1665
1707
|
// win. The client stays connected either way -- only the panel is
|
|
1666
1708
|
// held back -- so a later open() takes the already-connected branch
|
|
1667
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
|
+
}
|
|
1668
1717
|
if (!pendingOpen)
|
|
1669
1718
|
return;
|
|
1670
1719
|
setIsOpen(true);
|
|
1671
1720
|
}
|
|
1672
1721
|
catch (error) {
|
|
1673
|
-
|
|
1722
|
+
if (!isDestroyed) {
|
|
1723
|
+
console.error("Failed to connect:", error);
|
|
1724
|
+
}
|
|
1674
1725
|
}
|
|
1675
1726
|
finally {
|
|
1676
1727
|
pendingOpen = false;
|
|
@@ -1696,8 +1747,14 @@ export function createConvaiWidget(container, options) {
|
|
|
1696
1747
|
pendingOpen = false;
|
|
1697
1748
|
setIsOpen(false);
|
|
1698
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);
|
|
1699
1756
|
const handleSend = () => {
|
|
1700
|
-
if (inputValue.trim() &&
|
|
1757
|
+
if (inputValue.trim() && isTextInputReady()) {
|
|
1701
1758
|
client.sendUserTextMessage(inputValue);
|
|
1702
1759
|
inputValue = "";
|
|
1703
1760
|
inputElement.value = "";
|
|
@@ -1804,6 +1861,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1804
1861
|
chatContent.style.pointerEvents = "auto";
|
|
1805
1862
|
// Auto-enter voice mode if defaultVoiceMode is true and not already entered
|
|
1806
1863
|
if (defaultVoiceMode &&
|
|
1864
|
+
!requestPublicationGrant &&
|
|
1865
|
+
client.connectionType !== "text" &&
|
|
1807
1866
|
!hasEnteredDefaultVoiceMode &&
|
|
1808
1867
|
client.state.isConnected) {
|
|
1809
1868
|
setTimeout(async () => {
|
|
@@ -1847,7 +1906,15 @@ export function createConvaiWidget(container, options) {
|
|
|
1847
1906
|
return;
|
|
1848
1907
|
// Clear previous content
|
|
1849
1908
|
sendButton.innerHTML = "";
|
|
1850
|
-
|
|
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") {
|
|
1851
1918
|
// Show Send Button
|
|
1852
1919
|
sendButton.appendChild(Icons.Send("md"));
|
|
1853
1920
|
sendButton.style.background = `var(--convai-accent, ${aeroTheme.colors.text.primary})`;
|
|
@@ -1860,7 +1927,20 @@ export function createConvaiWidget(container, options) {
|
|
|
1860
1927
|
// must contrast with each other, since this is the one place they meet.
|
|
1861
1928
|
sendButton.style.color = "var(--convai-panel-bg, white)";
|
|
1862
1929
|
sendButton.style.border = "none";
|
|
1863
|
-
sendButton.
|
|
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
|
+
}
|
|
1864
1944
|
}
|
|
1865
1945
|
else {
|
|
1866
1946
|
// Show Voice Mode Toggle
|
|
@@ -1868,6 +1948,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1868
1948
|
sendButton.style.background = "transparent";
|
|
1869
1949
|
sendButton.style.color = `var(--convai-panel-fg, ${aeroTheme.colors.text.primary})`;
|
|
1870
1950
|
sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
|
|
1951
|
+
sendButton.style.opacity = "1";
|
|
1952
|
+
sendButton.disabled = false;
|
|
1871
1953
|
sendButton.title = "Voice Mode";
|
|
1872
1954
|
}
|
|
1873
1955
|
};
|
|
@@ -1997,59 +2079,65 @@ export function createConvaiWidget(container, options) {
|
|
|
1997
2079
|
const lines = normalizedText.split("\n");
|
|
1998
2080
|
lines.forEach((line, lineIndex) => {
|
|
1999
2081
|
// Process markdown within each line
|
|
2082
|
+
// Bold, italic, links, images and bare URLs — see utils/inlineMarkdown.
|
|
2083
|
+
const mediaPreview = (src, alt, media) => {
|
|
2084
|
+
const el = media === "image"
|
|
2085
|
+
? Object.assign(document.createElement("img"), { src, alt, loading: "lazy" })
|
|
2086
|
+
: Object.assign(document.createElement("video"), { src, controls: true, preload: "metadata" });
|
|
2087
|
+
el.setAttribute("part", "message-media");
|
|
2088
|
+
el.style.cssText = `
|
|
2089
|
+
display: block;
|
|
2090
|
+
max-width: 100%;
|
|
2091
|
+
max-height: 240px;
|
|
2092
|
+
margin-top: 6px;
|
|
2093
|
+
border-radius: 8px;
|
|
2094
|
+
`;
|
|
2095
|
+
return el;
|
|
2096
|
+
};
|
|
2000
2097
|
const processLine = (text) => {
|
|
2001
2098
|
const parts = [];
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
parts.push(document.createTextNode(textBefore));
|
|
2099
|
+
for (const node of parseInlineMarkdown(text)) {
|
|
2100
|
+
switch (node.kind) {
|
|
2101
|
+
case "strong": {
|
|
2102
|
+
// matches React UserBubble styling
|
|
2103
|
+
const strong = document.createElement("strong");
|
|
2104
|
+
strong.textContent = node.text;
|
|
2105
|
+
strong.style.cssText = `
|
|
2106
|
+
font-weight: 600;
|
|
2107
|
+
color: var(--convai-panel-fg, ${aeroTheme.colors.text.primary});
|
|
2108
|
+
`;
|
|
2109
|
+
parts.push(strong);
|
|
2110
|
+
break;
|
|
2015
2111
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
lastIndex = match.index + match[0].length;
|
|
2047
|
-
}
|
|
2048
|
-
// Add remaining text
|
|
2049
|
-
if (lastIndex < remaining.length) {
|
|
2050
|
-
const textAfter = remaining.substring(lastIndex);
|
|
2051
|
-
if (textAfter) {
|
|
2052
|
-
parts.push(document.createTextNode(textAfter));
|
|
2112
|
+
case "em": {
|
|
2113
|
+
const em = document.createElement("em");
|
|
2114
|
+
em.textContent = node.text;
|
|
2115
|
+
em.style.cssText = `font-style: italic;`;
|
|
2116
|
+
parts.push(em);
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
2119
|
+
case "link": {
|
|
2120
|
+
const a = document.createElement("a");
|
|
2121
|
+
a.href = node.href;
|
|
2122
|
+
a.textContent = node.label;
|
|
2123
|
+
a.target = "_blank";
|
|
2124
|
+
a.rel = "noopener noreferrer";
|
|
2125
|
+
a.setAttribute("part", "message-link");
|
|
2126
|
+
a.style.cssText = `
|
|
2127
|
+
color: #10b981;
|
|
2128
|
+
text-decoration: underline;
|
|
2129
|
+
word-break: break-all;
|
|
2130
|
+
`;
|
|
2131
|
+
parts.push(a);
|
|
2132
|
+
if (node.media)
|
|
2133
|
+
parts.push(mediaPreview(node.href, node.label, node.media));
|
|
2134
|
+
break;
|
|
2135
|
+
}
|
|
2136
|
+
case "image":
|
|
2137
|
+
parts.push(mediaPreview(node.src, node.alt, "image"));
|
|
2138
|
+
break;
|
|
2139
|
+
default:
|
|
2140
|
+
parts.push(document.createTextNode(node.text));
|
|
2053
2141
|
}
|
|
2054
2142
|
}
|
|
2055
2143
|
return parts;
|
|
@@ -2263,7 +2351,17 @@ export function createConvaiWidget(container, options) {
|
|
|
2263
2351
|
};
|
|
2264
2352
|
// Setup event listeners for client state changes
|
|
2265
2353
|
const setupClientListeners = () => {
|
|
2266
|
-
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
|
+
}
|
|
2267
2365
|
updateHeader();
|
|
2268
2366
|
// Update pulse animation based on connecting state
|
|
2269
2367
|
if (client.state.isConnecting) {
|
|
@@ -2321,7 +2419,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2321
2419
|
});
|
|
2322
2420
|
// Audio state changes now only affect voice mode (microphone button removed)
|
|
2323
2421
|
// Video state change listener
|
|
2324
|
-
client.videoControls.on("videoStateChange", (videoState) => {
|
|
2422
|
+
eventSubscriptions.register((listener) => client.videoControls.on("videoStateChange", listener), (videoState) => {
|
|
2325
2423
|
if (videoState.isVideoEnabled !== undefined) {
|
|
2326
2424
|
isVideoVisible = videoState.isVideoEnabled;
|
|
2327
2425
|
applyVoiceMode(); // synchronous listener; see applyVoiceMode()
|
|
@@ -2374,19 +2472,15 @@ export function createConvaiWidget(container, options) {
|
|
|
2374
2472
|
}
|
|
2375
2473
|
}
|
|
2376
2474
|
});
|
|
2377
|
-
client.on("messagesChange", () => {
|
|
2475
|
+
eventSubscriptions.register((listener) => client.on("messagesChange", listener), () => {
|
|
2378
2476
|
updateMessageList();
|
|
2379
|
-
// Call onMessage callback if provided
|
|
2380
|
-
if (onMessage && client.chatMessages.length > 0) {
|
|
2381
|
-
const lastMessage = client.chatMessages[client.chatMessages.length - 1];
|
|
2382
|
-
onMessage(lastMessage);
|
|
2383
|
-
}
|
|
2384
2477
|
});
|
|
2385
2478
|
// Bot ready listener - updates UI when bot becomes ready
|
|
2386
|
-
client.on("botReady", () => {
|
|
2479
|
+
eventSubscriptions.register((listener) => client.on("botReady", listener), () => {
|
|
2387
2480
|
updateHeader(); // Update header to show green status
|
|
2481
|
+
updateSendButton();
|
|
2388
2482
|
});
|
|
2389
|
-
client.on("connect", () => {
|
|
2483
|
+
eventSubscriptions.register((listener) => client.on("connect", listener), () => {
|
|
2390
2484
|
hasConnectedBefore = true;
|
|
2391
2485
|
// Initialize audio renderer on connection
|
|
2392
2486
|
// NOTE: the audio elements live under rootElement, so playback ends if
|
|
@@ -2404,7 +2498,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2404
2498
|
onConnect();
|
|
2405
2499
|
}
|
|
2406
2500
|
});
|
|
2407
|
-
client.on("disconnect", () => {
|
|
2501
|
+
eventSubscriptions.register((listener) => client.on("disconnect", listener), () => {
|
|
2408
2502
|
// Cleanup audio renderer
|
|
2409
2503
|
if (audioRenderer) {
|
|
2410
2504
|
audioRenderer.destroy();
|
|
@@ -2416,6 +2510,42 @@ export function createConvaiWidget(container, options) {
|
|
|
2416
2510
|
onDisconnect();
|
|
2417
2511
|
}
|
|
2418
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
|
+
});
|
|
2419
2549
|
};
|
|
2420
2550
|
// Initialize
|
|
2421
2551
|
createDOM();
|
|
@@ -2485,16 +2615,24 @@ export function createConvaiWidget(container, options) {
|
|
|
2485
2615
|
// message never reaches the character and is never billed as an LLM
|
|
2486
2616
|
// turn. The `local-` id keeps it from colliding with a server-issued id.
|
|
2487
2617
|
appendMessage: (message) => {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2618
|
+
// Shared core path, so React and vanilla insert identically. A
|
|
2619
|
+
// hand-rolled IConvaiClient without it falls back to the old local push.
|
|
2620
|
+
if (typeof client.appendMessage === "function") {
|
|
2621
|
+
client.appendMessage(message);
|
|
2622
|
+
}
|
|
2623
|
+
else {
|
|
2624
|
+
const chatMessage = {
|
|
2625
|
+
id: `local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
2626
|
+
type: message.sender === "user" ? "user-llm-text" : "bot-llm-text",
|
|
2627
|
+
content: message.content,
|
|
2628
|
+
timestamp: new Date().toISOString(),
|
|
2629
|
+
};
|
|
2630
|
+
client.chatMessages.push(chatMessage);
|
|
2631
|
+
}
|
|
2495
2632
|
updateMessageList();
|
|
2496
2633
|
},
|
|
2497
2634
|
destroy: () => {
|
|
2635
|
+
isDestroyed = true;
|
|
2498
2636
|
// Cleanup audio renderer
|
|
2499
2637
|
if (audioRenderer) {
|
|
2500
2638
|
audioRenderer.destroy();
|
|
@@ -2509,12 +2647,9 @@ export function createConvaiWidget(container, options) {
|
|
|
2509
2647
|
if (floatingVideo.parentElement) {
|
|
2510
2648
|
floatingVideo.remove();
|
|
2511
2649
|
}
|
|
2512
|
-
// Unsubscribe
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
client.off("botReady", () => { });
|
|
2516
|
-
client.off("connect", () => { });
|
|
2517
|
-
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();
|
|
2518
2653
|
},
|
|
2519
2654
|
};
|
|
2520
2655
|
return widget;
|