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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +333 -13
  2. package/dist/core/CharacterRoster.d.ts +84 -0
  3. package/dist/core/CharacterRoster.d.ts.map +1 -0
  4. package/dist/core/CharacterRoster.js +348 -0
  5. package/dist/core/CharacterRoster.js.map +1 -0
  6. package/dist/core/ConvaiClient.d.ts +152 -2
  7. package/dist/core/ConvaiClient.d.ts.map +1 -1
  8. package/dist/core/ConvaiClient.js +1036 -49
  9. package/dist/core/ConvaiClient.js.map +1 -1
  10. package/dist/core/ConvaiRoomError.d.ts +48 -0
  11. package/dist/core/ConvaiRoomError.d.ts.map +1 -0
  12. package/dist/core/ConvaiRoomError.js +111 -0
  13. package/dist/core/ConvaiRoomError.js.map +1 -0
  14. package/dist/core/MessageHandler.d.ts +25 -0
  15. package/dist/core/MessageHandler.d.ts.map +1 -1
  16. package/dist/core/MessageHandler.js +431 -30
  17. package/dist/core/MessageHandler.js.map +1 -1
  18. package/dist/core/SSESession.d.ts +15 -0
  19. package/dist/core/SSESession.d.ts.map +1 -1
  20. package/dist/core/SSESession.js +92 -3
  21. package/dist/core/SSESession.js.map +1 -1
  22. package/dist/core/connectRequest.d.ts +38 -2
  23. package/dist/core/connectRequest.d.ts.map +1 -1
  24. package/dist/core/connectRequest.js +57 -0
  25. package/dist/core/connectRequest.js.map +1 -1
  26. package/dist/core/index.d.ts +6 -1
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +5 -1
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/logicalTurn.d.ts +4 -1
  31. package/dist/core/logicalTurn.d.ts.map +1 -1
  32. package/dist/core/logicalTurn.js +35 -4
  33. package/dist/core/logicalTurn.js.map +1 -1
  34. package/dist/core/modelOutput.d.ts +40 -0
  35. package/dist/core/modelOutput.d.ts.map +1 -0
  36. package/dist/core/modelOutput.js +187 -0
  37. package/dist/core/modelOutput.js.map +1 -0
  38. package/dist/core/publishedChat.d.ts +9 -0
  39. package/dist/core/publishedChat.d.ts.map +1 -0
  40. package/dist/core/publishedChat.js +13 -0
  41. package/dist/core/publishedChat.js.map +1 -0
  42. package/dist/core/rosterRequest.d.ts +57 -0
  43. package/dist/core/rosterRequest.d.ts.map +1 -0
  44. package/dist/core/rosterRequest.js +210 -0
  45. package/dist/core/rosterRequest.js.map +1 -0
  46. package/dist/core/types.d.ts +667 -10
  47. package/dist/core/types.d.ts.map +1 -1
  48. package/dist/core/types.js.map +1 -1
  49. package/dist/embed/ConvaiChatElement.d.ts +27 -0
  50. package/dist/embed/ConvaiChatElement.d.ts.map +1 -0
  51. package/dist/embed/ConvaiChatElement.js +300 -0
  52. package/dist/embed/ConvaiChatElement.js.map +1 -0
  53. package/dist/embed/browser.d.ts +3 -0
  54. package/dist/embed/browser.d.ts.map +1 -0
  55. package/dist/embed/browser.js +5 -0
  56. package/dist/embed/browser.js.map +1 -0
  57. package/dist/embed/chat-embed-v1.js +100 -0
  58. package/dist/embed/chat-embed-v1.js.map +7 -0
  59. package/dist/embed/index.d.ts +9 -0
  60. package/dist/embed/index.d.ts.map +1 -0
  61. package/dist/embed/index.js +9 -0
  62. package/dist/embed/index.js.map +1 -0
  63. package/dist/embed/publicationApi.d.ts +23 -0
  64. package/dist/embed/publicationApi.d.ts.map +1 -0
  65. package/dist/embed/publicationApi.js +141 -0
  66. package/dist/embed/publicationApi.js.map +1 -0
  67. package/dist/react/components/ConvaiWidget.d.ts +11 -1
  68. package/dist/react/components/ConvaiWidget.d.ts.map +1 -1
  69. package/dist/react/components/ConvaiWidget.js +67 -11
  70. package/dist/react/components/ConvaiWidget.js.map +1 -1
  71. package/dist/react/components/rtc-widget/components/MarkdownRenderer.d.ts.map +1 -1
  72. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js +39 -44
  73. package/dist/react/components/rtc-widget/components/MarkdownRenderer.js.map +1 -1
  74. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts +1 -1
  75. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.d.ts.map +1 -1
  76. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js +2 -2
  77. package/dist/react/components/rtc-widget/components/conviComponents/SettingsTray.js.map +1 -1
  78. package/dist/react/hooks/useConvaiClient.d.ts +2 -0
  79. package/dist/react/hooks/useConvaiClient.d.ts.map +1 -1
  80. package/dist/react/hooks/useConvaiClient.js +16 -0
  81. package/dist/react/hooks/useConvaiClient.js.map +1 -1
  82. package/dist/react/index.d.ts +1 -1
  83. package/dist/react/index.d.ts.map +1 -1
  84. package/dist/react/index.js +1 -1
  85. package/dist/react/index.js.map +1 -1
  86. package/dist/utils/inlineMarkdown.d.ts +38 -0
  87. package/dist/utils/inlineMarkdown.d.ts.map +1 -0
  88. package/dist/utils/inlineMarkdown.js +106 -0
  89. package/dist/utils/inlineMarkdown.js.map +1 -0
  90. package/dist/vanilla/AudioRenderer.d.ts.map +1 -1
  91. package/dist/vanilla/AudioRenderer.js +6 -0
  92. package/dist/vanilla/AudioRenderer.js.map +1 -1
  93. package/dist/vanilla/ConvaiWidget.d.ts.map +1 -1
  94. package/dist/vanilla/ConvaiWidget.js +224 -89
  95. package/dist/vanilla/ConvaiWidget.js.map +1 -1
  96. package/dist/vanilla/WidgetEventSubscriptions.d.ts +15 -0
  97. package/dist/vanilla/WidgetEventSubscriptions.d.ts.map +1 -0
  98. package/dist/vanilla/WidgetEventSubscriptions.js +37 -0
  99. package/dist/vanilla/WidgetEventSubscriptions.js.map +1 -0
  100. package/dist/vanilla/index.d.ts +2 -1
  101. package/dist/vanilla/index.d.ts.map +1 -1
  102. package/dist/vanilla/index.js +2 -1
  103. package/dist/vanilla/index.js.map +1 -1
  104. package/dist/vanilla/types.d.ts +8 -1
  105. package/dist/vanilla/types.d.ts.map +1 -1
  106. package/dist/vanilla/types.js.map +1 -1
  107. package/dist/version.d.ts +1 -1
  108. package/dist/version.js +1 -1
  109. package/dist/version.js.map +1 -1
  110. package/package.json +19 -6
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Published-chat descriptor and grant helpers. Server callers must provide a
3
+ * caller-scoped requester ID when minting a grant.
4
+ *
5
+ * Import `@convai/web-sdk/embed/browser` in a browser to register the
6
+ * `<convai-chat>` custom element.
7
+ */
8
+ export * from "./publicationApi.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/embed/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Published-chat descriptor and grant helpers. Server callers must provide a
3
+ * caller-scoped requester ID when minting a grant.
4
+ *
5
+ * Import `@convai/web-sdk/embed/browser` in a browser to register the
6
+ * `<convai-chat>` custom element.
7
+ */
8
+ export * from "./publicationApi.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/embed/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,qBAAqB,CAAC","sourcesContent":["/**\n * Published-chat descriptor and grant helpers. Server callers must provide a\n * caller-scoped requester ID when minting a grant.\n *\n * Import `@convai/web-sdk/embed/browser` in a browser to register the\n * `<convai-chat>` custom element.\n */\nexport * from \"./publicationApi.js\";\n"]}
@@ -0,0 +1,23 @@
1
+ export interface PublishedChatDescriptor {
2
+ publication_id: string;
3
+ display_name: string;
4
+ description: string | null;
5
+ image_url: string | null;
6
+ mode: "text";
7
+ }
8
+ export type PublishedChatFetch = typeof fetch;
9
+ export declare const DEFAULT_PUBLICATION_API_URL = "https://api.convai.com";
10
+ export interface PublishedChatEndpoints {
11
+ descriptorUrl: URL;
12
+ launchGrantUrl: URL;
13
+ }
14
+ export interface PublishedChatSession {
15
+ descriptor: PublishedChatDescriptor;
16
+ launchToken: string;
17
+ }
18
+ export declare function parsePublishedChatUrl(value: string, name: string): URL;
19
+ export declare function getPublishedChatEndpoints(publicationId: string, publicationApiUrl?: string): PublishedChatEndpoints;
20
+ export declare function fetchPublishedChatDescriptor(descriptorUrl: string, fetchImpl?: PublishedChatFetch, signal?: AbortSignal): Promise<PublishedChatDescriptor>;
21
+ export declare function requestPublishedChatGrant(launchGrantUrl: string, fetchImpl?: PublishedChatFetch, signal?: AbortSignal, requesterId?: string): Promise<string>;
22
+ export declare function loadPublishedChatSession(publicationId: string, publicationApiUrl?: string, fetchImpl?: PublishedChatFetch, signal?: AbortSignal, requesterId?: string): Promise<PublishedChatSession>;
23
+ //# sourceMappingURL=publicationApi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicationApi.d.ts","sourceRoot":"","sources":["../../src/embed/publicationApi.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,KAAK,CAAC;AAW9C,eAAO,MAAM,2BAA2B,2BAA2B,CAAC;AAEpE,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,GAAG,CAAC;IACnB,cAAc,EAAE,GAAG,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,UAAU,EAAE,uBAAuB,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;CACrB;AA2CD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CActE;AAED,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,MAAM,EACrB,iBAAiB,SAA8B,GAC9C,sBAAsB,CAYxB;AAUD,wBAAsB,4BAA4B,CAChD,aAAa,EAAE,MAAM,EACrB,SAAS,GAAE,kBAA0B,EACrC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAyBlC;AAED,wBAAsB,yBAAyB,CAC7C,cAAc,EAAE,MAAM,EACtB,SAAS,GAAE,kBAA0B,EACrC,MAAM,CAAC,EAAE,WAAW,EACpB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAED,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,iBAAiB,CAAC,EAAE,MAAM,EAC1B,SAAS,GAAE,kBAA0B,EACrC,MAAM,CAAC,EAAE,WAAW,EACpB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAoB/B"}
@@ -0,0 +1,141 @@
1
+ const LAUNCH_TOKEN_MIN_LENGTH = 32;
2
+ const LAUNCH_TOKEN_MAX_LENGTH = 512;
3
+ const PUBLICATION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+ const PUBLICATION_CLIENT_ID_RE = /^[A-Za-z0-9_-]{16,128}$/;
5
+ const PUBLICATION_CLIENT_ID_STORAGE_KEY = "convai.published-chat-client-id.v1";
6
+ let inMemoryPublicationClientId;
7
+ export const DEFAULT_PUBLICATION_API_URL = "https://api.convai.com";
8
+ function newPublicationClientId() {
9
+ const clientId = globalThis.crypto?.randomUUID?.();
10
+ if (!clientId) {
11
+ throw new Error("Published chat requires a secure browser random source");
12
+ }
13
+ return clientId;
14
+ }
15
+ function publicationClientId() {
16
+ if (inMemoryPublicationClientId)
17
+ return inMemoryPublicationClientId;
18
+ try {
19
+ const stored = globalThis.localStorage?.getItem(PUBLICATION_CLIENT_ID_STORAGE_KEY);
20
+ if (stored && PUBLICATION_CLIENT_ID_RE.test(stored)) {
21
+ inMemoryPublicationClientId = stored;
22
+ return stored;
23
+ }
24
+ }
25
+ catch {
26
+ // Sandboxed embeds can block storage. Keep a per-page random fallback.
27
+ }
28
+ const generated = newPublicationClientId();
29
+ inMemoryPublicationClientId = generated;
30
+ try {
31
+ globalThis.localStorage?.setItem(PUBLICATION_CLIENT_ID_STORAGE_KEY, generated);
32
+ }
33
+ catch {
34
+ // The in-memory value still scopes retries in this embedded page.
35
+ }
36
+ return generated;
37
+ }
38
+ function resolvePublicationClientId(requesterId) {
39
+ if (requesterId)
40
+ return requesterId;
41
+ if (typeof window === "undefined") {
42
+ throw new Error("Server callers must provide a caller-scoped published chat requester ID");
43
+ }
44
+ return publicationClientId();
45
+ }
46
+ export function parsePublishedChatUrl(value, name) {
47
+ let url;
48
+ try {
49
+ url = new URL(value);
50
+ }
51
+ catch {
52
+ throw new Error(`${name} must be an absolute URL`);
53
+ }
54
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
55
+ throw new Error(`${name} must use HTTP or HTTPS`);
56
+ }
57
+ if (url.username || url.password) {
58
+ throw new Error(`${name} must not include credentials`);
59
+ }
60
+ return url;
61
+ }
62
+ export function getPublishedChatEndpoints(publicationId, publicationApiUrl = DEFAULT_PUBLICATION_API_URL) {
63
+ if (!PUBLICATION_ID_RE.test(publicationId)) {
64
+ throw new Error("publication-id must be a UUID");
65
+ }
66
+ const apiUrl = parsePublishedChatUrl(publicationApiUrl, "publication-api-url");
67
+ apiUrl.pathname = `${apiUrl.pathname.replace(/\/+$/, "")}/`;
68
+ const publicationPath = `chat-publications/${encodeURIComponent(publicationId)}`;
69
+ const descriptorUrl = new URL(publicationPath, apiUrl);
70
+ return {
71
+ descriptorUrl,
72
+ launchGrantUrl: new URL(`${publicationPath}/launch-grants`, apiUrl),
73
+ };
74
+ }
75
+ async function errorMessage(response, fallback) {
76
+ const payload = await response.json().catch(() => null);
77
+ const detail = payload?.detail ?? payload?.message ?? payload?.error;
78
+ return typeof detail === "string" && detail.trim()
79
+ ? detail.trim().slice(0, 240)
80
+ : fallback;
81
+ }
82
+ export async function fetchPublishedChatDescriptor(descriptorUrl, fetchImpl = fetch, signal) {
83
+ const url = parsePublishedChatUrl(descriptorUrl, "descriptor-url");
84
+ const response = await fetchImpl(url, {
85
+ method: "GET",
86
+ cache: "no-store",
87
+ credentials: "omit",
88
+ headers: { Accept: "application/json" },
89
+ signal,
90
+ });
91
+ if (!response.ok) {
92
+ throw new Error(await errorMessage(response, `Unable to load published chat (${response.status})`));
93
+ }
94
+ const payload = await response.json();
95
+ if (typeof payload?.publication_id !== "string" ||
96
+ typeof payload?.display_name !== "string" ||
97
+ (typeof payload?.description !== "string" && payload?.description !== null) ||
98
+ (typeof payload?.image_url !== "string" && payload?.image_url !== null) ||
99
+ payload?.mode !== "text") {
100
+ throw new Error("Published chat returned an invalid descriptor");
101
+ }
102
+ return payload;
103
+ }
104
+ export async function requestPublishedChatGrant(launchGrantUrl, fetchImpl = fetch, signal, requesterId) {
105
+ const effectiveRequesterId = resolvePublicationClientId(requesterId);
106
+ if (!PUBLICATION_CLIENT_ID_RE.test(effectiveRequesterId)) {
107
+ throw new Error("Published chat browser identifier is invalid");
108
+ }
109
+ const url = parsePublishedChatUrl(launchGrantUrl, "launch-grant-url");
110
+ const response = await fetchImpl(url, {
111
+ method: "POST",
112
+ cache: "no-store",
113
+ credentials: "omit",
114
+ headers: {
115
+ Accept: "application/json",
116
+ "X-Convai-Publication-Client": effectiveRequesterId,
117
+ },
118
+ signal,
119
+ });
120
+ if (!response.ok) {
121
+ throw new Error(await errorMessage(response, `Unable to start published chat (${response.status})`));
122
+ }
123
+ const payload = await response.json();
124
+ const launchToken = payload?.launch_token;
125
+ if (typeof launchToken !== "string" ||
126
+ launchToken.length < LAUNCH_TOKEN_MIN_LENGTH ||
127
+ launchToken.length > LAUNCH_TOKEN_MAX_LENGTH) {
128
+ throw new Error("Published chat returned an invalid launch grant");
129
+ }
130
+ return launchToken;
131
+ }
132
+ export async function loadPublishedChatSession(publicationId, publicationApiUrl, fetchImpl = fetch, signal, requesterId) {
133
+ const { descriptorUrl, launchGrantUrl } = getPublishedChatEndpoints(publicationId, publicationApiUrl);
134
+ const descriptor = await fetchPublishedChatDescriptor(descriptorUrl.toString(), fetchImpl, signal);
135
+ if (descriptor.publication_id.toLowerCase() !== publicationId.toLowerCase()) {
136
+ throw new Error("Published chat descriptor does not match publication-id");
137
+ }
138
+ const launchToken = await requestPublishedChatGrant(launchGrantUrl.toString(), fetchImpl, signal, requesterId);
139
+ return { descriptor, launchToken };
140
+ }
141
+ //# sourceMappingURL=publicationApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publicationApi.js","sourceRoot":"","sources":["../../src/embed/publicationApi.ts"],"names":[],"mappings":"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","sourcesContent":["export interface PublishedChatDescriptor {\n publication_id: string;\n display_name: string;\n description: string | null;\n image_url: string | null;\n mode: \"text\";\n}\n\nexport type PublishedChatFetch = typeof fetch;\n\nconst LAUNCH_TOKEN_MIN_LENGTH = 32;\nconst LAUNCH_TOKEN_MAX_LENGTH = 512;\nconst PUBLICATION_ID_RE =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\nconst PUBLICATION_CLIENT_ID_RE = /^[A-Za-z0-9_-]{16,128}$/;\nconst PUBLICATION_CLIENT_ID_STORAGE_KEY = \"convai.published-chat-client-id.v1\";\n\nlet inMemoryPublicationClientId: string | undefined;\n\nexport const DEFAULT_PUBLICATION_API_URL = \"https://api.convai.com\";\n\nexport interface PublishedChatEndpoints {\n descriptorUrl: URL;\n launchGrantUrl: URL;\n}\n\nexport interface PublishedChatSession {\n descriptor: PublishedChatDescriptor;\n launchToken: string;\n}\n\nfunction newPublicationClientId(): string {\n const clientId = globalThis.crypto?.randomUUID?.();\n if (!clientId) {\n throw new Error(\"Published chat requires a secure browser random source\");\n }\n return clientId;\n}\n\nfunction publicationClientId(): string {\n if (inMemoryPublicationClientId) return inMemoryPublicationClientId;\n\n try {\n const stored = globalThis.localStorage?.getItem(PUBLICATION_CLIENT_ID_STORAGE_KEY);\n if (stored && PUBLICATION_CLIENT_ID_RE.test(stored)) {\n inMemoryPublicationClientId = stored;\n return stored;\n }\n } catch {\n // Sandboxed embeds can block storage. Keep a per-page random fallback.\n }\n\n const generated = newPublicationClientId();\n inMemoryPublicationClientId = generated;\n try {\n globalThis.localStorage?.setItem(PUBLICATION_CLIENT_ID_STORAGE_KEY, generated);\n } catch {\n // The in-memory value still scopes retries in this embedded page.\n }\n return generated;\n}\n\nfunction resolvePublicationClientId(requesterId?: string): string {\n if (requesterId) return requesterId;\n if (typeof window === \"undefined\") {\n throw new Error(\n \"Server callers must provide a caller-scoped published chat requester ID\",\n );\n }\n return publicationClientId();\n}\n\nexport function parsePublishedChatUrl(value: string, name: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error(`${name} must be an absolute URL`);\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(`${name} must use HTTP or HTTPS`);\n }\n if (url.username || url.password) {\n throw new Error(`${name} must not include credentials`);\n }\n return url;\n}\n\nexport function getPublishedChatEndpoints(\n publicationId: string,\n publicationApiUrl = DEFAULT_PUBLICATION_API_URL,\n): PublishedChatEndpoints {\n if (!PUBLICATION_ID_RE.test(publicationId)) {\n throw new Error(\"publication-id must be a UUID\");\n }\n const apiUrl = parsePublishedChatUrl(publicationApiUrl, \"publication-api-url\");\n apiUrl.pathname = `${apiUrl.pathname.replace(/\\/+$/, \"\")}/`;\n const publicationPath = `chat-publications/${encodeURIComponent(publicationId)}`;\n const descriptorUrl = new URL(publicationPath, apiUrl);\n return {\n descriptorUrl,\n launchGrantUrl: new URL(`${publicationPath}/launch-grants`, apiUrl),\n };\n}\n\nasync function errorMessage(response: Response, fallback: string): Promise<string> {\n const payload = await response.json().catch(() => null);\n const detail = payload?.detail ?? payload?.message ?? payload?.error;\n return typeof detail === \"string\" && detail.trim()\n ? detail.trim().slice(0, 240)\n : fallback;\n}\n\nexport async function fetchPublishedChatDescriptor(\n descriptorUrl: string,\n fetchImpl: PublishedChatFetch = fetch,\n signal?: AbortSignal,\n): Promise<PublishedChatDescriptor> {\n const url = parsePublishedChatUrl(descriptorUrl, \"descriptor-url\");\n const response = await fetchImpl(url, {\n method: \"GET\",\n cache: \"no-store\",\n credentials: \"omit\",\n headers: { Accept: \"application/json\" },\n signal,\n });\n if (!response.ok) {\n throw new Error(\n await errorMessage(response, `Unable to load published chat (${response.status})`),\n );\n }\n const payload = await response.json();\n if (\n typeof payload?.publication_id !== \"string\" ||\n typeof payload?.display_name !== \"string\" ||\n (typeof payload?.description !== \"string\" && payload?.description !== null) ||\n (typeof payload?.image_url !== \"string\" && payload?.image_url !== null) ||\n payload?.mode !== \"text\"\n ) {\n throw new Error(\"Published chat returned an invalid descriptor\");\n }\n return payload as PublishedChatDescriptor;\n}\n\nexport async function requestPublishedChatGrant(\n launchGrantUrl: string,\n fetchImpl: PublishedChatFetch = fetch,\n signal?: AbortSignal,\n requesterId?: string,\n): Promise<string> {\n const effectiveRequesterId = resolvePublicationClientId(requesterId);\n if (!PUBLICATION_CLIENT_ID_RE.test(effectiveRequesterId)) {\n throw new Error(\"Published chat browser identifier is invalid\");\n }\n const url = parsePublishedChatUrl(launchGrantUrl, \"launch-grant-url\");\n const response = await fetchImpl(url, {\n method: \"POST\",\n cache: \"no-store\",\n credentials: \"omit\",\n headers: {\n Accept: \"application/json\",\n \"X-Convai-Publication-Client\": effectiveRequesterId,\n },\n signal,\n });\n if (!response.ok) {\n throw new Error(\n await errorMessage(response, `Unable to start published chat (${response.status})`),\n );\n }\n const payload = await response.json();\n const launchToken = payload?.launch_token;\n if (\n typeof launchToken !== \"string\" ||\n launchToken.length < LAUNCH_TOKEN_MIN_LENGTH ||\n launchToken.length > LAUNCH_TOKEN_MAX_LENGTH\n ) {\n throw new Error(\"Published chat returned an invalid launch grant\");\n }\n return launchToken;\n}\n\nexport async function loadPublishedChatSession(\n publicationId: string,\n publicationApiUrl?: string,\n fetchImpl: PublishedChatFetch = fetch,\n signal?: AbortSignal,\n requesterId?: string,\n): Promise<PublishedChatSession> {\n const { descriptorUrl, launchGrantUrl } = getPublishedChatEndpoints(\n publicationId,\n publicationApiUrl,\n );\n const descriptor = await fetchPublishedChatDescriptor(\n descriptorUrl.toString(),\n fetchImpl,\n signal,\n );\n if (descriptor.publication_id.toLowerCase() !== publicationId.toLowerCase()) {\n throw new Error(\"Published chat descriptor does not match publication-id\");\n }\n const launchToken = await requestPublishedChatGrant(\n launchGrantUrl.toString(),\n fetchImpl,\n signal,\n requesterId,\n );\n return { descriptor, launchToken };\n}\n"]}
@@ -1,4 +1,5 @@
1
- import type { IConvaiClient } from "../../core/types.js";
1
+ import type { IConvaiClient, PublishedChatConnectOptions } from "../../core/types.js";
2
+ import type { PublishedChatDescriptor } from "../../embed/publicationApi.js";
2
3
  interface ConvaiWidgetProps {
3
4
  /** Convai client instance (required) */
4
5
  convaiClient: IConvaiClient & {
@@ -22,6 +23,15 @@ interface ConvaiWidgetProps {
22
23
  * When true, the widget will open directly in voice mode with microphone enabled.
23
24
  */
24
25
  defaultVoiceMode?: boolean;
26
+ /**
27
+ * Mint a fresh single-use grant when a published-chat session is reopened.
28
+ * Required when using this widget with connectWithPublicationGrant().
29
+ */
30
+ requestPublicationGrant?: () => Promise<string>;
31
+ /** Core endpoint override used with requestPublicationGrant. */
32
+ publishedChatConnectOptions?: PublishedChatConnectOptions;
33
+ /** Public descriptor used to label a credential-free published session. */
34
+ publishedChatDescriptor?: Pick<PublishedChatDescriptor, "display_name" | "image_url">;
25
35
  }
26
36
  /**
27
37
  * ConvaiWidget - A consolidated chat widget component for real-time conversations
@@ -1 +1 @@
1
- {"version":3,"file":"ConvaiWidget.d.ts","sourceRoot":"","sources":["../../../src/react/components/ConvaiWidget.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AA0CtD,UAAU,iBAAiB;IACzB,wCAAwC;IACxC,YAAY,EAAE,aAAa,GAAG;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,CAAC;IACF;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA21BpD,CAAC"}
1
+ {"version":3,"file":"ConvaiWidget.d.ts","sourceRoot":"","sources":["../../../src/react/components/ConvaiWidget.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,aAAa,EACb,2BAA2B,EAC5B,MAAM,kBAAkB,CAAC;AAI1B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAwC1E,UAAU,iBAAiB;IACzB,wCAAwC;IACxC,YAAY,EAAE,aAAa,GAAG;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,CAAC;IACF;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,gEAAgE;IAChE,2BAA2B,CAAC,EAAE,2BAA2B,CAAC;IAC1D,2EAA2E;IAC3E,uBAAuB,CAAC,EAAE,IAAI,CAC5B,uBAAuB,EACvB,cAAc,GAAG,WAAW,CAC7B,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA46BpD,CAAC"}
@@ -7,6 +7,7 @@ import { PiWaveform } from "react-icons/pi";
7
7
  import { RoomContext } from "@livekit/components-react";
8
8
  import { useCharacterInfo } from "../hooks/useCharacterInfo.js";
9
9
  import { AudioRenderer } from "../../vanilla/AudioRenderer.js";
10
+ import { freshPublicationGrantOptions } from "../../core/publishedChat.js";
10
11
  // Import styled components and theme from rtc-widget
11
12
  import { ConvaiContainer, MorphingContainer, ButtonContent, Logo, ChatContent, Header, Title, CloseButton, SettingsButton, Content, Footer, InputContainer, Input, ActionButton, aeroTheme, iosTransitions, ConvaiLogo, ChevronDown, } from "./rtc-widget/index.js";
12
13
  // Import message components
@@ -51,20 +52,36 @@ import { MessageList, SettingsTray, VoiceModeOverlay, FloatingVideo, } from "./r
51
52
  * }
52
53
  * ```
53
54
  */
54
- export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare = true, defaultVoiceMode = true, }) => {
55
+ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare = true, defaultVoiceMode = true, requestPublicationGrant, publishedChatConnectOptions, publishedChatDescriptor, }) => {
56
+ const isTextOnly = Boolean(requestPublicationGrant) || convaiClient.connectionType === "text";
55
57
  // Fetch character information dynamically from client's stored values
56
- const { name: characterName, image: characterImage, isLoading: isCharacterLoading, } = useCharacterInfo(convaiClient.characterId || undefined, convaiClient.apiKey || undefined, convaiClient.authToken || undefined);
58
+ const { name: fetchedCharacterName, image: fetchedCharacterImage, isLoading: isFetchedCharacterLoading, } = useCharacterInfo(convaiClient.characterId || undefined, convaiClient.apiKey || undefined, convaiClient.authToken || undefined);
59
+ const characterName = publishedChatDescriptor?.display_name || fetchedCharacterName;
60
+ const characterImage = publishedChatDescriptor?.image_url ?? fetchedCharacterImage;
61
+ const isCharacterLoading = publishedChatDescriptor
62
+ ? false
63
+ : isFetchedCharacterLoading;
57
64
  // UI State
58
65
  const [isOpen, setIsOpen] = useState(false);
59
66
  const [inputValue, setInputValue] = useState("");
60
67
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
61
- const [isVoiceMode, setIsVoiceMode] = useState(defaultVoiceMode);
68
+ const [isVoiceMode, setIsVoiceMode] = useState(defaultVoiceMode && !isTextOnly);
62
69
  const [isMuted, setIsMuted] = useState(false); // Character audio starts unmuted
63
70
  const [isVideoVisible, setIsVideoVisible] = useState(false);
64
71
  const [hasEnteredDefaultVoiceMode, setHasEnteredDefaultVoiceMode] = useState(false);
65
72
  const settingsButtonRef = useRef(null);
66
73
  /** True after we have connected at least once; used to call reconnect() instead of connect() when opening after disconnect */
67
74
  const hasConnectedBeforeRef = useRef(false);
75
+ /** Prevent repeated open clicks from minting multiple one-time publication grants. */
76
+ const isStartingConnectionRef = useRef(false);
77
+ /** Avoid opening a room after this widget has been removed from the tree. */
78
+ const isUnmountedRef = useRef(false);
79
+ // Only published sessions wait for Core's bot-ready handshake. A normal
80
+ // authenticated text session still uses the text-only layout, but can send
81
+ // as soon as it connects.
82
+ const requiresBotReadyForText = Boolean(requestPublicationGrant) || convaiClient.isPublishedChatSession;
83
+ const canSendText = convaiClient.state.isConnected &&
84
+ (!requiresBotReadyForText || convaiClient.isBotReady);
68
85
  // Character info from API (using client's stored values)
69
86
  const selectedCharacter = {
70
87
  id: convaiClient.characterId || "unknown",
@@ -108,21 +125,44 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
108
125
  };
109
126
  // Event Handlers
110
127
  const handleToggle = async () => {
128
+ if (isStartingConnectionRef.current) {
129
+ return;
130
+ }
111
131
  // Connect on first click if not already connected/connecting
112
132
  if (!convaiClient.state.isConnected && !convaiClient.state.isConnecting) {
133
+ isStartingConnectionRef.current = true;
113
134
  try {
114
- // Use reconnect when opening again after a disconnect (reuses stored config)
115
- if (hasConnectedBeforeRef.current && typeof convaiClient.reconnect === "function") {
135
+ if (requestPublicationGrant || convaiClient.isPublishedChatSession) {
136
+ if (!requestPublicationGrant) {
137
+ throw new Error("Published chat widgets require requestPublicationGrant to start a new session");
138
+ }
139
+ if (!convaiClient.connectWithPublicationGrant) {
140
+ throw new Error("This Convai client does not support published chat connections");
141
+ }
142
+ const launchToken = await requestPublicationGrant();
143
+ if (isUnmountedRef.current)
144
+ return;
145
+ await convaiClient.connectWithPublicationGrant(launchToken, freshPublicationGrantOptions(publishedChatConnectOptions));
146
+ }
147
+ else if (hasConnectedBeforeRef.current &&
148
+ typeof convaiClient.reconnect === "function") {
116
149
  await convaiClient.reconnect();
117
150
  }
118
151
  else {
119
152
  await convaiClient.connect();
120
153
  }
154
+ if (isUnmountedRef.current) {
155
+ await convaiClient.disconnect();
156
+ return;
157
+ }
121
158
  setIsOpen(true); // Open widget after successful connection
122
159
  }
123
160
  catch (error) {
124
161
  console.error("Failed to connect:", error);
125
162
  }
163
+ finally {
164
+ isStartingConnectionRef.current = false;
165
+ }
126
166
  }
127
167
  else {
128
168
  // Just toggle open/close if already connected
@@ -136,7 +176,7 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
136
176
  setInputValue(e.target.value);
137
177
  };
138
178
  const handleSend = () => {
139
- if (inputValue.trim() && convaiClient.state.isConnected) {
179
+ if (inputValue.trim() && canSendText) {
140
180
  convaiClient.sendUserTextMessage(inputValue);
141
181
  setInputValue("");
142
182
  }
@@ -209,6 +249,12 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
209
249
  const handleSettingsToggle = () => {
210
250
  setIsSettingsOpen(!isSettingsOpen);
211
251
  };
252
+ useEffect(() => {
253
+ isUnmountedRef.current = false;
254
+ return () => {
255
+ isUnmountedRef.current = true;
256
+ };
257
+ }, []);
212
258
  // Remember that we have connected at least once (so we use reconnect() next time after disconnect)
213
259
  useEffect(() => {
214
260
  if (convaiClient.state.isConnected) {
@@ -232,6 +278,11 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
232
278
  setIsVideoVisible(convaiClient.isVideoEnabled);
233
279
  }, [convaiClient.isVideoEnabled]);
234
280
  // Ensure microphone is muted when not in voice mode
281
+ useEffect(() => {
282
+ if (isTextOnly && isVoiceMode) {
283
+ setIsVoiceMode(false);
284
+ }
285
+ }, [isTextOnly, isVoiceMode]);
235
286
  useEffect(() => {
236
287
  const ensureMicState = async () => {
237
288
  if (!isVoiceMode && !convaiClient.isAudioMuted) {
@@ -245,6 +296,7 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
245
296
  useEffect(() => {
246
297
  if (isOpen &&
247
298
  defaultVoiceMode &&
299
+ !isTextOnly &&
248
300
  convaiClient.state.isConnected &&
249
301
  !hasEnteredDefaultVoiceMode &&
250
302
  !isVoiceMode) {
@@ -260,7 +312,7 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
260
312
  };
261
313
  enterVoiceMode();
262
314
  }
263
- }, [isOpen, defaultVoiceMode, convaiClient.state.isConnected, hasEnteredDefaultVoiceMode, isVoiceMode, convaiClient]);
315
+ }, [isOpen, defaultVoiceMode, isTextOnly, convaiClient.state.isConnected, hasEnteredDefaultVoiceMode, isVoiceMode, convaiClient]);
264
316
  // Close settings when clicking outside
265
317
  useEffect(() => {
266
318
  const handleClickOutside = (event) => {
@@ -495,7 +547,7 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
495
547
  flexShrink: 0,
496
548
  padding: 0,
497
549
  outline: "none",
498
- }, "aria-label": "Exit Voice Mode", title: "Exit voice mode", children: _jsx(FiPhoneOff, { size: 20 }) }, "voice-mode-exit")) : (_jsx(_Fragment, { children: _jsxs(InputContainer, { style: { flex: 1, position: "relative" }, children: [_jsx(Input, { type: "text", placeholder: "Conversation", value: inputValue, onChange: handleInputChange, onKeyPress: handleKeyPress, style: { paddingRight: "2.75rem" } }), _jsx("div", { style: {
550
+ }, "aria-label": "Exit Voice Mode", title: "Exit voice mode", children: _jsx(FiPhoneOff, { size: 20 }) }, "voice-mode-exit")) : (_jsx(_Fragment, { children: _jsxs(InputContainer, { style: { flex: 1, position: "relative" }, children: [_jsx(Input, { type: "text", placeholder: "Conversation", value: inputValue, onChange: handleInputChange, onKeyPress: handleKeyPress, disabled: !canSendText, style: { paddingRight: "2.75rem" } }), _jsx("div", { style: {
499
551
  position: "absolute",
500
552
  right: "0.375rem",
501
553
  top: "50%",
@@ -510,10 +562,10 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
510
562
  alignItems: "center",
511
563
  justifyContent: "center",
512
564
  zIndex: 2,
513
- }, children: _jsx(AnimatePresence, { mode: "wait", children: inputValue.length > 0 ? (_jsx(ActionButton, { initial: { y: "100%" }, animate: { y: "0%" }, exit: { y: "-100%" }, transition: {
565
+ }, children: _jsx(AnimatePresence, { mode: "wait", children: inputValue.length > 0 || isTextOnly ? (_jsx(ActionButton, { initial: { y: "100%" }, animate: { y: "0%" }, exit: { y: "-100%" }, transition: {
514
566
  duration: 0.1,
515
567
  ease: "linear",
516
- }, onClick: handleSend, whileTap: { scale: 0.9 }, style: {
568
+ }, onClick: handleSend, disabled: inputValue.length === 0 || !canSendText, whileTap: { scale: 0.9 }, style: {
517
569
  position: "absolute",
518
570
  top: 0,
519
571
  left: 0,
@@ -548,7 +600,11 @@ export const ConvaiWidget = ({ convaiClient, showVideo = true, showScreenShare =
548
600
  flexShrink: 0,
549
601
  padding: 0,
550
602
  outline: "none",
551
- }, "aria-label": "Enter Voice Mode", title: "Use voice mode", children: _jsx(PiWaveform, { size: 20 }) }, "voice")) }) })] }) })) })] }))] }), convaiClient.state.isConnected && (_jsx(SettingsTray, { isOpen: isSettingsOpen, onReset: handleReset, onDisconnect: handleDisconnect, onToggleVideo: convaiClient.connectionType === "video" && showVideo
603
+ }, "aria-label": "Enter Voice Mode", title: "Use voice mode", children: _jsx(PiWaveform, { size: 20 }) }, "voice")) }) })] }) })) })] }))] }), convaiClient.state.isConnected && (_jsx(SettingsTray, { isOpen: isSettingsOpen, onReset: convaiClient.isPublishedChatSession && !requestPublicationGrant
604
+ ? undefined
605
+ : handleReset, onDisconnect: convaiClient.isPublishedChatSession && !requestPublicationGrant
606
+ ? undefined
607
+ : handleDisconnect, onToggleVideo: convaiClient.connectionType === "video" && showVideo
552
608
  ? handleToggleVideo
553
609
  : undefined, isVideoVisible: isVideoVisible, onToggleScreenShare: convaiClient.connectionType === "video" && showScreenShare
554
610
  ? handleToggleScreenShare