@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
@@ -1,6 +1,13 @@
1
1
  import React from "react";
2
2
  interface MarkdownRendererProps {
3
3
  text: string;
4
+ /**
5
+ * Render inline image/video previews under a media link. Bot messages want
6
+ * them; the user's own bubble does not — you already know what you typed, and
7
+ * a preview there would let a URL a user pasted embed remote media into the
8
+ * transcript. Links stay clickable either way.
9
+ */
10
+ media?: boolean;
4
11
  }
5
12
  export declare const MarkdownRenderer: React.FC<MarkdownRendererProps>;
6
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownRenderer.d.ts","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/MarkdownRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,UAAU,qBAAqB;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAiDD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAkC5D,CAAC"}
1
+ {"version":3,"file":"MarkdownRenderer.d.ts","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/MarkdownRenderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AA6B1B,UAAU,qBAAqB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA2KD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAkC5D,CAAC"}
@@ -1,6 +1,27 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { parseInlineMarkdown } from "../../../../utils/inlineMarkdown.js";
3
+ import { parseInlineMarkdown, compactUrl, } from "../../../../utils/inlineMarkdown.js";
4
+ const MEDIA_RADIUS = 8;
5
+ const SHIMMER = "convai-preview-shimmer";
6
+ /**
7
+ * The renderer styles inline, but a keyframe cannot be expressed that way, so
8
+ * the one animation it needs is injected once per document. Guarded for SSR and
9
+ * for the tests, which render to a string with no document.
10
+ */
11
+ function ensureShimmerKeyframes() {
12
+ if (typeof document === "undefined")
13
+ return;
14
+ if (document.getElementById(SHIMMER))
15
+ return;
16
+ const style = document.createElement("style");
17
+ style.id = SHIMMER;
18
+ style.textContent = `
19
+ @keyframes ${SHIMMER} { from { background-position: 200% 0 } to { background-position: -200% 0 } }
20
+ @media (prefers-reduced-motion: reduce) {
21
+ [data-convai-skeleton] { animation: none !important }
22
+ }`;
23
+ document.head.appendChild(style);
24
+ }
4
25
  const linkStyle = {
5
26
  color: "#10b981",
6
27
  textDecoration: "underline",
@@ -8,36 +29,109 @@ const linkStyle = {
8
29
  };
9
30
  const mediaStyle = {
10
31
  display: "block",
11
- maxWidth: "100%",
12
- maxHeight: 240,
32
+ width: "100%",
33
+ height: "100%",
34
+ objectFit: "cover",
35
+ borderRadius: MEDIA_RADIUS,
36
+ };
37
+ /**
38
+ * A preview card sizes itself before the media arrives, so the bubble does not
39
+ * jump when an image decodes. 16/10 is a compromise: tall enough to read, short
40
+ * enough that several in a row still scroll.
41
+ */
42
+ const CARD_ASPECT = "16 / 10";
43
+ const cardStyle = {
44
+ // A span by default is inline, where aspect-ratio does nothing.
45
+ display: "block",
46
+ position: "relative",
47
+ marginTop: 6,
48
+ maxWidth: 320,
49
+ aspectRatio: CARD_ASPECT,
50
+ borderRadius: MEDIA_RADIUS,
51
+ overflow: "hidden",
52
+ // The widget is embedded in someone else's page on an unknown background, so
53
+ // chrome is derived from the inherited text colour rather than a fixed grey.
54
+ background: "color-mix(in srgb, currentColor 6%, transparent)",
55
+ boxShadow: "inset 0 0 0 1px color-mix(in srgb, currentColor 10%, transparent)",
56
+ };
57
+ const skeletonStyle = {
58
+ position: "absolute",
59
+ inset: 0,
60
+ borderRadius: MEDIA_RADIUS,
61
+ background: "color-mix(in srgb, currentColor 8%, transparent)",
62
+ backgroundImage: "linear-gradient(90deg, transparent, color-mix(in srgb, currentColor 12%, transparent), transparent)",
63
+ backgroundSize: "200% 100%",
64
+ animation: `${SHIMMER} 1.4s ease-in-out infinite`,
65
+ };
66
+ const captionStyle = {
67
+ display: "block",
68
+ marginTop: 4,
69
+ fontSize: 11,
70
+ opacity: 0.7,
71
+ whiteSpace: "nowrap",
72
+ overflow: "hidden",
73
+ textOverflow: "ellipsis",
74
+ };
75
+ const unavailableStyle = {
76
+ ...captionStyle,
13
77
  marginTop: 6,
14
- borderRadius: 8,
78
+ opacity: 0.55,
79
+ fontStyle: "italic",
15
80
  };
16
- /** Inline preview for an image or video URL, shown under its link. */
17
- const MediaPreview = ({ node }) => {
18
- if (node.media === "image") {
19
- return _jsx("img", { src: node.href, alt: node.label, loading: "lazy", style: mediaStyle });
81
+ /** An unsafe target: the words the character wrote, with no href behind them. */
82
+ const blockedStyle = {
83
+ opacity: 0.75,
84
+ textDecoration: "underline",
85
+ textDecorationStyle: "dotted",
86
+ cursor: "help",
87
+ };
88
+ /** `cdn.example.com/a/b/c.png` → `cdn.example.com · c.png` */
89
+ function describeUrl(url) {
90
+ try {
91
+ const { host, pathname } = new URL(url);
92
+ const file = pathname.split("/").filter(Boolean).pop();
93
+ return file ? `${host} · ${file}` : host;
94
+ }
95
+ catch {
96
+ return compactUrl(url);
20
97
  }
21
- if (node.media === "video") {
22
- return _jsx("video", { src: node.href, controls: true, preload: "metadata", style: mediaStyle });
98
+ }
99
+ /**
100
+ * Inline preview for an image or video URL, shown under its link.
101
+ *
102
+ * The media element is always in the tree so the browser starts fetching it
103
+ * immediately; the skeleton sits on top until it decodes. A URL that does not
104
+ * resolve — which a character can easily invent — collapses to one muted line
105
+ * instead of leaving an empty player in the transcript.
106
+ */
107
+ const MediaPreview = ({ src, alt, kind }) => {
108
+ const [state, setState] = React.useState("loading");
109
+ React.useEffect(ensureShimmerKeyframes, []);
110
+ if (state === "error") {
111
+ return _jsxs("span", { style: unavailableStyle, children: ["Preview unavailable \u2014 ", describeUrl(src)] });
23
112
  }
24
- return null;
113
+ const ready = state === "ready";
114
+ return (_jsxs(_Fragment, { children: [_jsxs("span", { style: cardStyle, children: [kind === "image" ? (_jsx("img", { src: src, alt: alt, loading: "lazy", onLoad: () => setState("ready"), onError: () => setState("error"), style: { ...mediaStyle, opacity: ready ? 1 : 0 } })) : (_jsx("video", { src: src, controls: true, preload: "metadata", onLoadedMetadata: () => setState("ready"), onError: () => setState("error"), style: { ...mediaStyle, objectFit: "contain", opacity: ready ? 1 : 0 } })), !ready && _jsx("span", { "data-convai-skeleton": "", style: skeletonStyle, "aria-hidden": "true" })] }), _jsx("span", { style: captionStyle, children: describeUrl(src) })] }));
25
115
  };
26
- const renderNode = (node, key) => {
116
+ const renderNode = (node, key, media) => {
27
117
  switch (node.kind) {
28
118
  case "strong":
29
119
  return _jsx("strong", { children: node.text }, key);
30
120
  case "em":
31
121
  return _jsx("em", { children: node.text }, key);
32
122
  case "link":
33
- return (_jsxs(React.Fragment, { children: [_jsx("a", { href: node.href, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: node.label }), _jsx(MediaPreview, { node: node })] }, key));
123
+ return (_jsxs(React.Fragment, { children: [_jsx("a", { href: node.href, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: node.label === node.href ? compactUrl(node.label) : node.label }), media && node.media && (_jsx(MediaPreview, { src: node.href, alt: node.label, kind: node.media }))] }, key));
124
+ case "blocked":
125
+ return (_jsx("span", { style: blockedStyle, title: `Link blocked: ${node.href}`, children: node.label }, key));
34
126
  case "image":
35
- return _jsx("img", { src: node.src, alt: node.alt, loading: "lazy", style: mediaStyle }, key);
127
+ // With previews off, an image still has to say what it pointed at, so it
128
+ // degrades to the same anchor a plain link would render.
129
+ return media ? (_jsx(MediaPreview, { src: node.src, alt: node.alt, kind: "image" }, key)) : (_jsx("a", { href: node.src, target: "_blank", rel: "noopener noreferrer", style: linkStyle, children: node.alt || node.src }, key));
36
130
  default:
37
131
  return _jsx(React.Fragment, { children: node.text }, key);
38
132
  }
39
133
  };
40
- export const MarkdownRenderer = ({ text }) => {
134
+ export const MarkdownRenderer = ({ text, media = true }) => {
41
135
  // Convert markdown to JSX elements
42
136
  const renderMarkdown = (input) => {
43
137
  if (!input)
@@ -49,7 +143,7 @@ export const MarkdownRenderer = ({ text }) => {
49
143
  const lines = normalizedText.split('\n');
50
144
  return lines.map((line, lineIndex) => {
51
145
  // Bold, italic, links, images and bare URLs — see utils/inlineMarkdown.
52
- const processedLine = parseInlineMarkdown(line).map((node, i) => renderNode(node, `${node.kind}-${lineIndex}-${i}`));
146
+ const processedLine = parseInlineMarkdown(line).map((node, i) => renderNode(node, `${node.kind}-${lineIndex}-${i}`, media));
53
147
  // Handle empty lines (creates paragraph spacing)
54
148
  if (line.trim() === "" && lineIndex > 0) {
55
149
  return _jsx("br", {}, `br-${lineIndex}`);
@@ -1 +1 @@
1
- {"version":3,"file":"MarkdownRenderer.js","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/MarkdownRenderer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from \"react\";\nimport { parseInlineMarkdown, type InlineNode } from \"../../../../utils/inlineMarkdown\";\n\ninterface MarkdownRendererProps {\n text: string;\n}\n\nconst linkStyle: React.CSSProperties = {\n color: \"#10b981\",\n textDecoration: \"underline\",\n wordBreak: \"break-all\",\n};\n\nconst mediaStyle: React.CSSProperties = {\n display: \"block\",\n maxWidth: \"100%\",\n maxHeight: 240,\n marginTop: 6,\n borderRadius: 8,\n};\n\n/** Inline preview for an image or video URL, shown under its link. */\nconst MediaPreview = ({ node }: { node: Extract<InlineNode, { kind: \"link\" }> }) => {\n if (node.media === \"image\") {\n return <img src={node.href} alt={node.label} loading=\"lazy\" style={mediaStyle} />;\n }\n if (node.media === \"video\") {\n return <video src={node.href} controls preload=\"metadata\" style={mediaStyle} />;\n }\n return null;\n};\n\nconst renderNode = (node: InlineNode, key: string): React.ReactNode => {\n switch (node.kind) {\n case \"strong\":\n return <strong key={key}>{node.text}</strong>;\n case \"em\":\n return <em key={key}>{node.text}</em>;\n case \"link\":\n return (\n <React.Fragment key={key}>\n <a href={node.href} target=\"_blank\" rel=\"noopener noreferrer\" style={linkStyle}>\n {node.label}\n </a>\n <MediaPreview node={node} />\n </React.Fragment>\n );\n case \"image\":\n return <img key={key} src={node.src} alt={node.alt} loading=\"lazy\" style={mediaStyle} />;\n default:\n return <React.Fragment key={key}>{node.text}</React.Fragment>;\n }\n};\n\nexport const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ text }) => {\n // Convert markdown to JSX elements\n const renderMarkdown = (input: string): React.ReactNode => {\n if (!input) return null;\n\n // Handle both actual newlines and \\n escape sequences\n // Replace literal \\n strings with actual newlines\n const normalizedText = input.replace(/\\\\n/g, '\\n');\n\n // Split by actual newlines\n const lines = normalizedText.split('\\n');\n\n return lines.map((line, lineIndex) => {\n // Bold, italic, links, images and bare URLs — see utils/inlineMarkdown.\n const processedLine = parseInlineMarkdown(line).map((node, i) =>\n renderNode(node, `${node.kind}-${lineIndex}-${i}`),\n );\n\n // Handle empty lines (creates paragraph spacing)\n if (line.trim() === \"\" && lineIndex > 0) {\n return <br key={`br-${lineIndex}`} />;\n }\n\n // Return the line with line break\n return (\n <React.Fragment key={`line-${lineIndex}`}>\n {processedLine.length > 0 ? processedLine : line}\n {lineIndex < lines.length - 1 && <br />}\n </React.Fragment>\n );\n });\n };\n\n return <div style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>{renderMarkdown(text)}</div>;\n};\n"]}
1
+ {"version":3,"file":"MarkdownRenderer.js","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/MarkdownRenderer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from \"react\";\nimport {\n parseInlineMarkdown,\n compactUrl,\n type InlineNode,\n type MediaKind,\n} from \"../../../../utils/inlineMarkdown\";\n\nconst MEDIA_RADIUS = 8;\nconst SHIMMER = \"convai-preview-shimmer\";\n\n/**\n * The renderer styles inline, but a keyframe cannot be expressed that way, so\n * the one animation it needs is injected once per document. Guarded for SSR and\n * for the tests, which render to a string with no document.\n */\nfunction ensureShimmerKeyframes(): void {\n if (typeof document === \"undefined\") return;\n if (document.getElementById(SHIMMER)) return;\n const style = document.createElement(\"style\");\n style.id = SHIMMER;\n style.textContent = `\n@keyframes ${SHIMMER} { from { background-position: 200% 0 } to { background-position: -200% 0 } }\n@media (prefers-reduced-motion: reduce) {\n [data-convai-skeleton] { animation: none !important }\n}`;\n document.head.appendChild(style);\n}\n\ninterface MarkdownRendererProps {\n text: string;\n /**\n * Render inline image/video previews under a media link. Bot messages want\n * them; the user's own bubble does not — you already know what you typed, and\n * a preview there would let a URL a user pasted embed remote media into the\n * transcript. Links stay clickable either way.\n */\n media?: boolean;\n}\n\nconst linkStyle: React.CSSProperties = {\n color: \"#10b981\",\n textDecoration: \"underline\",\n wordBreak: \"break-all\",\n};\n\nconst mediaStyle: React.CSSProperties = {\n display: \"block\",\n width: \"100%\",\n height: \"100%\",\n objectFit: \"cover\",\n borderRadius: MEDIA_RADIUS,\n};\n\n/**\n * A preview card sizes itself before the media arrives, so the bubble does not\n * jump when an image decodes. 16/10 is a compromise: tall enough to read, short\n * enough that several in a row still scroll.\n */\nconst CARD_ASPECT = \"16 / 10\";\n\nconst cardStyle: React.CSSProperties = {\n // A span by default is inline, where aspect-ratio does nothing.\n display: \"block\",\n position: \"relative\",\n marginTop: 6,\n maxWidth: 320,\n aspectRatio: CARD_ASPECT,\n borderRadius: MEDIA_RADIUS,\n overflow: \"hidden\",\n // The widget is embedded in someone else's page on an unknown background, so\n // chrome is derived from the inherited text colour rather than a fixed grey.\n background: \"color-mix(in srgb, currentColor 6%, transparent)\",\n boxShadow: \"inset 0 0 0 1px color-mix(in srgb, currentColor 10%, transparent)\",\n};\n\nconst skeletonStyle: React.CSSProperties = {\n position: \"absolute\",\n inset: 0,\n borderRadius: MEDIA_RADIUS,\n background: \"color-mix(in srgb, currentColor 8%, transparent)\",\n backgroundImage:\n \"linear-gradient(90deg, transparent, color-mix(in srgb, currentColor 12%, transparent), transparent)\",\n backgroundSize: \"200% 100%\",\n animation: `${SHIMMER} 1.4s ease-in-out infinite`,\n};\n\nconst captionStyle: React.CSSProperties = {\n display: \"block\",\n marginTop: 4,\n fontSize: 11,\n opacity: 0.7,\n whiteSpace: \"nowrap\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n};\n\nconst unavailableStyle: React.CSSProperties = {\n ...captionStyle,\n marginTop: 6,\n opacity: 0.55,\n fontStyle: \"italic\",\n};\n\n/** An unsafe target: the words the character wrote, with no href behind them. */\nconst blockedStyle: React.CSSProperties = {\n opacity: 0.75,\n textDecoration: \"underline\",\n textDecorationStyle: \"dotted\",\n cursor: \"help\",\n};\n\n/** `cdn.example.com/a/b/c.png` → `cdn.example.com · c.png` */\nfunction describeUrl(url: string): string {\n try {\n const { host, pathname } = new URL(url);\n const file = pathname.split(\"/\").filter(Boolean).pop();\n return file ? `${host} · ${file}` : host;\n } catch {\n return compactUrl(url);\n }\n}\n\ntype LoadState = \"loading\" | \"ready\" | \"error\";\n\n/**\n * Inline preview for an image or video URL, shown under its link.\n *\n * The media element is always in the tree so the browser starts fetching it\n * immediately; the skeleton sits on top until it decodes. A URL that does not\n * resolve — which a character can easily invent — collapses to one muted line\n * instead of leaving an empty player in the transcript.\n */\nconst MediaPreview = ({ src, alt, kind }: { src: string; alt: string; kind: MediaKind }) => {\n const [state, setState] = React.useState<LoadState>(\"loading\");\n React.useEffect(ensureShimmerKeyframes, []);\n\n if (state === \"error\") {\n return <span style={unavailableStyle}>Preview unavailable — {describeUrl(src)}</span>;\n }\n\n const ready = state === \"ready\";\n return (\n <>\n <span style={cardStyle}>\n {kind === \"image\" ? (\n <img\n src={src}\n alt={alt}\n loading=\"lazy\"\n onLoad={() => setState(\"ready\")}\n onError={() => setState(\"error\")}\n style={{ ...mediaStyle, opacity: ready ? 1 : 0 }}\n />\n ) : (\n <video\n src={src}\n controls\n preload=\"metadata\"\n onLoadedMetadata={() => setState(\"ready\")}\n onError={() => setState(\"error\")}\n style={{ ...mediaStyle, objectFit: \"contain\", opacity: ready ? 1 : 0 }}\n />\n )}\n {!ready && <span data-convai-skeleton=\"\" style={skeletonStyle} aria-hidden=\"true\" />}\n </span>\n <span style={captionStyle}>{describeUrl(src)}</span>\n </>\n );\n};\n\nconst renderNode = (node: InlineNode, key: string, media: boolean): React.ReactNode => {\n switch (node.kind) {\n case \"strong\":\n return <strong key={key}>{node.text}</strong>;\n case \"em\":\n return <em key={key}>{node.text}</em>;\n case \"link\":\n return (\n <React.Fragment key={key}>\n <a href={node.href} target=\"_blank\" rel=\"noopener noreferrer\" style={linkStyle}>\n {node.label === node.href ? compactUrl(node.label) : node.label}\n </a>\n {media && node.media && (\n <MediaPreview src={node.href} alt={node.label} kind={node.media} />\n )}\n </React.Fragment>\n );\n case \"blocked\":\n return (\n <span key={key} style={blockedStyle} title={`Link blocked: ${node.href}`}>\n {node.label}\n </span>\n );\n case \"image\":\n // With previews off, an image still has to say what it pointed at, so it\n // degrades to the same anchor a plain link would render.\n return media ? (\n <MediaPreview key={key} src={node.src} alt={node.alt} kind=\"image\" />\n ) : (\n <a key={key} href={node.src} target=\"_blank\" rel=\"noopener noreferrer\" style={linkStyle}>\n {node.alt || node.src}\n </a>\n );\n default:\n return <React.Fragment key={key}>{node.text}</React.Fragment>;\n }\n};\n\nexport const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ text, media = true }) => {\n // Convert markdown to JSX elements\n const renderMarkdown = (input: string): React.ReactNode => {\n if (!input) return null;\n\n // Handle both actual newlines and \\n escape sequences\n // Replace literal \\n strings with actual newlines\n const normalizedText = input.replace(/\\\\n/g, '\\n');\n\n // Split by actual newlines\n const lines = normalizedText.split('\\n');\n\n return lines.map((line, lineIndex) => {\n // Bold, italic, links, images and bare URLs — see utils/inlineMarkdown.\n const processedLine = parseInlineMarkdown(line).map((node, i) =>\n renderNode(node, `${node.kind}-${lineIndex}-${i}`, media),\n );\n\n // Handle empty lines (creates paragraph spacing)\n if (line.trim() === \"\" && lineIndex > 0) {\n return <br key={`br-${lineIndex}`} />;\n }\n\n // Return the line with line break\n return (\n <React.Fragment key={`line-${lineIndex}`}>\n {processedLine.length > 0 ? processedLine : line}\n {lineIndex < lines.length - 1 && <br />}\n </React.Fragment>\n );\n });\n };\n\n return <div style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>{renderMarkdown(text)}</div>;\n};\n"]}
@@ -4,7 +4,7 @@ import { UserBubble, UserMessageContainer, MessageHeader, UserLabel, LogoWrapper
4
4
  import { FaUser } from "react-icons/fa";
5
5
  import { MarkdownRenderer } from "./MarkdownRenderer.js";
6
6
  export const UserMessage = ({ message, timestamp, className, sender, }) => {
7
- return (_jsxs(UserMessageContainer, { className: className, children: [_jsxs(UserBubble, { children: [sender && (_jsxs(MessageHeader, { children: [_jsx(LogoWrapper, { children: _jsx(FaUser, { size: 16, color: aeroTheme.colors.convai.dark }) }), _jsx(UserLabel, { children: sender === "User" ? "You" : sender })] })), _jsx(MarkdownRenderer, { text: message })] }), timestamp && (_jsx("span", { style: {
7
+ return (_jsxs(UserMessageContainer, { className: className, children: [_jsxs(UserBubble, { children: [sender && (_jsxs(MessageHeader, { children: [_jsx(LogoWrapper, { children: _jsx(FaUser, { size: 16, color: aeroTheme.colors.convai.dark }) }), _jsx(UserLabel, { children: sender === "User" ? "You" : sender })] })), _jsx(MarkdownRenderer, { text: message, media: false })] }), timestamp && (_jsx("span", { style: {
8
8
  fontSize: aeroTheme.typography.sizes.caption,
9
9
  color: aeroTheme.colors.text.secondary,
10
10
  marginTop: 4,
@@ -1 +1 @@
1
- {"version":3,"file":"UserMessage.js","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/UserMessage.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAStD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,OAAO,EACP,SAAS,EACT,SAAS,EACT,MAAM,GACW,EAAE,EAAE;IACrB,OAAO,CACL,MAAC,oBAAoB,IAAC,SAAS,EAAE,SAAS,aACxC,MAAC,UAAU,eACR,MAAM,IAAI,CACT,MAAC,aAAa,eACZ,KAAC,WAAW,cACV,KAAC,MAAM,IAAC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAI,GAC7C,EACd,KAAC,SAAS,cAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAa,IAC7C,CACjB,EACD,KAAC,gBAAgB,IAAC,IAAI,EAAE,OAAO,GAAI,IACxB,EACZ,SAAS,IAAI,CACZ,eACE,KAAK,EAAE;oBACL,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO;oBAC5C,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS;oBACtC,SAAS,EAAE,CAAC;oBACZ,SAAS,EAAE,UAAU;oBACrB,UAAU,EAAE,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI;iBACjD,YAEA,SAAS,GACL,CACR,IACoB,CACxB,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import React from \"react\";\nimport { aeroTheme } from \"../styles\";\nimport {\n UserBubble,\n UserMessageContainer,\n MessageHeader,\n UserLabel,\n LogoWrapper,\n} from \"../styles\";\n\nimport { FaUser } from \"react-icons/fa\";\nimport { MarkdownRenderer } from \"./MarkdownRenderer\";\n\ninterface UserMessageProps {\n message: string;\n timestamp?: string;\n className?: string;\n sender?: string;\n}\n\nexport const UserMessage = ({\n message,\n timestamp,\n className,\n sender,\n}: UserMessageProps) => {\n return (\n <UserMessageContainer className={className}>\n <UserBubble>\n {sender && (\n <MessageHeader>\n <LogoWrapper>\n <FaUser size={16} color={aeroTheme.colors.convai.dark} />\n </LogoWrapper>\n <UserLabel>{sender === \"User\" ? \"You\" : sender}</UserLabel>\n </MessageHeader>\n )}\n <MarkdownRenderer text={message} />\n </UserBubble>\n {timestamp && (\n <span\n style={{\n fontSize: aeroTheme.typography.sizes.caption,\n color: aeroTheme.colors.text.secondary,\n marginTop: 4,\n alignSelf: \"flex-end\",\n fontFamily: aeroTheme.typography.fontFamily.body,\n }}\n >\n {timestamp}\n </span>\n )}\n </UserMessageContainer>\n );\n};\n\n"]}
1
+ {"version":3,"file":"UserMessage.js","sourceRoot":"","sources":["../../../../../src/react/components/rtc-widget/components/UserMessage.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAStD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,OAAO,EACP,SAAS,EACT,SAAS,EACT,MAAM,GACW,EAAE,EAAE;IACrB,OAAO,CACL,MAAC,oBAAoB,IAAC,SAAS,EAAE,SAAS,aACxC,MAAC,UAAU,eACR,MAAM,IAAI,CACT,MAAC,aAAa,eACZ,KAAC,WAAW,cACV,KAAC,MAAM,IAAC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,GAAI,GAC7C,EACd,KAAC,SAAS,cAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAa,IAC7C,CACjB,EACD,KAAC,gBAAgB,IAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAI,IACtC,EACZ,SAAS,IAAI,CACZ,eACE,KAAK,EAAE;oBACL,QAAQ,EAAE,SAAS,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO;oBAC5C,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS;oBACtC,SAAS,EAAE,CAAC;oBACZ,SAAS,EAAE,UAAU;oBACrB,UAAU,EAAE,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI;iBACjD,YAEA,SAAS,GACL,CACR,IACoB,CACxB,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import React from \"react\";\nimport { aeroTheme } from \"../styles\";\nimport {\n UserBubble,\n UserMessageContainer,\n MessageHeader,\n UserLabel,\n LogoWrapper,\n} from \"../styles\";\n\nimport { FaUser } from \"react-icons/fa\";\nimport { MarkdownRenderer } from \"./MarkdownRenderer\";\n\ninterface UserMessageProps {\n message: string;\n timestamp?: string;\n className?: string;\n sender?: string;\n}\n\nexport const UserMessage = ({\n message,\n timestamp,\n className,\n sender,\n}: UserMessageProps) => {\n return (\n <UserMessageContainer className={className}>\n <UserBubble>\n {sender && (\n <MessageHeader>\n <LogoWrapper>\n <FaUser size={16} color={aeroTheme.colors.convai.dark} />\n </LogoWrapper>\n <UserLabel>{sender === \"User\" ? \"You\" : sender}</UserLabel>\n </MessageHeader>\n )}\n <MarkdownRenderer text={message} media={false} />\n </UserBubble>\n {timestamp && (\n <span\n style={{\n fontSize: aeroTheme.typography.sizes.caption,\n color: aeroTheme.colors.text.secondary,\n marginTop: 4,\n alignSelf: \"flex-end\",\n fontFamily: aeroTheme.typography.fontFamily.body,\n }}\n >\n {timestamp}\n </span>\n )}\n </UserMessageContainer>\n );\n};\n\n"]}
@@ -1,5 +1,8 @@
1
1
  export { useConvaiClient } from './hooks/useConvaiClient.js';
2
2
  export { ConvaiWidget } from './components/ConvaiWidget.js';
3
+ export { MarkdownRenderer } from './components/rtc-widget/components/MarkdownRenderer.js';
4
+ export { parseInlineMarkdown, mediaKindForUrl, isSafeHref, compactUrl } from '../utils/inlineMarkdown.js';
5
+ export type { InlineNode, MediaKind } from '../utils/inlineMarkdown.js';
3
6
  export * from './hooks/index.js';
4
7
  export * from '../core/types.js';
5
8
  export { getDisconnectReasonMessage } from '../core/types.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,cAAc,SAAS,CAAC;AAGxB,cAAc,eAAe,CAAC;AAG9B,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,EAAE,iBAAiB,IAAI,aAAa,EAAE,WAAW,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAG1D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAMzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,qDAAqD,CAAC;AAGvF,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACvG,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAGrE,cAAc,SAAS,CAAC;AAGxB,cAAc,eAAe,CAAC;AAG9B,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7D,OAAO,EAAE,iBAAiB,IAAI,aAAa,EAAE,WAAW,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC"}
@@ -2,6 +2,13 @@
2
2
  export { useConvaiClient } from './hooks/useConvaiClient.js';
3
3
  // Components
4
4
  export { ConvaiWidget } from './components/ConvaiWidget.js';
5
+ // Bubble renderer, for a custom chat UI that is not ConvaiWidget. Pass
6
+ // `media={false}` on the user's own bubble: previews are a bot-side
7
+ // affordance, and rendering them on a line the user typed lets any pasted URL
8
+ // pull remote media into the transcript.
9
+ export { MarkdownRenderer } from './components/rtc-widget/components/MarkdownRenderer.js';
10
+ // The tokenizer behind it, for building a renderer from scratch.
11
+ export { parseInlineMarkdown, mediaKindForUrl, isSafeHref, compactUrl } from '../utils/inlineMarkdown.js';
5
12
  // Additional hooks (for advanced usage)
6
13
  export * from './hooks/index.js';
7
14
  // Re-export core types for convenience
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,aAAa;AACb,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,wCAAwC;AACxC,cAAc,SAAS,CAAC;AAExB,uCAAuC;AACvC,cAAc,eAAe,CAAC;AAE9B,wCAAwC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAE3D,2CAA2C;AAC3C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7D,qEAAqE;AACrE,OAAO,EAAE,iBAAiB,IAAI,aAAa,EAAE,WAAW,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["// Main hook (backward compatible)\nexport { useConvaiClient } from './hooks/useConvaiClient';\n\n// Components\nexport { ConvaiWidget } from './components/ConvaiWidget';\n\n// Additional hooks (for advanced usage)\nexport * from './hooks';\n\n// Re-export core types for convenience\nexport * from '../core/types';\n\n// Re-export helper functions explicitly\nexport { getDisconnectReasonMessage } from '../core/types';\n\n// Re-export core client for advanced usage\nexport { ActionResultAckError, ConvaiClient } from '../core';\n\n// Re-export LiveKit components for convenience (backward compatible)\nexport { RoomAudioRenderer as AudioRenderer, RoomContext as AudioContext } from '@livekit/components-react';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,aAAa;AACb,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,uEAAuE;AACvE,oEAAoE;AACpE,8EAA8E;AAC9E,yCAAyC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,qDAAqD,CAAC;AAEvF,iEAAiE;AACjE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAGvG,wCAAwC;AACxC,cAAc,SAAS,CAAC;AAExB,uCAAuC;AACvC,cAAc,eAAe,CAAC;AAE9B,wCAAwC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAE3D,2CAA2C;AAC3C,OAAO,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7D,qEAAqE;AACrE,OAAO,EAAE,iBAAiB,IAAI,aAAa,EAAE,WAAW,IAAI,YAAY,EAAE,MAAM,2BAA2B,CAAC","sourcesContent":["// Main hook (backward compatible)\nexport { useConvaiClient } from './hooks/useConvaiClient';\n\n// Components\nexport { ConvaiWidget } from './components/ConvaiWidget';\n\n// Bubble renderer, for a custom chat UI that is not ConvaiWidget. Pass\n// `media={false}` on the user's own bubble: previews are a bot-side\n// affordance, and rendering them on a line the user typed lets any pasted URL\n// pull remote media into the transcript.\nexport { MarkdownRenderer } from './components/rtc-widget/components/MarkdownRenderer';\n\n// The tokenizer behind it, for building a renderer from scratch.\nexport { parseInlineMarkdown, mediaKindForUrl, isSafeHref, compactUrl } from '../utils/inlineMarkdown';\nexport type { InlineNode, MediaKind } from '../utils/inlineMarkdown';\n\n// Additional hooks (for advanced usage)\nexport * from './hooks';\n\n// Re-export core types for convenience\nexport * from '../core/types';\n\n// Re-export helper functions explicitly\nexport { getDisconnectReasonMessage } from '../core/types';\n\n// Re-export core client for advanced usage\nexport { ActionResultAckError, ConvaiClient } from '../core';\n\n// Re-export LiveKit components for convenience (backward compatible)\nexport { RoomAudioRenderer as AudioRenderer, RoomContext as AudioContext } from '@livekit/components-react';\n"]}
@@ -31,8 +31,30 @@ export type InlineNode = {
31
31
  kind: "image";
32
32
  src: string;
33
33
  alt: string;
34
+ }
35
+ /**
36
+ * A `[label](target)` whose scheme is not on the allowlist. It carries the
37
+ * label so a renderer can show the words the character wrote instead of raw
38
+ * markdown, and the target so it can be surfaced on hover — but a renderer
39
+ * must never put `href` on an anchor. That is the whole point of the node.
40
+ */
41
+ | {
42
+ kind: "blocked";
43
+ href: string;
44
+ label: string;
34
45
  };
35
46
  /** Classify a URL by its path extension; null for anything not previewable. */
36
47
  export declare function mediaKindForUrl(url: string): MediaKind | null;
48
+ /**
49
+ * A bare URL is its own label, and a long one wraps over several lines before
50
+ * the message even starts — in a 400px panel that swamps the bubble. Compact it
51
+ * the way a browser address bar does: keep what identifies the target, elide
52
+ * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`
53
+ *
54
+ * Presentation only: the href is always the full URL.
55
+ */
56
+ export declare function compactUrl(url: string, max?: number): string;
57
+ /** True when `url` uses a scheme safe to expose as an anchor href. */
58
+ export declare function isSafeHref(url: string): boolean;
37
59
  export declare function parseInlineMarkdown(line: string): InlineNode[];
38
60
  //# sourceMappingURL=inlineMarkdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inlineMarkdown.d.ts","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAKhD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU7D;AAqCD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAkC9D"}
1
+ {"version":3,"file":"inlineMarkdown.d.ts","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1C,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,GAAG,IAAI,CAAA;CAAE,GACtE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAC7C;;;;;GAKG;GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAKrD,+EAA+E;AAC/E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAU7D;AAoBD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,SAAK,GAAG,MAAM,CAkBxD;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG/C;AAqDD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CA4C9D"}
@@ -29,12 +29,78 @@ export function mediaKindForUrl(url) {
29
29
  return "video";
30
30
  return null;
31
31
  }
32
+ /**
33
+ * Schemes a renderer may turn into an anchor.
34
+ *
35
+ * The list is an allowlist, never a denylist, because the server does NOT
36
+ * sanitise schemes — core-service echoes `[x](javascript:alert(1))` back
37
+ * verbatim (verified against dev4, 2026-09-10). The character's output is
38
+ * attacker-influenced text, so this is the only thing standing between a
39
+ * prompt-injected `javascript:` href and script execution in the host page.
40
+ * Anything not listed here stays literal text.
41
+ */
42
+ const SAFE_SCHEMES = ["http", "https", "ftp", "ftps", "mailto", "tel", "sms"];
43
+ /** Schemes with an authority; the rest (mailto/tel/sms) are opaque. */
44
+ const NETWORK_SCHEMES = ["http", "https", "ftp", "ftps"];
45
+ const schemeAlt = SAFE_SCHEMES.join("|");
46
+ const networkAlt = NETWORK_SCHEMES.join("|");
47
+ /**
48
+ * A bare URL is its own label, and a long one wraps over several lines before
49
+ * the message even starts — in a 400px panel that swamps the bubble. Compact it
50
+ * the way a browser address bar does: keep what identifies the target, elide
51
+ * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`
52
+ *
53
+ * Presentation only: the href is always the full URL.
54
+ */
55
+ export function compactUrl(url, max = 44) {
56
+ if (url.length <= max)
57
+ return url;
58
+ let host;
59
+ let pathname;
60
+ try {
61
+ ({ host, pathname } = new URL(url));
62
+ }
63
+ catch {
64
+ return `${url.slice(0, max - 1)}…`;
65
+ }
66
+ const file = pathname.split("/").filter(Boolean).pop();
67
+ if (!file)
68
+ return host.length <= max ? host : `${host.slice(0, max - 1)}…`;
69
+ const compact = `${host}/…/${file}`;
70
+ if (compact.length <= max)
71
+ return compact;
72
+ // A long host would otherwise eat the budget and clip the filename down to
73
+ // its extension. The filename identifies what the link actually is, so drop
74
+ // the host instead of shortening it.
75
+ if (file.length <= max)
76
+ return file;
77
+ return `…${file.slice(-(max - 1))}`;
78
+ }
79
+ /** True when `url` uses a scheme safe to expose as an anchor href. */
80
+ export function isSafeHref(url) {
81
+ const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1];
82
+ return scheme !== undefined && SAFE_SCHEMES.includes(scheme.toLowerCase());
83
+ }
32
84
  // Alternatives are tried in this order at every position:
33
- // 1. ![alt](url) / [text](url) with an http(s) URL
34
- // 2. bare http(s) URL, unless it is the target of a link still streaming in
35
- // 3. **bold** / __bold__ / *em* / _em_
85
+ // 1. ![alt](url) / [text](url), with an optional CommonMark "title"
86
+ // 2. <url> autolink, angle brackets consumed
87
+ // 3. bare URL, unless it is the target of a link still streaming in
88
+ // 4. **bold** / __bold__ / *em* / _em_
36
89
  // Matching URLs before emphasis is what keeps `_` inside a URL literal.
37
- const TOKEN = /(!?)\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)|(?<!\]\()(https?:\/\/[^\s<>"'`]+)|(\*\*|__)(.*?)\5|\*([^*]+)\*|_([^_]+)_/g;
90
+ // Alternative 1 accepts ANY scheme so an unsafe one can be recognised and
91
+ // re-emitted as literal text; without that, `[x](javascript:…)` would fall
92
+ // through to the emphasis rules and get partially mangled.
93
+ const TOKEN = new RegExp([
94
+ // The target allows one level of balanced parens, so `javascript:alert(1)`
95
+ // and `…/a_(b).pdf` are consumed whole. Without it the inner `)` ended the
96
+ // match and the outer one was left stranded in the text.
97
+ `(!?)\\[([^\\]]*)\\]\\(\\s*((?:[^\\s()]|\\([^()]*\\))+)(?:\\s+"[^"]*")?\\s*\\)`,
98
+ `<((?:${schemeAlt}):[^\\s<>]+)>`,
99
+ `(?<!\\]\\()((?:${networkAlt}):\\/\\/[^\\s<>"'\`]+|(?:mailto|tel|sms):[^\\s<>"'\`]+)`,
100
+ `(\\*\\*|__)(.*?)\\6`,
101
+ `\\*([^*]+)\\*`,
102
+ `_([^_]+)_`,
103
+ ].join("|"), "gi");
38
104
  const TRAILING_PUNCTUATION = ".,;:!?'\"";
39
105
  /**
40
106
  * Sentence punctuation after a bare URL belongs to the sentence, and an
@@ -76,12 +142,23 @@ export function parseInlineMarkdown(line) {
76
142
  let match;
77
143
  while ((match = TOKEN.exec(line)) !== null) {
78
144
  pushText(line.slice(lastIndex, match.index));
79
- const [, bang, label, linkUrl, bareUrl, , boldText, starEm, underscoreEm] = match;
145
+ const [, bang, label, linkUrl, autoUrl, bareUrl, , boldText, starEm, underscoreEm] = match;
80
146
  if (linkUrl !== undefined) {
81
- if (bang)
147
+ if (!isSafeHref(linkUrl)) {
148
+ // Not an anchor, ever. It keeps the label so the bubble reads as the
149
+ // character wrote it rather than showing raw markdown, and the target
150
+ // so a renderer can reveal it on hover.
151
+ nodes.push({ kind: "blocked", href: linkUrl, label: bang ? label || linkUrl : label });
152
+ }
153
+ else if (bang) {
82
154
  nodes.push({ kind: "image", src: linkUrl, alt: label });
83
- else
155
+ }
156
+ else {
84
157
  nodes.push({ kind: "link", href: linkUrl, label, media: mediaKindForUrl(linkUrl) });
158
+ }
159
+ }
160
+ else if (autoUrl !== undefined) {
161
+ nodes.push({ kind: "link", href: autoUrl, label: autoUrl, media: mediaKindForUrl(autoUrl) });
85
162
  }
86
163
  else if (bareUrl !== undefined) {
87
164
  const [href, rest] = splitTrailingPunctuation(bareUrl);
@@ -1 +1 @@
1
- {"version":3,"file":"inlineMarkdown.js","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Inline markdown tokenizer shared by the React and vanilla chat bubbles.\n *\n * Core-service can hand the client bot text with links preserved\n * (`preserve_links_in_output` on /connect): `[text](url)`, `![alt](url)` and\n * bare `http(s)://` URLs arrive verbatim, everything else is still flattened\n * server-side. This turns one line of that text into nodes a renderer can\n * map onto DOM or JSX.\n *\n * Only `http:` and `https:` become links or images. Anything else — including\n * `javascript:` — stays literal text, regardless of what the server sent.\n * A link whose closing paren has not streamed in yet is left as text too, so\n * a bubble does not flicker between shapes mid-response.\n */\n\nexport type MediaKind = \"image\" | \"video\";\n\nexport type InlineNode =\n | { kind: \"text\"; text: string }\n | { kind: \"strong\"; text: string }\n | { kind: \"em\"; text: string }\n | { kind: \"link\"; href: string; label: string; media: MediaKind | null }\n | { kind: \"image\"; src: string; alt: string };\n\nconst IMAGE_EXT = /\\.(png|jpe?g|gif|webp|avif)$/i;\nconst VIDEO_EXT = /\\.(mp4|webm|mov|ogg)$/i;\n\n/** Classify a URL by its path extension; null for anything not previewable. */\nexport function mediaKindForUrl(url: string): MediaKind | null {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n return null;\n }\n if (IMAGE_EXT.test(pathname)) return \"image\";\n if (VIDEO_EXT.test(pathname)) return \"video\";\n return null;\n}\n\n// Alternatives are tried in this order at every position:\n// 1. ![alt](url) / [text](url) with an http(s) URL\n// 2. bare http(s) URL, unless it is the target of a link still streaming in\n// 3. **bold** / __bold__ / *em* / _em_\n// Matching URLs before emphasis is what keeps `_` inside a URL literal.\nconst TOKEN =\n /(!?)\\[([^\\]]*)\\]\\((https?:\\/\\/[^\\s)]+)\\)|(?<!\\]\\()(https?:\\/\\/[^\\s<>\"'`]+)|(\\*\\*|__)(.*?)\\5|\\*([^*]+)\\*|_([^_]+)_/g;\n\nconst TRAILING_PUNCTUATION = \".,;:!?'\\\"\";\n\n/**\n * Sentence punctuation after a bare URL belongs to the sentence, and an\n * unbalanced `)` belongs to the surrounding parenthetical.\n */\nfunction splitTrailingPunctuation(raw: string): [string, string] {\n let url = raw;\n while (url.length > 0) {\n const last = url[url.length - 1];\n if (TRAILING_PUNCTUATION.includes(last)) {\n url = url.slice(0, -1);\n continue;\n }\n if (last === \")\") {\n const opens = (url.match(/\\(/g) ?? []).length;\n const closes = (url.match(/\\)/g) ?? []).length;\n if (closes > opens) {\n url = url.slice(0, -1);\n continue;\n }\n }\n break;\n }\n return [url, raw.slice(url.length)];\n}\n\nexport function parseInlineMarkdown(line: string): InlineNode[] {\n const nodes: InlineNode[] = [];\n const pushText = (text: string) => {\n if (!text) return;\n const last = nodes[nodes.length - 1];\n if (last?.kind === \"text\") last.text += text;\n else nodes.push({ kind: \"text\", text });\n };\n\n let lastIndex = 0;\n TOKEN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN.exec(line)) !== null) {\n pushText(line.slice(lastIndex, match.index));\n const [, bang, label, linkUrl, bareUrl, , boldText, starEm, underscoreEm] = match;\n\n if (linkUrl !== undefined) {\n if (bang) nodes.push({ kind: \"image\", src: linkUrl, alt: label });\n else nodes.push({ kind: \"link\", href: linkUrl, label, media: mediaKindForUrl(linkUrl) });\n } else if (bareUrl !== undefined) {\n const [href, rest] = splitTrailingPunctuation(bareUrl);\n if (href) nodes.push({ kind: \"link\", href, label: href, media: mediaKindForUrl(href) });\n pushText(rest);\n } else if (boldText !== undefined) {\n nodes.push({ kind: \"strong\", text: boldText });\n } else if (starEm !== undefined) {\n nodes.push({ kind: \"em\", text: starEm });\n } else if (underscoreEm !== undefined) {\n nodes.push({ kind: \"em\", text: underscoreEm });\n }\n lastIndex = match.index + match[0].length;\n }\n pushText(line.slice(lastIndex));\n return nodes;\n}\n"]}
1
+ {"version":3,"file":"inlineMarkdown.js","sourceRoot":"","sources":["../../src/utils/inlineMarkdown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Inline markdown tokenizer shared by the React and vanilla chat bubbles.\n *\n * Core-service can hand the client bot text with links preserved\n * (`preserve_links_in_output` on /connect): `[text](url)`, `![alt](url)` and\n * bare `http(s)://` URLs arrive verbatim, everything else is still flattened\n * server-side. This turns one line of that text into nodes a renderer can\n * map onto DOM or JSX.\n *\n * Only `http:` and `https:` become links or images. Anything else — including\n * `javascript:` — stays literal text, regardless of what the server sent.\n * A link whose closing paren has not streamed in yet is left as text too, so\n * a bubble does not flicker between shapes mid-response.\n */\n\nexport type MediaKind = \"image\" | \"video\";\n\nexport type InlineNode =\n | { kind: \"text\"; text: string }\n | { kind: \"strong\"; text: string }\n | { kind: \"em\"; text: string }\n | { kind: \"link\"; href: string; label: string; media: MediaKind | null }\n | { kind: \"image\"; src: string; alt: string }\n /**\n * A `[label](target)` whose scheme is not on the allowlist. It carries the\n * label so a renderer can show the words the character wrote instead of raw\n * markdown, and the target so it can be surfaced on hover — but a renderer\n * must never put `href` on an anchor. That is the whole point of the node.\n */\n | { kind: \"blocked\"; href: string; label: string };\n\nconst IMAGE_EXT = /\\.(png|jpe?g|gif|webp|avif)$/i;\nconst VIDEO_EXT = /\\.(mp4|webm|mov|ogg)$/i;\n\n/** Classify a URL by its path extension; null for anything not previewable. */\nexport function mediaKindForUrl(url: string): MediaKind | null {\n let pathname: string;\n try {\n pathname = new URL(url).pathname;\n } catch {\n return null;\n }\n if (IMAGE_EXT.test(pathname)) return \"image\";\n if (VIDEO_EXT.test(pathname)) return \"video\";\n return null;\n}\n\n/**\n * Schemes a renderer may turn into an anchor.\n *\n * The list is an allowlist, never a denylist, because the server does NOT\n * sanitise schemes — core-service echoes `[x](javascript:alert(1))` back\n * verbatim (verified against dev4, 2026-09-10). The character's output is\n * attacker-influenced text, so this is the only thing standing between a\n * prompt-injected `javascript:` href and script execution in the host page.\n * Anything not listed here stays literal text.\n */\nconst SAFE_SCHEMES = [\"http\", \"https\", \"ftp\", \"ftps\", \"mailto\", \"tel\", \"sms\"];\n\n/** Schemes with an authority; the rest (mailto/tel/sms) are opaque. */\nconst NETWORK_SCHEMES = [\"http\", \"https\", \"ftp\", \"ftps\"];\n\nconst schemeAlt = SAFE_SCHEMES.join(\"|\");\nconst networkAlt = NETWORK_SCHEMES.join(\"|\");\n\n/**\n * A bare URL is its own label, and a long one wraps over several lines before\n * the message even starts — in a 400px panel that swamps the bubble. Compact it\n * the way a browser address bar does: keep what identifies the target, elide\n * the middle. `https://a.example.com/x/y/z/file.mp4` → `a.example.com/…/file.mp4`\n *\n * Presentation only: the href is always the full URL.\n */\nexport function compactUrl(url: string, max = 44): string {\n if (url.length <= max) return url;\n let host: string;\n let pathname: string;\n try {\n ({ host, pathname } = new URL(url));\n } catch {\n return `${url.slice(0, max - 1)}…`;\n }\n const file = pathname.split(\"/\").filter(Boolean).pop();\n if (!file) return host.length <= max ? host : `${host.slice(0, max - 1)}…`;\n const compact = `${host}/…/${file}`;\n if (compact.length <= max) return compact;\n // A long host would otherwise eat the budget and clip the filename down to\n // its extension. The filename identifies what the link actually is, so drop\n // the host instead of shortening it.\n if (file.length <= max) return file;\n return `…${file.slice(-(max - 1))}`;\n}\n\n/** True when `url` uses a scheme safe to expose as an anchor href. */\nexport function isSafeHref(url: string): boolean {\n const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1];\n return scheme !== undefined && SAFE_SCHEMES.includes(scheme.toLowerCase());\n}\n\n// Alternatives are tried in this order at every position:\n// 1. ![alt](url) / [text](url), with an optional CommonMark \"title\"\n// 2. <url> autolink, angle brackets consumed\n// 3. bare URL, unless it is the target of a link still streaming in\n// 4. **bold** / __bold__ / *em* / _em_\n// Matching URLs before emphasis is what keeps `_` inside a URL literal.\n// Alternative 1 accepts ANY scheme so an unsafe one can be recognised and\n// re-emitted as literal text; without that, `[x](javascript:…)` would fall\n// through to the emphasis rules and get partially mangled.\nconst TOKEN = new RegExp(\n [\n // The target allows one level of balanced parens, so `javascript:alert(1)`\n // and `…/a_(b).pdf` are consumed whole. Without it the inner `)` ended the\n // match and the outer one was left stranded in the text.\n `(!?)\\\\[([^\\\\]]*)\\\\]\\\\(\\\\s*((?:[^\\\\s()]|\\\\([^()]*\\\\))+)(?:\\\\s+\"[^\"]*\")?\\\\s*\\\\)`,\n `<((?:${schemeAlt}):[^\\\\s<>]+)>`,\n `(?<!\\\\]\\\\()((?:${networkAlt}):\\\\/\\\\/[^\\\\s<>\"'\\`]+|(?:mailto|tel|sms):[^\\\\s<>\"'\\`]+)`,\n `(\\\\*\\\\*|__)(.*?)\\\\6`,\n `\\\\*([^*]+)\\\\*`,\n `_([^_]+)_`,\n ].join(\"|\"),\n \"gi\",\n);\n\nconst TRAILING_PUNCTUATION = \".,;:!?'\\\"\";\n\n/**\n * Sentence punctuation after a bare URL belongs to the sentence, and an\n * unbalanced `)` belongs to the surrounding parenthetical.\n */\nfunction splitTrailingPunctuation(raw: string): [string, string] {\n let url = raw;\n while (url.length > 0) {\n const last = url[url.length - 1];\n if (TRAILING_PUNCTUATION.includes(last)) {\n url = url.slice(0, -1);\n continue;\n }\n if (last === \")\") {\n const opens = (url.match(/\\(/g) ?? []).length;\n const closes = (url.match(/\\)/g) ?? []).length;\n if (closes > opens) {\n url = url.slice(0, -1);\n continue;\n }\n }\n break;\n }\n return [url, raw.slice(url.length)];\n}\n\nexport function parseInlineMarkdown(line: string): InlineNode[] {\n const nodes: InlineNode[] = [];\n const pushText = (text: string) => {\n if (!text) return;\n const last = nodes[nodes.length - 1];\n if (last?.kind === \"text\") last.text += text;\n else nodes.push({ kind: \"text\", text });\n };\n\n let lastIndex = 0;\n TOKEN.lastIndex = 0;\n let match: RegExpExecArray | null;\n while ((match = TOKEN.exec(line)) !== null) {\n pushText(line.slice(lastIndex, match.index));\n const [, bang, label, linkUrl, autoUrl, bareUrl, , boldText, starEm, underscoreEm] = match;\n\n if (linkUrl !== undefined) {\n if (!isSafeHref(linkUrl)) {\n // Not an anchor, ever. It keeps the label so the bubble reads as the\n // character wrote it rather than showing raw markdown, and the target\n // so a renderer can reveal it on hover.\n nodes.push({ kind: \"blocked\", href: linkUrl, label: bang ? label || linkUrl : label });\n } else if (bang) {\n nodes.push({ kind: \"image\", src: linkUrl, alt: label });\n } else {\n nodes.push({ kind: \"link\", href: linkUrl, label, media: mediaKindForUrl(linkUrl) });\n }\n } else if (autoUrl !== undefined) {\n nodes.push({ kind: \"link\", href: autoUrl, label: autoUrl, media: mediaKindForUrl(autoUrl) });\n } else if (bareUrl !== undefined) {\n const [href, rest] = splitTrailingPunctuation(bareUrl);\n if (href) nodes.push({ kind: \"link\", href, label: href, media: mediaKindForUrl(href) });\n pushText(rest);\n } else if (boldText !== undefined) {\n nodes.push({ kind: \"strong\", text: boldText });\n } else if (starEm !== undefined) {\n nodes.push({ kind: \"em\", text: starEm });\n } else if (underscoreEm !== undefined) {\n nodes.push({ kind: \"em\", text: underscoreEm });\n }\n lastIndex = match.index + match[0].length;\n }\n pushText(line.slice(lastIndex));\n return nodes;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ConvaiWidget.d.ts","sourceRoot":"","sources":["../../src/vanilla/ConvaiWidget.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EAEd,MAAM,SAAS,CAAC;AA4HjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CA2wFf;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAE/D"}
1
+ {"version":3,"file":"ConvaiWidget.d.ts","sourceRoot":"","sources":["../../src/vanilla/ConvaiWidget.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EAEd,MAAM,SAAS,CAAC;AA4HjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,WAAW,EACtB,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAk5Ff;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAE/D"}