@convai/web-sdk 1.8.0-beta.7 → 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.
Files changed (39) hide show
  1. package/README.md +19 -1
  2. package/dist/core/index.d.ts +2 -0
  3. package/dist/core/index.d.ts.map +1 -1
  4. package/dist/core/index.js +6 -0
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/embed/ConvaiChatElement.d.ts +77 -1
  7. package/dist/embed/ConvaiChatElement.d.ts.map +1 -1
  8. package/dist/embed/ConvaiChatElement.js +433 -54
  9. package/dist/embed/ConvaiChatElement.js.map +1 -1
  10. package/dist/embed/chat-embed-v1.js +135 -35
  11. package/dist/embed/chat-embed-v1.js.map +3 -3
  12. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts +7 -0
  13. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
  14. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +111 -17
  15. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
  16. package/dist/react/components/rtc-widget/components/UserMessage.js +1 -1
  17. package/dist/react/components/rtc-widget/components/UserMessage.js.map +1 -1
  18. package/dist/react/index.d.ts +3 -0
  19. package/dist/react/index.d.ts.map +1 -1
  20. package/dist/react/index.js +7 -0
  21. package/dist/react/index.js.map +1 -1
  22. package/dist/utils/inlineMarkdown.d.ts +22 -0
  23. package/dist/utils/inlineMarkdown.d.ts.map +1 -1
  24. package/dist/utils/inlineMarkdown.js +84 -7
  25. package/dist/utils/inlineMarkdown.js.map +1 -1
  26. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  27. package/dist/vanilla/ConvaiWidget.js +137 -11
  28. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  29. package/dist/vanilla/index.d.ts +2 -0
  30. package/dist/vanilla/index.d.ts.map +1 -1
  31. package/dist/vanilla/index.js +2 -0
  32. package/dist/vanilla/index.js.map +1 -1
  33. package/dist/vanilla/styles.d.ts.map +1 -1
  34. package/dist/vanilla/styles.js +12 -0
  35. package/dist/vanilla/styles.js.map +1 -1
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/dist/version.js.map +1 -1
  39. package/package.json +32 -10
@@ -7,7 +7,7 @@ import { aeroTheme, injectGlobalStyles, injectKeyframes, } from "./styles.js";
7
7
  import { Icons } from "./icons.js";
8
8
  import { WidgetEventSubscriptions } from "./WidgetEventSubscriptions.js";
9
9
  import { ConvaiClient } from "../core/ConvaiClient.js";
10
- import { parseInlineMarkdown } from "../utils/inlineMarkdown.js";
10
+ import { parseInlineMarkdown, compactUrl } from "../utils/inlineMarkdown.js";
11
11
  import { freshPublicationGrantOptions } from "../core/publishedChat.js";
12
12
  /**
13
13
  * Positioning declarations for the widget root, keyed by `placement`. The
@@ -182,6 +182,10 @@ export function createConvaiWidget(container, options) {
182
182
  : "bottom-right";
183
183
  // Inject global styles
184
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);
185
189
  // State
186
190
  let isOpen = false;
187
191
  let isSettingsOpen = false;
@@ -2071,7 +2075,11 @@ export function createConvaiWidget(container, options) {
2071
2075
  };
2072
2076
  // Microphone button removed - only controlled via voice mode now
2073
2077
  // Helper for Markdown - matches React MarkdownRenderer.tsx
2074
- const renderMarkdown = (text, container) => {
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 } = {}) => {
2075
2083
  if (!text)
2076
2084
  return;
2077
2085
  // Handle both actual newlines and \n escape sequences
@@ -2080,19 +2088,106 @@ export function createConvaiWidget(container, options) {
2080
2088
  lines.forEach((line, lineIndex) => {
2081
2089
  // Process markdown within each line
2082
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
+ */
2083
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
+ `;
2084
2125
  const el = media === "image"
2085
2126
  ? Object.assign(document.createElement("img"), { src, alt, loading: "lazy" })
2086
2127
  : Object.assign(document.createElement("video"), { src, controls: true, preload: "metadata" });
2087
2128
  el.setAttribute("part", "message-media");
2088
2129
  el.style.cssText = `
2089
2130
  display: block;
2090
- max-width: 100%;
2091
- max-height: 240px;
2092
- margin-top: 6px;
2131
+ width: 100%;
2132
+ height: 100%;
2133
+ object-fit: ${media === "image" ? "cover" : "contain"};
2093
2134
  border-radius: 8px;
2135
+ opacity: 0;
2136
+ transition: opacity .18s ease;
2137
+ `;
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;
2094
2162
  `;
2095
- return el;
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;
2096
2191
  };
2097
2192
  const processLine = (text) => {
2098
2193
  const parts = [];
@@ -2119,7 +2214,7 @@ export function createConvaiWidget(container, options) {
2119
2214
  case "link": {
2120
2215
  const a = document.createElement("a");
2121
2216
  a.href = node.href;
2122
- a.textContent = node.label;
2217
+ a.textContent = node.label === node.href ? compactUrl(node.label) : node.label;
2123
2218
  a.target = "_blank";
2124
2219
  a.rel = "noopener noreferrer";
2125
2220
  a.setAttribute("part", "message-link");
@@ -2129,13 +2224,44 @@ export function createConvaiWidget(container, options) {
2129
2224
  word-break: break-all;
2130
2225
  `;
2131
2226
  parts.push(a);
2132
- if (node.media)
2227
+ if (media && node.media)
2133
2228
  parts.push(mediaPreview(node.href, node.label, node.media));
2134
2229
  break;
2135
2230
  }
2136
- case "image":
2137
- parts.push(mediaPreview(node.src, node.alt, "image"));
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);
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);
2138
2263
  break;
2264
+ }
2139
2265
  default:
2140
2266
  parts.push(document.createTextNode(node.text));
2141
2267
  }
@@ -2296,7 +2422,7 @@ export function createConvaiWidget(container, options) {
2296
2422
  white-space: pre-wrap;
2297
2423
  word-wrap: break-word;
2298
2424
  `;
2299
- renderMarkdown(msg.text, contentDiv);
2425
+ renderMarkdown(msg.text, contentDiv, { media: !msg.isUser });
2300
2426
  bubble.appendChild(contentDiv);
2301
2427
  messageWrapper.appendChild(bubble);
2302
2428
  // Timestamp - matches React exactly