@convai/web-sdk 1.8.0-beta.6 → 1.8.0-beta.8
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 +303 -1
- package/dist/core/ConvaiClient.d.ts +59 -2
- package/dist/core/ConvaiClient.d.ts.map +1 -1
- package/dist/core/ConvaiClient.js +457 -6
- package/dist/core/ConvaiClient.js.map +1 -1
- package/dist/core/MessageHandler.d.ts +11 -0
- package/dist/core/MessageHandler.d.ts.map +1 -1
- package/dist/core/MessageHandler.js +325 -30
- package/dist/core/MessageHandler.js.map +1 -1
- package/dist/core/SSESession.js +2 -2
- package/dist/core/SSESession.js.map +1 -1
- package/dist/core/connectRequest.d.ts +38 -3
- package/dist/core/connectRequest.d.ts.map +1 -1
- package/dist/core/connectRequest.js +57 -11
- package/dist/core/connectRequest.js.map +1 -1
- package/dist/core/index.d.ts +5 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -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/types.d.ts +225 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/embed/ConvaiChatElement.d.ts +103 -0
- package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
- package/dist/embed/ConvaiChatElement.js +679 -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 +200 -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 +7 -0
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +111 -17
- package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
- package/dist/react/components/rtc-widget/components/UserMessage.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 +8 -0
- package/dist/react/hooks/useConvaiClient.js.map +1 -1
- package/dist/react/index.d.ts +4 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -1
- package/dist/react/index.js.map +1 -1
- package/dist/utils/inlineMarkdown.d.ts +22 -0
- package/dist/utils/inlineMarkdown.d.ts.map +1 -1
- package/dist/utils/inlineMarkdown.js +84 -7
- package/dist/utils/inlineMarkdown.js.map +1 -1
- package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
- package/dist/vanilla/ConvaiWidget.js +290 -43
- 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 +3 -1
- package/dist/vanilla/index.d.ts.map +1 -1
- package/dist/vanilla/index.js +3 -1
- package/dist/vanilla/index.js.map +1 -1
- package/dist/vanilla/styles.d.ts.map +1 -1
- package/dist/vanilla/styles.js +12 -0
- package/dist/vanilla/styles.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 +48 -13
|
@@ -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
|
-
import { parseInlineMarkdown } from "../utils/inlineMarkdown.js";
|
|
10
|
+
import { parseInlineMarkdown, compactUrl } 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.
|
|
@@ -180,15 +182,21 @@ export function createConvaiWidget(container, options) {
|
|
|
180
182
|
: "bottom-right";
|
|
181
183
|
// Inject global styles
|
|
182
184
|
injectGlobalStyles(styleTarget);
|
|
185
|
+
// Keyframes were injected lazily by the connecting overlay and the settings
|
|
186
|
+
// tray, but a link-preview skeleton can render before either exists — a
|
|
187
|
+
// replayed transcript paints message bubbles immediately. Idempotent.
|
|
188
|
+
injectKeyframes(styleTarget);
|
|
183
189
|
// State
|
|
184
190
|
let isOpen = false;
|
|
185
191
|
let isSettingsOpen = false;
|
|
186
|
-
let isVoiceMode = defaultVoiceMode
|
|
192
|
+
let isVoiceMode = defaultVoiceMode &&
|
|
193
|
+
!requestPublicationGrant &&
|
|
194
|
+
client.connectionType !== "text";
|
|
187
195
|
let isMuted = false;
|
|
188
196
|
let isVideoVisible = false;
|
|
189
197
|
let inputValue = "";
|
|
190
|
-
let characterName = "Character";
|
|
191
|
-
let characterImage = "";
|
|
198
|
+
let characterName = publishedChatDescriptor?.display_name || "Character";
|
|
199
|
+
let characterImage = publishedChatDescriptor?.image_url || "";
|
|
192
200
|
let audioRenderer = null;
|
|
193
201
|
let hasEnteredDefaultVoiceMode = false;
|
|
194
202
|
/** True after we have connected at least once; used to call reconnect() instead of connect() when opening after disconnect */
|
|
@@ -209,6 +217,9 @@ export function createConvaiWidget(container, options) {
|
|
|
209
217
|
* so a cancellation during the await is honoured.
|
|
210
218
|
*/
|
|
211
219
|
let pendingOpen = false;
|
|
220
|
+
/** Prevent an in-flight publication grant request from connecting after teardown. */
|
|
221
|
+
let isDestroyed = false;
|
|
222
|
+
const eventSubscriptions = new WidgetEventSubscriptions(() => isDestroyed);
|
|
212
223
|
// DOM elements (will be created below)
|
|
213
224
|
let rootElement;
|
|
214
225
|
let morphingContainer;
|
|
@@ -237,6 +248,8 @@ export function createConvaiWidget(container, options) {
|
|
|
237
248
|
let source = null;
|
|
238
249
|
// Fetch character info - matches React useCharacterInfo hook
|
|
239
250
|
const fetchCharacterInfo = async () => {
|
|
251
|
+
if (publishedChatDescriptor)
|
|
252
|
+
return;
|
|
240
253
|
// Prefer authToken over apiKey, matching the precedence ConvaiClient
|
|
241
254
|
// uses when building /connect headers -- a client carrying both must
|
|
242
255
|
// behave identically in both places.
|
|
@@ -1204,7 +1217,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1204
1217
|
if (inputValue.length > 0) {
|
|
1205
1218
|
handleSend();
|
|
1206
1219
|
}
|
|
1207
|
-
else
|
|
1220
|
+
else if (!requestPublicationGrant &&
|
|
1221
|
+
client.connectionType !== "text") {
|
|
1208
1222
|
// Toggle Voice Mode
|
|
1209
1223
|
client.sendInterruptMessage();
|
|
1210
1224
|
try {
|
|
@@ -1319,7 +1333,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1319
1333
|
const resetIcon = Icons.Redo("md");
|
|
1320
1334
|
resetIcon.style.width = "18px";
|
|
1321
1335
|
resetIcon.style.height = "18px";
|
|
1322
|
-
|
|
1336
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1337
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
1338
|
+
}
|
|
1323
1339
|
// Video - only show if connection type is video
|
|
1324
1340
|
const connectionType = client.connectionType;
|
|
1325
1341
|
if (connectionType === "video" && showVideo) {
|
|
@@ -1350,7 +1366,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1350
1366
|
const disconnectIcon = document.createElement("span");
|
|
1351
1367
|
disconnectIcon.innerHTML = "⏻";
|
|
1352
1368
|
disconnectIcon.style.fontSize = "18px";
|
|
1353
|
-
|
|
1369
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1370
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
1371
|
+
}
|
|
1354
1372
|
tray.appendChild(settingsRow);
|
|
1355
1373
|
return tray;
|
|
1356
1374
|
};
|
|
@@ -1419,7 +1437,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1419
1437
|
const resetIcon = Icons.Redo("md");
|
|
1420
1438
|
resetIcon.style.width = "18px";
|
|
1421
1439
|
resetIcon.style.height = "18px";
|
|
1422
|
-
|
|
1440
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1441
|
+
settingsRow.appendChild(createOption(resetIcon, "Reset", handleReset));
|
|
1442
|
+
}
|
|
1423
1443
|
// Video - Always show if showVideo is true and connection type is video
|
|
1424
1444
|
const connectionType = client.connectionType;
|
|
1425
1445
|
if (connectionType === "video" && showVideo) {
|
|
@@ -1450,7 +1470,9 @@ export function createConvaiWidget(container, options) {
|
|
|
1450
1470
|
const disconnectIcon = document.createElement("span");
|
|
1451
1471
|
disconnectIcon.innerHTML = "⏻";
|
|
1452
1472
|
disconnectIcon.style.fontSize = "18px";
|
|
1453
|
-
|
|
1473
|
+
if (!client.isPublishedChatSession || requestPublicationGrant) {
|
|
1474
|
+
settingsRow.appendChild(createOption(disconnectIcon, "Disconnect", handleDisconnect, false, true));
|
|
1475
|
+
}
|
|
1454
1476
|
};
|
|
1455
1477
|
// Create floating video - Matches React FloatingVideo component
|
|
1456
1478
|
const createFloatingVideo = () => {
|
|
@@ -1643,6 +1665,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1643
1665
|
};
|
|
1644
1666
|
// Event handlers
|
|
1645
1667
|
const handleToggle = async () => {
|
|
1668
|
+
if (isDestroyed)
|
|
1669
|
+
return;
|
|
1646
1670
|
if (isOpen || pendingOpen) {
|
|
1647
1671
|
// Already open, or an open is already in flight: nothing to do. The
|
|
1648
1672
|
// `pendingOpen` half is what keeps a repeat call from falling through
|
|
@@ -1654,24 +1678,54 @@ export function createConvaiWidget(container, options) {
|
|
|
1654
1678
|
if (!client.state.isConnected && !client.state.isConnecting) {
|
|
1655
1679
|
pendingOpen = true;
|
|
1656
1680
|
try {
|
|
1657
|
-
|
|
1658
|
-
|
|
1681
|
+
if (requestPublicationGrant || client.isPublishedChatSession) {
|
|
1682
|
+
if (!requestPublicationGrant) {
|
|
1683
|
+
throw new Error("Published chat widgets require requestPublicationGrant to start a new session");
|
|
1684
|
+
}
|
|
1685
|
+
if (!client.connectWithPublicationGrant) {
|
|
1686
|
+
throw new Error("This Convai client does not support published chat connections");
|
|
1687
|
+
}
|
|
1688
|
+
const launchToken = await requestPublicationGrant();
|
|
1689
|
+
// destroy() can run while the grant endpoint is in flight. A one-time
|
|
1690
|
+
// grant must not start a session for a widget which no longer exists.
|
|
1691
|
+
if (isDestroyed) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
await client.connectWithPublicationGrant(launchToken, freshPublicationGrantOptions(publishedChatConnectOptions));
|
|
1695
|
+
}
|
|
1696
|
+
else if (hasConnectedBefore && typeof client.reconnect === "function") {
|
|
1659
1697
|
await client.reconnect();
|
|
1660
1698
|
}
|
|
1661
1699
|
else {
|
|
1662
1700
|
await client.connect();
|
|
1663
1701
|
}
|
|
1702
|
+
if (isDestroyed) {
|
|
1703
|
+
// destroy() can also run after the grant is exchanged but before
|
|
1704
|
+
// Core or LiveKit finishes connecting. Disconnect that late session
|
|
1705
|
+
// instead of leaving an invisible room alive.
|
|
1706
|
+
await client.disconnect();
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1664
1709
|
// Re-checked *after* the await: a close()/toggle()/launcher click
|
|
1665
1710
|
// during the connect clears the flag, and that cancellation has to
|
|
1666
1711
|
// win. The client stays connected either way -- only the panel is
|
|
1667
1712
|
// held back -- so a later open() takes the already-connected branch
|
|
1668
1713
|
// and opens instantly.
|
|
1714
|
+
if (isDestroyed) {
|
|
1715
|
+
// destroy() can also run after the grant is exchanged but before
|
|
1716
|
+
// Core or LiveKit finishes connecting. Disconnect that late session
|
|
1717
|
+
// instead of leaving an invisible room alive.
|
|
1718
|
+
await client.disconnect();
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1669
1721
|
if (!pendingOpen)
|
|
1670
1722
|
return;
|
|
1671
1723
|
setIsOpen(true);
|
|
1672
1724
|
}
|
|
1673
1725
|
catch (error) {
|
|
1674
|
-
|
|
1726
|
+
if (!isDestroyed) {
|
|
1727
|
+
console.error("Failed to connect:", error);
|
|
1728
|
+
}
|
|
1675
1729
|
}
|
|
1676
1730
|
finally {
|
|
1677
1731
|
pendingOpen = false;
|
|
@@ -1697,8 +1751,14 @@ export function createConvaiWidget(container, options) {
|
|
|
1697
1751
|
pendingOpen = false;
|
|
1698
1752
|
setIsOpen(false);
|
|
1699
1753
|
};
|
|
1754
|
+
// Published sessions must wait for Core's bot-ready handshake. A normal
|
|
1755
|
+
// authenticated text session still gets the text-only UI, but can send as
|
|
1756
|
+
// soon as it connects.
|
|
1757
|
+
const requiresBotReadyForText = () => Boolean(requestPublicationGrant) || Boolean(client.isPublishedChatSession);
|
|
1758
|
+
const isTextInputReady = () => client.state.isConnected &&
|
|
1759
|
+
(!requiresBotReadyForText() || client.isBotReady);
|
|
1700
1760
|
const handleSend = () => {
|
|
1701
|
-
if (inputValue.trim() &&
|
|
1761
|
+
if (inputValue.trim() && isTextInputReady()) {
|
|
1702
1762
|
client.sendUserTextMessage(inputValue);
|
|
1703
1763
|
inputValue = "";
|
|
1704
1764
|
inputElement.value = "";
|
|
@@ -1805,6 +1865,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1805
1865
|
chatContent.style.pointerEvents = "auto";
|
|
1806
1866
|
// Auto-enter voice mode if defaultVoiceMode is true and not already entered
|
|
1807
1867
|
if (defaultVoiceMode &&
|
|
1868
|
+
!requestPublicationGrant &&
|
|
1869
|
+
client.connectionType !== "text" &&
|
|
1808
1870
|
!hasEnteredDefaultVoiceMode &&
|
|
1809
1871
|
client.state.isConnected) {
|
|
1810
1872
|
setTimeout(async () => {
|
|
@@ -1848,7 +1910,15 @@ export function createConvaiWidget(container, options) {
|
|
|
1848
1910
|
return;
|
|
1849
1911
|
// Clear previous content
|
|
1850
1912
|
sendButton.innerHTML = "";
|
|
1851
|
-
|
|
1913
|
+
// A published session must wait for Core's bot-ready handshake before
|
|
1914
|
+
// accepting text. Preserve the established interactive-widget behavior
|
|
1915
|
+
// for authenticated voice/video sessions.
|
|
1916
|
+
const isPublishedTextChat = requiresBotReadyForText();
|
|
1917
|
+
const textInputReady = isTextInputReady();
|
|
1918
|
+
inputElement.disabled = isPublishedTextChat && !textInputReady;
|
|
1919
|
+
if (inputValue.length > 0 ||
|
|
1920
|
+
requestPublicationGrant ||
|
|
1921
|
+
client.connectionType === "text") {
|
|
1852
1922
|
// Show Send Button
|
|
1853
1923
|
sendButton.appendChild(Icons.Send("md"));
|
|
1854
1924
|
sendButton.style.background = `var(--convai-accent, ${aeroTheme.colors.text.primary})`;
|
|
@@ -1861,7 +1931,20 @@ export function createConvaiWidget(container, options) {
|
|
|
1861
1931
|
// must contrast with each other, since this is the one place they meet.
|
|
1862
1932
|
sendButton.style.color = "var(--convai-panel-bg, white)";
|
|
1863
1933
|
sendButton.style.border = "none";
|
|
1864
|
-
sendButton.
|
|
1934
|
+
sendButton.disabled =
|
|
1935
|
+
isPublishedTextChat && (inputValue.length === 0 || !textInputReady);
|
|
1936
|
+
if (isPublishedTextChat) {
|
|
1937
|
+
sendButton.style.opacity =
|
|
1938
|
+
inputValue.length > 0 && textInputReady ? "1" : "0.5";
|
|
1939
|
+
sendButton.title = textInputReady
|
|
1940
|
+
? inputValue.length > 0
|
|
1941
|
+
? "Send"
|
|
1942
|
+
: "Type a message"
|
|
1943
|
+
: "Waiting for character";
|
|
1944
|
+
}
|
|
1945
|
+
else {
|
|
1946
|
+
sendButton.title = "Send";
|
|
1947
|
+
}
|
|
1865
1948
|
}
|
|
1866
1949
|
else {
|
|
1867
1950
|
// Show Voice Mode Toggle
|
|
@@ -1869,6 +1952,8 @@ export function createConvaiWidget(container, options) {
|
|
|
1869
1952
|
sendButton.style.background = "transparent";
|
|
1870
1953
|
sendButton.style.color = `var(--convai-panel-fg, ${aeroTheme.colors.text.primary})`;
|
|
1871
1954
|
sendButton.style.border = `1px solid ${aeroTheme.colors.neutral[300]}`;
|
|
1955
|
+
sendButton.style.opacity = "1";
|
|
1956
|
+
sendButton.disabled = false;
|
|
1872
1957
|
sendButton.title = "Voice Mode";
|
|
1873
1958
|
}
|
|
1874
1959
|
};
|
|
@@ -1990,7 +2075,11 @@ export function createConvaiWidget(container, options) {
|
|
|
1990
2075
|
};
|
|
1991
2076
|
// Microphone button removed - only controlled via voice mode now
|
|
1992
2077
|
// Helper for Markdown - matches React MarkdownRenderer.tsx
|
|
1993
|
-
|
|
2078
|
+
// `media` renders inline image/video previews under a media link. Bot messages
|
|
2079
|
+
// want them; the user's own bubble does not — you already know what you typed,
|
|
2080
|
+
// and a preview there would let a URL a user pasted embed remote media into
|
|
2081
|
+
// the transcript. Links stay clickable either way.
|
|
2082
|
+
const renderMarkdown = (text, container, { media = true } = {}) => {
|
|
1994
2083
|
if (!text)
|
|
1995
2084
|
return;
|
|
1996
2085
|
// Handle both actual newlines and \n escape sequences
|
|
@@ -1999,19 +2088,106 @@ export function createConvaiWidget(container, options) {
|
|
|
1999
2088
|
lines.forEach((line, lineIndex) => {
|
|
2000
2089
|
// Process markdown within each line
|
|
2001
2090
|
// Bold, italic, links, images and bare URLs — see utils/inlineMarkdown.
|
|
2091
|
+
// `cdn.example.com/a/b/c.png` -> `cdn.example.com · c.png`
|
|
2092
|
+
const describeUrl = (url) => {
|
|
2093
|
+
try {
|
|
2094
|
+
const { host, pathname } = new URL(url);
|
|
2095
|
+
const file = pathname.split("/").filter(Boolean).pop();
|
|
2096
|
+
return file ? `${host} · ${file}` : host;
|
|
2097
|
+
}
|
|
2098
|
+
catch {
|
|
2099
|
+
return url;
|
|
2100
|
+
}
|
|
2101
|
+
};
|
|
2102
|
+
/**
|
|
2103
|
+
* A preview card that reserves its space before the media arrives, so the
|
|
2104
|
+
* bubble does not jump when an image decodes, and collapses to one muted
|
|
2105
|
+
* line if the URL does not resolve — a character can easily invent one,
|
|
2106
|
+
* and an empty player in the transcript reads as a broken widget.
|
|
2107
|
+
*
|
|
2108
|
+
* Chrome is derived from the inherited text colour: the widget is embedded
|
|
2109
|
+
* in someone else's page, on a background this code cannot know.
|
|
2110
|
+
*/
|
|
2002
2111
|
const mediaPreview = (src, alt, media) => {
|
|
2112
|
+
const card = document.createElement("span");
|
|
2113
|
+
card.setAttribute("part", "message-media-card");
|
|
2114
|
+
card.style.cssText = `
|
|
2115
|
+
display: block;
|
|
2116
|
+
position: relative;
|
|
2117
|
+
margin-top: 6px;
|
|
2118
|
+
max-width: 320px;
|
|
2119
|
+
aspect-ratio: 16 / 10;
|
|
2120
|
+
border-radius: 8px;
|
|
2121
|
+
overflow: hidden;
|
|
2122
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
2123
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 10%, transparent);
|
|
2124
|
+
`;
|
|
2003
2125
|
const el = media === "image"
|
|
2004
2126
|
? Object.assign(document.createElement("img"), { src, alt, loading: "lazy" })
|
|
2005
2127
|
: Object.assign(document.createElement("video"), { src, controls: true, preload: "metadata" });
|
|
2006
2128
|
el.setAttribute("part", "message-media");
|
|
2007
2129
|
el.style.cssText = `
|
|
2008
2130
|
display: block;
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2131
|
+
width: 100%;
|
|
2132
|
+
height: 100%;
|
|
2133
|
+
object-fit: ${media === "image" ? "cover" : "contain"};
|
|
2012
2134
|
border-radius: 8px;
|
|
2135
|
+
opacity: 0;
|
|
2136
|
+
transition: opacity .18s ease;
|
|
2013
2137
|
`;
|
|
2014
|
-
|
|
2138
|
+
const skeleton = document.createElement("span");
|
|
2139
|
+
skeleton.setAttribute("part", "message-media-skeleton");
|
|
2140
|
+
skeleton.setAttribute("aria-hidden", "true");
|
|
2141
|
+
skeleton.dataset.convaiSkeleton = "";
|
|
2142
|
+
skeleton.style.cssText = `
|
|
2143
|
+
position: absolute;
|
|
2144
|
+
inset: 0;
|
|
2145
|
+
border-radius: 8px;
|
|
2146
|
+
background: color-mix(in srgb, currentColor 8%, transparent);
|
|
2147
|
+
background-image: linear-gradient(90deg, transparent, color-mix(in srgb, currentColor 12%, transparent), transparent);
|
|
2148
|
+
background-size: 200% 100%;
|
|
2149
|
+
animation: convai-preview-shimmer 1.4s ease-in-out infinite;
|
|
2150
|
+
`;
|
|
2151
|
+
const caption = document.createElement("span");
|
|
2152
|
+
caption.setAttribute("part", "message-media-caption");
|
|
2153
|
+
caption.textContent = describeUrl(src);
|
|
2154
|
+
caption.style.cssText = `
|
|
2155
|
+
display: block;
|
|
2156
|
+
margin-top: 4px;
|
|
2157
|
+
font-size: 11px;
|
|
2158
|
+
opacity: .7;
|
|
2159
|
+
white-space: nowrap;
|
|
2160
|
+
overflow: hidden;
|
|
2161
|
+
text-overflow: ellipsis;
|
|
2162
|
+
`;
|
|
2163
|
+
const settled = () => {
|
|
2164
|
+
el.style.opacity = "1";
|
|
2165
|
+
skeleton.remove();
|
|
2166
|
+
};
|
|
2167
|
+
el.addEventListener(media === "image" ? "load" : "loadedmetadata", settled);
|
|
2168
|
+
el.addEventListener("error", () => {
|
|
2169
|
+
const dead = document.createElement("span");
|
|
2170
|
+
dead.setAttribute("part", "message-media-unavailable");
|
|
2171
|
+
dead.textContent = `Preview unavailable — ${describeUrl(src)}`;
|
|
2172
|
+
dead.style.cssText = `
|
|
2173
|
+
display: block;
|
|
2174
|
+
margin-top: 6px;
|
|
2175
|
+
font-size: 11px;
|
|
2176
|
+
opacity: .55;
|
|
2177
|
+
font-style: italic;
|
|
2178
|
+
white-space: nowrap;
|
|
2179
|
+
overflow: hidden;
|
|
2180
|
+
text-overflow: ellipsis;
|
|
2181
|
+
`;
|
|
2182
|
+
card.replaceWith(dead);
|
|
2183
|
+
caption.remove();
|
|
2184
|
+
});
|
|
2185
|
+
card.appendChild(el);
|
|
2186
|
+
card.appendChild(skeleton);
|
|
2187
|
+
const frag = document.createDocumentFragment();
|
|
2188
|
+
frag.appendChild(card);
|
|
2189
|
+
frag.appendChild(caption);
|
|
2190
|
+
return frag;
|
|
2015
2191
|
};
|
|
2016
2192
|
const processLine = (text) => {
|
|
2017
2193
|
const parts = [];
|
|
@@ -2038,7 +2214,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2038
2214
|
case "link": {
|
|
2039
2215
|
const a = document.createElement("a");
|
|
2040
2216
|
a.href = node.href;
|
|
2041
|
-
a.textContent = node.label;
|
|
2217
|
+
a.textContent = node.label === node.href ? compactUrl(node.label) : node.label;
|
|
2042
2218
|
a.target = "_blank";
|
|
2043
2219
|
a.rel = "noopener noreferrer";
|
|
2044
2220
|
a.setAttribute("part", "message-link");
|
|
@@ -2048,13 +2224,44 @@ export function createConvaiWidget(container, options) {
|
|
|
2048
2224
|
word-break: break-all;
|
|
2049
2225
|
`;
|
|
2050
2226
|
parts.push(a);
|
|
2051
|
-
if (node.media)
|
|
2227
|
+
if (media && node.media)
|
|
2052
2228
|
parts.push(mediaPreview(node.href, node.label, node.media));
|
|
2053
2229
|
break;
|
|
2054
2230
|
}
|
|
2055
|
-
case "
|
|
2056
|
-
|
|
2231
|
+
case "blocked": {
|
|
2232
|
+
const blocked = document.createElement("span");
|
|
2233
|
+
blocked.setAttribute("part", "message-link-blocked");
|
|
2234
|
+
blocked.textContent = node.label;
|
|
2235
|
+
blocked.title = `Link blocked: ${node.href}`;
|
|
2236
|
+
blocked.style.cssText = `
|
|
2237
|
+
opacity: .75;
|
|
2238
|
+
text-decoration: underline dotted;
|
|
2239
|
+
cursor: help;
|
|
2240
|
+
`;
|
|
2241
|
+
parts.push(blocked);
|
|
2057
2242
|
break;
|
|
2243
|
+
}
|
|
2244
|
+
case "image": {
|
|
2245
|
+
if (media) {
|
|
2246
|
+
parts.push(mediaPreview(node.src, node.alt, "image"));
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
// With previews off, an image still has to say what it pointed at,
|
|
2250
|
+
// so it degrades to the same anchor a plain link would render.
|
|
2251
|
+
const imgLink = document.createElement("a");
|
|
2252
|
+
imgLink.href = node.src;
|
|
2253
|
+
imgLink.textContent = node.alt || node.src;
|
|
2254
|
+
imgLink.target = "_blank";
|
|
2255
|
+
imgLink.rel = "noopener noreferrer";
|
|
2256
|
+
imgLink.setAttribute("part", "message-link");
|
|
2257
|
+
imgLink.style.cssText = `
|
|
2258
|
+
color: #10b981;
|
|
2259
|
+
text-decoration: underline;
|
|
2260
|
+
word-break: break-all;
|
|
2261
|
+
`;
|
|
2262
|
+
parts.push(imgLink);
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
2058
2265
|
default:
|
|
2059
2266
|
parts.push(document.createTextNode(node.text));
|
|
2060
2267
|
}
|
|
@@ -2215,7 +2422,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2215
2422
|
white-space: pre-wrap;
|
|
2216
2423
|
word-wrap: break-word;
|
|
2217
2424
|
`;
|
|
2218
|
-
renderMarkdown(msg.text, contentDiv);
|
|
2425
|
+
renderMarkdown(msg.text, contentDiv, { media: !msg.isUser });
|
|
2219
2426
|
bubble.appendChild(contentDiv);
|
|
2220
2427
|
messageWrapper.appendChild(bubble);
|
|
2221
2428
|
// Timestamp - matches React exactly
|
|
@@ -2270,7 +2477,17 @@ export function createConvaiWidget(container, options) {
|
|
|
2270
2477
|
};
|
|
2271
2478
|
// Setup event listeners for client state changes
|
|
2272
2479
|
const setupClientListeners = () => {
|
|
2273
|
-
client.on("stateChange", () => {
|
|
2480
|
+
eventSubscriptions.register((listener) => client.on("stateChange", listener), () => {
|
|
2481
|
+
// Published chat (connectionType "text") has no microphone: never let
|
|
2482
|
+
// a voice-mode transition leave the panel in a mode it cannot honour.
|
|
2483
|
+
if (client.connectionType === "text") {
|
|
2484
|
+
if (isVoiceMode) {
|
|
2485
|
+
isVoiceMode = false;
|
|
2486
|
+
hasEnteredDefaultVoiceMode = true;
|
|
2487
|
+
void updateVoiceMode();
|
|
2488
|
+
}
|
|
2489
|
+
updateSendButton();
|
|
2490
|
+
}
|
|
2274
2491
|
updateHeader();
|
|
2275
2492
|
// Update pulse animation based on connecting state
|
|
2276
2493
|
if (client.state.isConnecting) {
|
|
@@ -2328,7 +2545,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2328
2545
|
});
|
|
2329
2546
|
// Audio state changes now only affect voice mode (microphone button removed)
|
|
2330
2547
|
// Video state change listener
|
|
2331
|
-
client.videoControls.on("videoStateChange", (videoState) => {
|
|
2548
|
+
eventSubscriptions.register((listener) => client.videoControls.on("videoStateChange", listener), (videoState) => {
|
|
2332
2549
|
if (videoState.isVideoEnabled !== undefined) {
|
|
2333
2550
|
isVideoVisible = videoState.isVideoEnabled;
|
|
2334
2551
|
applyVoiceMode(); // synchronous listener; see applyVoiceMode()
|
|
@@ -2381,19 +2598,15 @@ export function createConvaiWidget(container, options) {
|
|
|
2381
2598
|
}
|
|
2382
2599
|
}
|
|
2383
2600
|
});
|
|
2384
|
-
client.on("messagesChange", () => {
|
|
2601
|
+
eventSubscriptions.register((listener) => client.on("messagesChange", listener), () => {
|
|
2385
2602
|
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
2603
|
});
|
|
2392
2604
|
// Bot ready listener - updates UI when bot becomes ready
|
|
2393
|
-
client.on("botReady", () => {
|
|
2605
|
+
eventSubscriptions.register((listener) => client.on("botReady", listener), () => {
|
|
2394
2606
|
updateHeader(); // Update header to show green status
|
|
2607
|
+
updateSendButton();
|
|
2395
2608
|
});
|
|
2396
|
-
client.on("connect", () => {
|
|
2609
|
+
eventSubscriptions.register((listener) => client.on("connect", listener), () => {
|
|
2397
2610
|
hasConnectedBefore = true;
|
|
2398
2611
|
// Initialize audio renderer on connection
|
|
2399
2612
|
// NOTE: the audio elements live under rootElement, so playback ends if
|
|
@@ -2411,7 +2624,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2411
2624
|
onConnect();
|
|
2412
2625
|
}
|
|
2413
2626
|
});
|
|
2414
|
-
client.on("disconnect", () => {
|
|
2627
|
+
eventSubscriptions.register((listener) => client.on("disconnect", listener), () => {
|
|
2415
2628
|
// Cleanup audio renderer
|
|
2416
2629
|
if (audioRenderer) {
|
|
2417
2630
|
audioRenderer.destroy();
|
|
@@ -2423,6 +2636,42 @@ export function createConvaiWidget(container, options) {
|
|
|
2423
2636
|
onDisconnect();
|
|
2424
2637
|
}
|
|
2425
2638
|
});
|
|
2639
|
+
// `message` carries the row actually updated. That can differ from the
|
|
2640
|
+
// list tail when correlated v2 streams interleave, which is why onMessage
|
|
2641
|
+
// no longer reads the last element of chatMessages.
|
|
2642
|
+
eventSubscriptions.register((listener) => client.on("message", listener), (message) => {
|
|
2643
|
+
const row = message;
|
|
2644
|
+
if (onMessage && row) {
|
|
2645
|
+
onMessage(row);
|
|
2646
|
+
}
|
|
2647
|
+
});
|
|
2648
|
+
// Screen share state change listener
|
|
2649
|
+
eventSubscriptions.register((listener) => client.screenShareControls.on("screenShareStateChange", listener), (screenShareState) => {
|
|
2650
|
+
if (screenShareState.isScreenShareActive !== undefined) {
|
|
2651
|
+
const isSharing = screenShareState.isScreenShareActive;
|
|
2652
|
+
const shareBtn = document.getElementById("convai-settings-share-btn");
|
|
2653
|
+
if (shareBtn) {
|
|
2654
|
+
const isActive = isSharing;
|
|
2655
|
+
shareBtn.style.backgroundColor = isActive
|
|
2656
|
+
? "rgba(16, 185, 129, 0.15)"
|
|
2657
|
+
: "transparent";
|
|
2658
|
+
shareBtn.style.color = isActive
|
|
2659
|
+
? "#10b981"
|
|
2660
|
+
: aeroTheme.colors.text.primary;
|
|
2661
|
+
// Update Icon - first child is icon container
|
|
2662
|
+
const iconContainer = shareBtn.querySelector("div");
|
|
2663
|
+
if (iconContainer) {
|
|
2664
|
+
iconContainer.innerHTML = "";
|
|
2665
|
+
const icon = isActive
|
|
2666
|
+
? Icons.StopScreenShare("md")
|
|
2667
|
+
: Icons.ScreenShare("md");
|
|
2668
|
+
icon.style.width = "18px";
|
|
2669
|
+
icon.style.height = "18px";
|
|
2670
|
+
iconContainer.appendChild(icon);
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
});
|
|
2426
2675
|
};
|
|
2427
2676
|
// Initialize
|
|
2428
2677
|
createDOM();
|
|
@@ -2509,6 +2758,7 @@ export function createConvaiWidget(container, options) {
|
|
|
2509
2758
|
updateMessageList();
|
|
2510
2759
|
},
|
|
2511
2760
|
destroy: () => {
|
|
2761
|
+
isDestroyed = true;
|
|
2512
2762
|
// Cleanup audio renderer
|
|
2513
2763
|
if (audioRenderer) {
|
|
2514
2764
|
audioRenderer.destroy();
|
|
@@ -2523,12 +2773,9 @@ export function createConvaiWidget(container, options) {
|
|
|
2523
2773
|
if (floatingVideo.parentElement) {
|
|
2524
2774
|
floatingVideo.remove();
|
|
2525
2775
|
}
|
|
2526
|
-
// Unsubscribe
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
client.off("botReady", () => { });
|
|
2530
|
-
client.off("connect", () => { });
|
|
2531
|
-
client.off("disconnect", () => { });
|
|
2776
|
+
// Unsubscribe using the exact callbacks registered above. The guards
|
|
2777
|
+
// also suppress an event that was queued just before teardown.
|
|
2778
|
+
eventSubscriptions.unsubscribeAll();
|
|
2532
2779
|
},
|
|
2533
2780
|
};
|
|
2534
2781
|
return widget;
|