@thoughtspot/visual-embed-sdk 1.52.2-beta.1 → 1.53.0

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 (237) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/css-variables.d.ts +373 -5
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +14 -0
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js +6 -3
  7. package/cjs/src/embed/app.js.map +1 -1
  8. package/cjs/src/embed/app.spec.js +28 -0
  9. package/cjs/src/embed/app.spec.js.map +1 -1
  10. package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -1
  11. package/cjs/src/embed/auto-frame-renderer.js +244 -4
  12. package/cjs/src/embed/auto-frame-renderer.js.map +1 -1
  13. package/cjs/src/embed/auto-frame-renderer.spec.js +389 -0
  14. package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -1
  15. package/cjs/src/embed/bodyless-conversation.js +1 -1
  16. package/cjs/src/embed/bodyless-conversation.js.map +1 -1
  17. package/cjs/src/embed/conversation.d.ts +15 -0
  18. package/cjs/src/embed/conversation.d.ts.map +1 -1
  19. package/cjs/src/embed/conversation.js +7 -3
  20. package/cjs/src/embed/conversation.js.map +1 -1
  21. package/cjs/src/embed/conversation.spec.js +66 -0
  22. package/cjs/src/embed/conversation.spec.js.map +1 -1
  23. package/cjs/src/embed/liveboard-override-utils.d.ts +59 -0
  24. package/cjs/src/embed/liveboard-override-utils.d.ts.map +1 -0
  25. package/cjs/src/embed/liveboard-override-utils.js +19 -0
  26. package/cjs/src/embed/liveboard-override-utils.js.map +1 -0
  27. package/cjs/src/embed/liveboard-override-utils.spec.d.ts +2 -0
  28. package/cjs/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
  29. package/cjs/src/embed/liveboard-override-utils.spec.js +34 -0
  30. package/cjs/src/embed/liveboard-override-utils.spec.js.map +1 -0
  31. package/cjs/src/embed/liveboard.d.ts +43 -1
  32. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  33. package/cjs/src/embed/liveboard.js +11 -18
  34. package/cjs/src/embed/liveboard.js.map +1 -1
  35. package/cjs/src/embed/liveboard.spec.js +104 -54
  36. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  37. package/cjs/src/embed/search-bar.js +1 -1
  38. package/cjs/src/embed/search-bar.js.map +1 -1
  39. package/cjs/src/embed/search.js +1 -1
  40. package/cjs/src/embed/search.js.map +1 -1
  41. package/cjs/src/embed/search.spec.js +20 -0
  42. package/cjs/src/embed/search.spec.js.map +1 -1
  43. package/cjs/src/embed/spotter-utils.d.ts +11 -0
  44. package/cjs/src/embed/spotter-utils.d.ts.map +1 -1
  45. package/cjs/src/embed/spotter-utils.js +13 -1
  46. package/cjs/src/embed/spotter-utils.js.map +1 -1
  47. package/cjs/src/embed/spotter-viz-utils.d.ts +15 -0
  48. package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -1
  49. package/cjs/src/embed/spotter-viz-utils.js.map +1 -1
  50. package/cjs/src/embed/spotter-viz-utils.spec.js +11 -0
  51. package/cjs/src/embed/spotter-viz-utils.spec.js.map +1 -1
  52. package/cjs/src/embed/ts-embed.d.ts +27 -34
  53. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  54. package/cjs/src/embed/ts-embed.js +224 -102
  55. package/cjs/src/embed/ts-embed.js.map +1 -1
  56. package/cjs/src/embed/ts-embed.spec.d.ts.map +1 -1
  57. package/cjs/src/embed/ts-embed.spec.js +543 -171
  58. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  59. package/cjs/src/index.d.ts +4 -2
  60. package/cjs/src/index.d.ts.map +1 -1
  61. package/cjs/src/index.js +4 -2
  62. package/cjs/src/index.js.map +1 -1
  63. package/cjs/src/mixpanel-service.d.ts +1 -0
  64. package/cjs/src/mixpanel-service.d.ts.map +1 -1
  65. package/cjs/src/mixpanel-service.js +1 -0
  66. package/cjs/src/mixpanel-service.js.map +1 -1
  67. package/cjs/src/react/all-types-export.d.ts +1 -1
  68. package/cjs/src/react/all-types-export.d.ts.map +1 -1
  69. package/cjs/src/react/all-types-export.js +2 -1
  70. package/cjs/src/react/all-types-export.js.map +1 -1
  71. package/cjs/src/types.d.ts +182 -2
  72. package/cjs/src/types.d.ts.map +1 -1
  73. package/cjs/src/types.js +109 -0
  74. package/cjs/src/types.js.map +1 -1
  75. package/cjs/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
  76. package/cjs/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
  77. package/cjs/src/utils/graphql/answerService/answer-queries.js +8 -1
  78. package/cjs/src/utils/graphql/answerService/answer-queries.js.map +1 -1
  79. package/cjs/src/utils/graphql/answerService/answerService.d.ts +28 -4
  80. package/cjs/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  81. package/cjs/src/utils/graphql/answerService/answerService.js +39 -4
  82. package/cjs/src/utils/graphql/answerService/answerService.js.map +1 -1
  83. package/cjs/src/utils/graphql/answerService/answerService.spec.js +124 -0
  84. package/cjs/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
  85. package/dist/{index-Bt56pC82.js → index-CyDR8j_0.js} +1 -1
  86. package/dist/src/css-variables.d.ts +373 -5
  87. package/dist/src/css-variables.d.ts.map +1 -1
  88. package/dist/src/embed/app.d.ts +14 -0
  89. package/dist/src/embed/app.d.ts.map +1 -1
  90. package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -1
  91. package/dist/src/embed/conversation.d.ts +15 -0
  92. package/dist/src/embed/conversation.d.ts.map +1 -1
  93. package/dist/src/embed/liveboard-override-utils.d.ts +59 -0
  94. package/dist/src/embed/liveboard-override-utils.d.ts.map +1 -0
  95. package/dist/src/embed/liveboard-override-utils.spec.d.ts +2 -0
  96. package/dist/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
  97. package/dist/src/embed/liveboard.d.ts +43 -1
  98. package/dist/src/embed/liveboard.d.ts.map +1 -1
  99. package/dist/src/embed/spotter-utils.d.ts +11 -0
  100. package/dist/src/embed/spotter-utils.d.ts.map +1 -1
  101. package/dist/src/embed/spotter-viz-utils.d.ts +15 -0
  102. package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -1
  103. package/dist/src/embed/ts-embed.d.ts +27 -34
  104. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  105. package/dist/src/embed/ts-embed.spec.d.ts.map +1 -1
  106. package/dist/src/index.d.ts +4 -2
  107. package/dist/src/index.d.ts.map +1 -1
  108. package/dist/src/mixpanel-service.d.ts +1 -0
  109. package/dist/src/mixpanel-service.d.ts.map +1 -1
  110. package/dist/src/react/all-types-export.d.ts +1 -1
  111. package/dist/src/react/all-types-export.d.ts.map +1 -1
  112. package/dist/src/types.d.ts +182 -2
  113. package/dist/src/types.d.ts.map +1 -1
  114. package/dist/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
  115. package/dist/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
  116. package/dist/src/utils/graphql/answerService/answerService.d.ts +28 -4
  117. package/dist/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  118. package/dist/tsembed-react.es.js +437 -175
  119. package/dist/tsembed-react.js +436 -174
  120. package/dist/tsembed.es.js +680 -180
  121. package/dist/tsembed.js +678 -178
  122. package/dist/visual-embed-sdk-react-full.d.ts +702 -44
  123. package/dist/visual-embed-sdk-react.d.ts +697 -44
  124. package/dist/visual-embed-sdk.d.ts +729 -45
  125. package/lib/package.json +1 -1
  126. package/lib/src/css-variables.d.ts +373 -5
  127. package/lib/src/css-variables.d.ts.map +1 -1
  128. package/lib/src/embed/app.d.ts +14 -0
  129. package/lib/src/embed/app.d.ts.map +1 -1
  130. package/lib/src/embed/app.js +7 -4
  131. package/lib/src/embed/app.js.map +1 -1
  132. package/lib/src/embed/app.spec.js +28 -0
  133. package/lib/src/embed/app.spec.js.map +1 -1
  134. package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -1
  135. package/lib/src/embed/auto-frame-renderer.js +244 -4
  136. package/lib/src/embed/auto-frame-renderer.js.map +1 -1
  137. package/lib/src/embed/auto-frame-renderer.spec.js +389 -0
  138. package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -1
  139. package/lib/src/embed/bodyless-conversation.js +1 -1
  140. package/lib/src/embed/bodyless-conversation.js.map +1 -1
  141. package/lib/src/embed/conversation.d.ts +15 -0
  142. package/lib/src/embed/conversation.d.ts.map +1 -1
  143. package/lib/src/embed/conversation.js +8 -4
  144. package/lib/src/embed/conversation.js.map +1 -1
  145. package/lib/src/embed/conversation.spec.js +67 -1
  146. package/lib/src/embed/conversation.spec.js.map +1 -1
  147. package/lib/src/embed/liveboard-override-utils.d.ts +59 -0
  148. package/lib/src/embed/liveboard-override-utils.d.ts.map +1 -0
  149. package/lib/src/embed/liveboard-override-utils.js +15 -0
  150. package/lib/src/embed/liveboard-override-utils.js.map +1 -0
  151. package/lib/src/embed/liveboard-override-utils.spec.d.ts +2 -0
  152. package/lib/src/embed/liveboard-override-utils.spec.d.ts.map +1 -0
  153. package/lib/src/embed/liveboard-override-utils.spec.js +32 -0
  154. package/lib/src/embed/liveboard-override-utils.spec.js.map +1 -0
  155. package/lib/src/embed/liveboard.d.ts +43 -1
  156. package/lib/src/embed/liveboard.d.ts.map +1 -1
  157. package/lib/src/embed/liveboard.js +11 -17
  158. package/lib/src/embed/liveboard.js.map +1 -1
  159. package/lib/src/embed/liveboard.spec.js +104 -54
  160. package/lib/src/embed/liveboard.spec.js.map +1 -1
  161. package/lib/src/embed/search-bar.js +1 -1
  162. package/lib/src/embed/search-bar.js.map +1 -1
  163. package/lib/src/embed/search.js +1 -1
  164. package/lib/src/embed/search.js.map +1 -1
  165. package/lib/src/embed/search.spec.js +20 -0
  166. package/lib/src/embed/search.spec.js.map +1 -1
  167. package/lib/src/embed/spotter-utils.d.ts +11 -0
  168. package/lib/src/embed/spotter-utils.d.ts.map +1 -1
  169. package/lib/src/embed/spotter-utils.js +12 -0
  170. package/lib/src/embed/spotter-utils.js.map +1 -1
  171. package/lib/src/embed/spotter-viz-utils.d.ts +15 -0
  172. package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -1
  173. package/lib/src/embed/spotter-viz-utils.js.map +1 -1
  174. package/lib/src/embed/spotter-viz-utils.spec.js +11 -0
  175. package/lib/src/embed/spotter-viz-utils.spec.js.map +1 -1
  176. package/lib/src/embed/ts-embed.d.ts +27 -34
  177. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  178. package/lib/src/embed/ts-embed.js +224 -102
  179. package/lib/src/embed/ts-embed.js.map +1 -1
  180. package/lib/src/embed/ts-embed.spec.d.ts.map +1 -1
  181. package/lib/src/embed/ts-embed.spec.js +544 -172
  182. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  183. package/lib/src/index.d.ts +4 -2
  184. package/lib/src/index.d.ts.map +1 -1
  185. package/lib/src/index.js +3 -2
  186. package/lib/src/index.js.map +1 -1
  187. package/lib/src/mixpanel-service.d.ts +1 -0
  188. package/lib/src/mixpanel-service.d.ts.map +1 -1
  189. package/lib/src/mixpanel-service.js +1 -0
  190. package/lib/src/mixpanel-service.js.map +1 -1
  191. package/lib/src/react/all-types-export.d.ts +1 -1
  192. package/lib/src/react/all-types-export.d.ts.map +1 -1
  193. package/lib/src/react/all-types-export.js +1 -1
  194. package/lib/src/react/all-types-export.js.map +1 -1
  195. package/lib/src/types.d.ts +182 -2
  196. package/lib/src/types.d.ts.map +1 -1
  197. package/lib/src/types.js +109 -0
  198. package/lib/src/types.js.map +1 -1
  199. package/lib/src/utils/graphql/answerService/answer-queries.d.ts +1 -0
  200. package/lib/src/utils/graphql/answerService/answer-queries.d.ts.map +1 -1
  201. package/lib/src/utils/graphql/answerService/answer-queries.js +7 -0
  202. package/lib/src/utils/graphql/answerService/answer-queries.js.map +1 -1
  203. package/lib/src/utils/graphql/answerService/answerService.d.ts +28 -4
  204. package/lib/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  205. package/lib/src/utils/graphql/answerService/answerService.js +39 -4
  206. package/lib/src/utils/graphql/answerService/answerService.js.map +1 -1
  207. package/lib/src/utils/graphql/answerService/answerService.spec.js +125 -1
  208. package/lib/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
  209. package/lib/src/visual-embed-sdk.d.ts +729 -45
  210. package/package.json +1 -1
  211. package/src/css-variables.ts +475 -26
  212. package/src/embed/app.spec.ts +32 -0
  213. package/src/embed/app.ts +28 -4
  214. package/src/embed/auto-frame-renderer.spec.ts +464 -0
  215. package/src/embed/auto-frame-renderer.ts +305 -6
  216. package/src/embed/bodyless-conversation.ts +1 -1
  217. package/src/embed/conversation.spec.ts +94 -1
  218. package/src/embed/conversation.ts +28 -3
  219. package/src/embed/liveboard-override-utils.spec.ts +38 -0
  220. package/src/embed/liveboard-override-utils.ts +71 -0
  221. package/src/embed/liveboard.spec.ts +119 -62
  222. package/src/embed/liveboard.ts +56 -21
  223. package/src/embed/search-bar.tsx +1 -1
  224. package/src/embed/search.spec.ts +23 -0
  225. package/src/embed/search.ts +1 -1
  226. package/src/embed/spotter-utils.ts +12 -0
  227. package/src/embed/spotter-viz-utils.spec.ts +11 -0
  228. package/src/embed/spotter-viz-utils.ts +15 -0
  229. package/src/embed/ts-embed.spec.ts +684 -213
  230. package/src/embed/ts-embed.ts +227 -97
  231. package/src/index.ts +16 -1
  232. package/src/mixpanel-service.ts +1 -0
  233. package/src/react/all-types-export.ts +6 -0
  234. package/src/types.ts +186 -5
  235. package/src/utils/graphql/answerService/answer-queries.ts +8 -0
  236. package/src/utils/graphql/answerService/answerService.spec.ts +145 -1
  237. package/src/utils/graphql/answerService/answerService.ts +65 -3
@@ -1,6 +1,39 @@
1
1
  import { AutoMCPFrameRendererViewConfig, Param } from "../types";
2
2
  import { TsEmbed } from "./ts-embed";
3
3
  import { getQueryParamString } from "../utils";
4
+ import { tokenizedFetch } from "../tokenizedFetch";
5
+ import { logger } from "../utils/logger";
6
+
7
+ /**
8
+ * Route the ThoughtSpot application uses to render a single conversational
9
+ * answer. Used when a replayed frame carries no route of its own.
10
+ */
11
+ const CONV_ASSIST_ANSWER_ROUTE = '/embed/conv-assist-answer';
12
+
13
+ /**
14
+ * Base path of the conversation service, proxied by the cluster's nginx as
15
+ * the `convex` upstream. These are the same handlers the public
16
+ * `/api/rest/2.0/ai/agent/conversations/...` endpoints proxy to, so the
17
+ * response shapes are identical, but calling them directly skips the prism
18
+ * hop and its error re-wrapping.
19
+ */
20
+ const CONVERSATION_SERVICE_PATH = '/conversation/v2';
21
+
22
+ const DEFAULT_STALE_ANSWER_NOTICE =
23
+ 'This data may have changed since the last time you had a chat.';
24
+
25
+ /**
26
+ * Hash parameters that locate a specific answer generation. These expire with
27
+ * the answer object (roughly 8 hours), which is why a host app replaying a
28
+ * stored conversation must not persist them - it stores the conversation
29
+ * identifier instead and lets the renderer resolve these afresh.
30
+ */
31
+ interface AnswerSessionParams {
32
+ sessionId: string;
33
+ genNo: string;
34
+ acSessionId: string;
35
+ acGenNo: string;
36
+ }
4
37
 
5
38
 
6
39
  /**
@@ -36,7 +69,7 @@ import { getQueryParamString } from "../utils";
36
69
  * // Later, stop watching
37
70
  * observer.disconnect();
38
71
  * ```
39
- *
72
+ *
40
73
  * @example
41
74
  * Detailed example of how to use the auto-frame renderer:
42
75
  * [Python React Agent Simple UI](https://github.com/thoughtspot/developer-examples/tree/main/mcp/python-react-agent-simple-ui)
@@ -70,6 +103,122 @@ export function startAutoMCPFrameRenderer(viewConfig: AutoMCPFrameRendererViewCo
70
103
  return observer;
71
104
  }
72
105
 
106
+ /**
107
+ * Writes resolved session parameters into the route's hash query string,
108
+ * preserving the route and any other hash params the source URL carried.
109
+ *
110
+ * @param sourceHash - The source URL's hash, with or without a leading `#`.
111
+ * Falls back to the conversational-answer route when empty, so a host app
112
+ * replaying a chat need only supply the conversation identifier.
113
+ */
114
+ function buildAnswerHash(sourceHash: string, resolved: AnswerSessionParams): string {
115
+ const withoutHash = (sourceHash || '').replace('#', '');
116
+ const [route, existingQuery] = withoutHash.split('?');
117
+ const params = new URLSearchParams(existingQuery || '');
118
+ Object.entries(resolved).forEach(([key, value]) => params.set(key, value));
119
+ return `${route || CONV_ASSIST_ANSWER_ROUTE}?${params.toString()}`;
120
+ }
121
+
122
+ /**
123
+ * Reads {@link Param.TsmcpAnswerIndex} as a whole, non-negative array index.
124
+ *
125
+ * Anything else - absent, malformed, fractional, negative - falls back to the
126
+ * first answer rather than indexing with a value that can never match.
127
+ */
128
+ function answerIndexFromParam(raw: string | null): number {
129
+ const parsed = Number(raw);
130
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
131
+ }
132
+
133
+ /**
134
+ * In-flight answer-id list requests, keyed by conversation-service URL.
135
+ *
136
+ * Replaying a stored chat mounts every one of its frames at once, and each
137
+ * frame resolves on its own. Without this, N frames from one conversation make
138
+ * N identical list calls. Sharing the in-flight request collapses them to one.
139
+ *
140
+ * Only the id list is shared, and only while its request is in flight: the
141
+ * entry is dropped once the request settles, whether it succeeded or not. A
142
+ * failed response is therefore never reused, and a frame mounted later always
143
+ * reads a fresh list. The per-answer load call is never shared - it returns
144
+ * live session state with its own expiry.
145
+ */
146
+ const answerIdListRequests = new Map<string, Promise<string[] | null>>();
147
+
148
+ /**
149
+ * Lists a conversation's answer ids: its non-thinking `answer` items, in
150
+ * message order. The host app counts answers the same way, so the Nth id here
151
+ * is the Nth stored answer.
152
+ *
153
+ * @param conversationUrl - The conversation's base URL on the conversation
154
+ * service, `{host}/conversation/v2/{encoded id}`.
155
+ * @param conversationId - The conversation identifier, used in log messages.
156
+ * @returns The ids, or `null` when the conversation cannot be read. Never
157
+ * rejects, so callers sharing the request need no error handling of their own.
158
+ */
159
+ async function fetchAnswerIds(conversationUrl: string, conversationId: string): Promise<string[] | null> {
160
+ try {
161
+ const messagesResponse = await tokenizedFetch(`${conversationUrl}/public`, {
162
+ credentials: 'include',
163
+ headers: { Accept: 'application/json' },
164
+ });
165
+ if (!messagesResponse.ok) {
166
+ logger.warn(
167
+ `[AutoFrameRenderer] Conversation fetch failed for ${conversationId}: ${messagesResponse.status}`,
168
+ );
169
+ return null;
170
+ }
171
+ const messagesBody = await messagesResponse.json();
172
+ const messages = messagesBody?.messages;
173
+
174
+ const answerIds: string[] = [];
175
+ for (const message of Array.isArray(messages) ? messages : []) {
176
+ for (const item of message?.response_items || []) {
177
+ if (item?.type === 'answer' && item?.is_thinking === false && item?.answer_id) {
178
+ answerIds.push(item.answer_id);
179
+ }
180
+ }
181
+ }
182
+ return answerIds;
183
+ } catch (e) {
184
+ logger.warn(`[AutoFrameRenderer] Failed to list answers for ${conversationId}: ${e}`);
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * The answer-id list for a conversation, joining a request already in flight
191
+ * for it.
192
+ *
193
+ * @param conversationUrl - The conversation's base URL on the conversation
194
+ * service. Also the key requests are shared under.
195
+ * @param conversationId - The conversation identifier, used in log messages.
196
+ * @param stale - A request the caller already read and found too short. It is
197
+ * not reused; the first caller to pass it starts a new request, and the
198
+ * others join that one.
199
+ * @returns The shared request: the ids, or `null` when the conversation cannot
200
+ * be read. Never rejects.
201
+ */
202
+ function getAnswerIds(
203
+ conversationUrl: string,
204
+ conversationId: string,
205
+ stale?: Promise<string[] | null>,
206
+ ): Promise<string[] | null> {
207
+ const inFlight = answerIdListRequests.get(conversationUrl);
208
+ if (inFlight && inFlight !== stale) {
209
+ return inFlight;
210
+ }
211
+ const request = fetchAnswerIds(conversationUrl, conversationId);
212
+ answerIdListRequests.set(conversationUrl, request);
213
+ const release = () => {
214
+ if (answerIdListRequests.get(conversationUrl) === request) {
215
+ answerIdListRequests.delete(conversationUrl);
216
+ }
217
+ };
218
+ request.then(release, release);
219
+ return request;
220
+ }
221
+
73
222
  function isTSMCPIframe(iframe: HTMLIFrameElement) {
74
223
  try {
75
224
  const url = new URL(iframe.src);
@@ -105,22 +254,135 @@ class AutoFrameRenderer extends TsEmbed {
105
254
  * ThoughtSpot application.
106
255
  *
107
256
  * @param sourceSrc - The original iframe's `src` URL string.
257
+ * @param preserveSourceUrl - When true, return the source URL unchanged.
108
258
  * @returns The constructed URL to use for the ThoughtSpot embed iframe.
109
259
  */
110
- private getMCPIframeSrc(sourceSrc: string) {
111
- const queryParams = this.getEmbedParamsObject();
260
+ private getMCPIframeSrc(
261
+ sourceSrc: string,
262
+ resolved?: AnswerSessionParams,
263
+ preserveSourceUrl = false,
264
+ ) {
265
+ if (preserveSourceUrl) {
266
+ return sourceSrc;
267
+ }
268
+ const queryParams = this.getUrlQueryParamsObject();
112
269
  const sourceURL = new URL(sourceSrc);
113
270
  const existingQueryParams = sourceURL.searchParams;
114
271
  const existingQueryParamsObject = Object.fromEntries(existingQueryParams);
115
272
  delete existingQueryParamsObject[Param.Tsmcp];
273
+ // Replay markers address the SDK, not the ThoughtSpot application.
274
+ delete existingQueryParamsObject[Param.TsmcpConversationId];
275
+ delete existingQueryParamsObject[Param.TsmcpAnswerIndex];
116
276
 
117
277
  const mergedQueryParams = { ...queryParams, ...existingQueryParamsObject };
118
278
  const mergedQueryParamsString = getQueryParamString(mergedQueryParams, true);
119
279
  const queryString = mergedQueryParamsString ? `?${mergedQueryParamsString}` : '';
120
- const frameSrc = `${this.getEmbedBasePath(queryString)}${sourceURL.hash.replace('#', '')}`;
280
+ const hash = resolved
281
+ ? buildAnswerHash(sourceURL.hash, resolved)
282
+ : sourceURL.hash.replace('#', '');
283
+ const frameSrc = `${this.getEmbedBasePath(queryString)}${hash}`;
121
284
  return frameSrc;
122
285
  }
123
286
 
287
+ /**
288
+ * Resolves the current session parameters for an answer stored only as a
289
+ * conversation identifier plus an ordinal position.
290
+ *
291
+ * The answer object behind `sessionId`/`genNo` expires (roughly 8 hours),
292
+ * so a host app replaying a stored chat cannot persist those values. It
293
+ * persists the conversation identifier instead, and this walks the two
294
+ * conversation-service APIs to rebuild them:
295
+ *
296
+ * 1. `GET {conv}/public` lists the conversation's messages; the
297
+ * non-thinking `answer` items, in message order, give the answer ids.
298
+ * Frames from the same conversation share one in-flight call - see
299
+ * {@link getAnswerIds}.
300
+ * 2. `POST {conv}/message/{id}/load/public` turns the answer id at
301
+ * `answerIndex` into a live session identifier, generation number and
302
+ * agent-context state.
303
+ *
304
+ * @returns The resolved parameters, or `null` when resolution fails - the
305
+ * caller then falls back to whatever the source URL already carried.
306
+ */
307
+ private async resolveAnswerSessionParams(
308
+ conversationId: string,
309
+ answerIndex: number,
310
+ ): Promise<AnswerSessionParams | null> {
311
+ const base = `${this.thoughtSpotHost}${CONVERSATION_SERVICE_PATH}/${encodeURIComponent(conversationId)}`;
312
+ try {
313
+ const listRequest = getAnswerIds(base, conversationId);
314
+ let answerIds = await listRequest;
315
+ if (answerIds && !answerIds[answerIndex]) {
316
+ // The list only grows, so a short one may predate this answer:
317
+ // a frame that joined a request started before it was added.
318
+ // Read the list once more before giving up.
319
+ answerIds = await getAnswerIds(base, conversationId, listRequest);
320
+ }
321
+ if (!answerIds) {
322
+ return null;
323
+ }
324
+
325
+ const answerId = answerIds[answerIndex];
326
+ if (!answerId) {
327
+ logger.warn(
328
+ `[AutoFrameRenderer] No answer at index ${answerIndex} in conversation ${conversationId} (found ${answerIds.length}).`,
329
+ );
330
+ return null;
331
+ }
332
+
333
+ // The load handler is a POST upstream; the public REST endpoint
334
+ // exposed it as a GET and supplied this body on the caller's
335
+ // behalf.
336
+ const detailsResponse = await tokenizedFetch(
337
+ `${base}/message/${encodeURIComponent(answerId)}/load/public`,
338
+ {
339
+ credentials: 'include',
340
+ method: 'POST',
341
+ headers: {
342
+ Accept: 'application/json',
343
+ 'Content-Type': 'application/json',
344
+ 'x-requested-by': 'ThoughtSpot',
345
+ },
346
+ body: JSON.stringify({ type: 'TS_ANSWER' }),
347
+ },
348
+ );
349
+ if (!detailsResponse.ok) {
350
+ logger.warn(
351
+ `[AutoFrameRenderer] Answer load failed for ${answerId}: ${detailsResponse.status}`,
352
+ );
353
+ return null;
354
+ }
355
+ const detailsBody = await detailsResponse.json();
356
+ const answer = detailsBody?.answer;
357
+ const acState = answer?.ac_state;
358
+ // All four are nullable in the load-answer response. A missing
359
+ // generation number is as unusable as a missing session id:
360
+ // stringifying it would put a literal "null" in the hash. Fail
361
+ // the resolve instead and let the caller fall back.
362
+ if (
363
+ !answer?.session_identifier
364
+ || !acState?.transaction_identifier
365
+ || !(Number.isInteger(answer.generation_number) && answer.generation_number > 0)
366
+ || !(Number.isInteger(acState.generation_number) && acState.generation_number > 0)
367
+ ) {
368
+ logger.warn(
369
+ `[AutoFrameRenderer] Answer load returned incomplete session state for ${answerId}.`,
370
+ );
371
+ return null;
372
+ }
373
+
374
+ return {
375
+ sessionId: answer.session_identifier,
376
+ genNo: String(answer.generation_number),
377
+ acSessionId: acState.transaction_identifier,
378
+ acGenNo: String(acState.generation_number),
379
+ };
380
+ } catch (e) {
381
+ logger.warn(`[AutoFrameRenderer] Failed to resolve stored answer: ${e}`);
382
+ return null;
383
+ }
384
+ }
385
+
124
386
  /**
125
387
  * Overrides the base insertion behavior so the new embed iframe
126
388
  * replaces the original iframe in-place rather than being appended
@@ -149,9 +411,46 @@ class AutoFrameRenderer extends TsEmbed {
149
411
  if (this.shouldWaitForRenderPromise) {
150
412
  await this.isReadyForRenderPromise;
151
413
  }
152
- const src = this.getMCPIframeSrc(iframe.src);
414
+
415
+ // Only frames that ask for it take the replay path. A frame straight
416
+ // from the MCP server carries its own session params and no
417
+ // conversation id, so it renders exactly as it always has.
418
+ const sourceParams = new URL(iframe.src).searchParams;
419
+ const conversationId = sourceParams.get(Param.TsmcpConversationId);
420
+ let resolved: AnswerSessionParams | null = null;
421
+ if (conversationId) {
422
+ resolved = await this.resolveAnswerSessionParams(
423
+ conversationId,
424
+ answerIndexFromParam(sourceParams.get(Param.TsmcpAnswerIndex)),
425
+ );
426
+ }
427
+
428
+ const src = this.getMCPIframeSrc(
429
+ iframe.src,
430
+ resolved ?? undefined,
431
+ Boolean(conversationId) && !resolved,
432
+ );
153
433
  await this.renderIFrame(src);
434
+
435
+ // Mark only answers actually re-resolved: their numbers were computed
436
+ // now, not when the conversation happened.
437
+ if (resolved && !this.viewConfig.suppressStaleAnswerNotice) {
438
+ this.markAsStaleAnswer();
439
+ }
154
440
  this.isRendered = true;
155
441
  }
156
- }
157
442
 
443
+ /**
444
+ * Adds the "data may have changed" notice to the rendered frame as a
445
+ * native tooltip, so it needs no stylesheet and cannot disturb the host
446
+ * app's layout.
447
+ */
448
+ private markAsStaleAnswer(): void {
449
+ if (!this.iFrame) {
450
+ return;
451
+ }
452
+ this.iFrame.title = this.viewConfig.staleAnswerNoticeText || DEFAULT_STALE_ANSWER_NOTICE;
453
+ // Exposed so a host app can style or replace the native tooltip.
454
+ this.iFrame.dataset.tsStaleAnswer = 'true';
455
+ }
456
+ }
@@ -56,7 +56,7 @@ export class ConversationMessage extends TsEmbed {
56
56
  messageId,
57
57
  } = this.viewConfig;
58
58
  const path = 'conv-assist-answer';
59
- const queryParams = this.getEmbedParamsObject();
59
+ const queryParams = this.getUrlQueryParamsObject();
60
60
 
61
61
  let query = '';
62
62
  const queryParamsString = getQueryParamString(queryParams, true);
@@ -1,4 +1,10 @@
1
- import { SpotterEmbed, SpotterEmbedViewConfig, SpotterQueryMode, ConversationEmbed } from './conversation';
1
+ import {
2
+ SpotterEmbed,
3
+ SpotterEmbedViewConfig,
4
+ SpotterQueryMode,
5
+ ConversationEmbed,
6
+ } from './conversation';
7
+ import { SpotterExperienceVersion } from './spotter-utils';
2
8
  import { TsEmbed } from './ts-embed';
3
9
  import * as authInstance from '../auth';
4
10
  import { Action, init } from '../index';
@@ -79,6 +85,93 @@ describe('ConversationEmbed', () => {
79
85
  );
80
86
  });
81
87
 
88
+ it('should use the conv-assist path when spotterExperienceVersion is not set', async () => {
89
+ const viewConfig: SpotterEmbedViewConfig = {
90
+ worksheetId: 'worksheetId',
91
+ searchOptions: {
92
+ searchQuery: 'searchQuery',
93
+ },
94
+ };
95
+
96
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
97
+ await conversationEmbed.render();
98
+ expect(getIFrameSrc().split('#')[1].split('?')[0]).toBe(
99
+ '/embed/insights/conv-assist',
100
+ );
101
+ });
102
+
103
+ it('should use the spotter path when spotterExperienceVersion is SPOTTER_2026_11', async () => {
104
+ const viewConfig: SpotterEmbedViewConfig = {
105
+ worksheetId: 'worksheetId',
106
+ searchOptions: {
107
+ searchQuery: 'searchQuery',
108
+ },
109
+ spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
110
+ };
111
+
112
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
113
+ await conversationEmbed.render();
114
+ expect(getIFrameSrc().split('#')[1].split('?')[0]).toBe(
115
+ '/embed/insights/spotter',
116
+ );
117
+ });
118
+
119
+ it('should use the spotter reader-view path when spotterExperienceVersion is SPOTTER_2026_11 and sharedConversationId is set', async () => {
120
+ const viewConfig: SpotterEmbedViewConfig = {
121
+ worksheetId: 'worksheetId',
122
+ searchOptions: {
123
+ searchQuery: 'searchQuery',
124
+ },
125
+ sharedConversationId: 'conv-123',
126
+ spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
127
+ };
128
+
129
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
130
+ await conversationEmbed.render();
131
+ expect(getIFrameSrc().split('#')[1].split('?')[0]).toBe(
132
+ '/embed/insights/spotter/s/conv-123',
133
+ );
134
+ });
135
+
136
+ it('should keep the conv-assist path for an unrecognized spotterExperienceVersion value', async () => {
137
+ const viewConfig: SpotterEmbedViewConfig = {
138
+ worksheetId: 'worksheetId',
139
+ searchOptions: {
140
+ searchQuery: 'searchQuery',
141
+ },
142
+ spotterExperienceVersion: 'some_future_ui' as SpotterExperienceVersion,
143
+ };
144
+
145
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
146
+ await conversationEmbed.render();
147
+ expect(getIFrameSrc().split('#')[1].split('?')[0]).toBe(
148
+ '/embed/insights/conv-assist',
149
+ );
150
+ });
151
+
152
+ it('should add the spotterExperienceVersion param when spotterExperienceVersion is set', async () => {
153
+ const viewConfig: SpotterEmbedViewConfig = {
154
+ worksheetId: 'worksheetId',
155
+ spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
156
+ };
157
+
158
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
159
+ await conversationEmbed.render();
160
+ expect(getIFrameSrc()).toContain(
161
+ `spotterExperienceVersion=${SpotterExperienceVersion.SPOTTER_2026_11}`,
162
+ );
163
+ });
164
+
165
+ it('should not add the spotterExperienceVersion param when spotterExperienceVersion is not set', async () => {
166
+ const viewConfig: SpotterEmbedViewConfig = {
167
+ worksheetId: 'worksheetId',
168
+ };
169
+
170
+ const conversationEmbed = new SpotterEmbed(getRootEl(), viewConfig);
171
+ await conversationEmbed.render();
172
+ expect(getIFrameSrc()).not.toContain('spotterExperienceVersion=');
173
+ });
174
+
82
175
  it('should render the conversation embed with worksheets disabled', async () => {
83
176
  const viewConfig: SpotterEmbedViewConfig = {
84
177
  worksheetId: 'worksheetId',
@@ -1,6 +1,12 @@
1
1
  import { Param, BaseViewConfig, RuntimeFilter, RuntimeParameter, ErrorDetailsTypes, EmbedErrorCodes, DefaultAppInitData, VisualizationOverrides, SpotterFileUploadFileTypes } from '../types';
2
2
  import { TsEmbed } from './ts-embed';
3
- import { buildSpotterSidebarAppInitData, buildSpotterShareConversationAppInitData, buildStarterPromptsAppInitData, buildSpotterAnalystAppInitData } from './spotter-utils';
3
+ import {
4
+ buildSpotterSidebarAppInitData,
5
+ buildSpotterShareConversationAppInitData,
6
+ buildStarterPromptsAppInitData,
7
+ buildSpotterAnalystAppInitData,
8
+ SpotterExperienceVersion,
9
+ } from './spotter-utils';
4
10
  import { getQueryParamString, getFilterQuery, getRuntimeParameters, setParamIfDefined } from '../utils';
5
11
 
6
12
  /**
@@ -437,6 +443,20 @@ export interface SpotterAnalystConfig {
437
443
  * @group Embed components
438
444
  */
439
445
  export interface SpotterEmbedViewConfig extends Omit<BaseViewConfig, 'primaryAction'> {
446
+ /**
447
+ * The Spotter experience version to load. When set to
448
+ * `SpotterExperienceVersion.SPOTTER_2026_11`, the embed loads the updated Spotter
449
+ * surface instead of the default conversation surface.
450
+ * @version SDK: 1.53.0 | ThoughtSpot Cloud: 26.11.0.cl
451
+ * @example
452
+ * ```js
453
+ * const embed = new SpotterEmbed('#tsEmbed', {
454
+ * ... // other options
455
+ * spotterExperienceVersion: SpotterExperienceVersion.SPOTTER_2026_11,
456
+ * });
457
+ * ```
458
+ */
459
+ spotterExperienceVersion?: SpotterExperienceVersion;
440
460
  /**
441
461
  * The ID of the data source object. For example, Model, View, or Table. Spotter uses
442
462
  * this object to query data and generate Answers.
@@ -909,6 +929,7 @@ export class SpotterEmbed extends TsEmbed {
909
929
  }
910
930
 
911
931
  setParamIfDefined(queryParams, Param.UpdatedSpotterExperience, updatedSpotterExperience, true);
932
+ setParamIfDefined(queryParams, Param.SpotterExperienceVersion, this.viewConfig.spotterExperienceVersion);
912
933
 
913
934
  return queryParams;
914
935
  }
@@ -923,15 +944,19 @@ export class SpotterEmbed extends TsEmbed {
923
944
  excludeRuntimeParametersfromURL,
924
945
  sharedConversationId,
925
946
  dataSources,
947
+ spotterExperienceVersion,
926
948
  } = this.viewConfig;
927
949
  // Deep-link into the read-only shared-conversation reader view when a
928
950
  // shared conversation id is supplied (e.g. a recipient landing from a
929
951
  // host-configured CONVERSATION_URL share link); otherwise the normal
930
952
  // Spotter conversation surface.
931
- const path = sharedConversationId
953
+ let path = sharedConversationId
932
954
  ? `insights/conv-assist/s/${encodeURIComponent(sharedConversationId)}`
933
955
  : 'insights/conv-assist';
934
- const queryParams = this.getEmbedParamsObject();
956
+ if (spotterExperienceVersion === SpotterExperienceVersion.SPOTTER_2026_11) {
957
+ path = path.replace('insights/conv-assist', 'insights/spotter');
958
+ }
959
+ const queryParams = this.getUrlQueryParamsObject();
935
960
 
936
961
  let query = '';
937
962
  const queryParamsString = getQueryParamString(queryParams, true);
@@ -0,0 +1,38 @@
1
+ import { buildLiveboardOverrideAppInitData, LiveboardOverride } from './liveboard-override-utils';
2
+
3
+ describe('buildLiveboardOverrideAppInitData', () => {
4
+ const base = { type: 'APP_INIT' } as any;
5
+ const liveboardOverride: LiveboardOverride = {
6
+ dataSourceOverride: [{
7
+ dataSourceIdentifier: 'model-guid',
8
+ filterQuery: [{ tabName: 'Sales', groupName: 'Region', filterQueryToken: 'color = red' }],
9
+ parameterQuery: [{ parameterQueryToken: 'discount = 10' }],
10
+ }],
11
+ };
12
+
13
+ it('returns initData unchanged when liveboardOverride is not provided', () => {
14
+ const result = buildLiveboardOverrideAppInitData(base, {});
15
+ expect(result).toBe(base);
16
+ });
17
+
18
+ it('returns initData unchanged when dataSourceOverride is empty', () => {
19
+ const result = buildLiveboardOverrideAppInitData(base, {
20
+ liveboardOverride: { dataSourceOverride: [] },
21
+ });
22
+ expect(result).toBe(base);
23
+ });
24
+
25
+ it('nests liveboardOverride under embedParams.liveboardOverride', () => {
26
+ const result = buildLiveboardOverrideAppInitData(base, { liveboardOverride });
27
+ expect(result.embedParams?.liveboardOverride).toEqual(liveboardOverride);
28
+ });
29
+
30
+ it('preserves existing embedParams keys', () => {
31
+ const spotterVizConfig = { brandName: 'MyBrand' };
32
+ const result = buildLiveboardOverrideAppInitData(
33
+ { ...base, embedParams: { spotterVizConfig } },
34
+ { liveboardOverride },
35
+ );
36
+ expect(result.embedParams).toEqual({ spotterVizConfig, liveboardOverride });
37
+ });
38
+ });
@@ -0,0 +1,71 @@
1
+ import type { DefaultAppInitData } from '../types';
2
+
3
+ /**
4
+ * A filter to apply to a Liveboard when it loads.
5
+ * @version SDK: 1.54.0 | ThoughtSpot Cloud: 26.11.0.cl
6
+ * @group Embed components
7
+ */
8
+ export interface LiveboardFilterQuery {
9
+ /** Name of the Liveboard tab the filter applies to. */
10
+ tabName?: string;
11
+ /** Name of the group within the tab that the filter applies to. */
12
+ groupName?: string;
13
+ /**
14
+ * The filter condition, in search sentence form. For example, `color = red`.
15
+ */
16
+ filterQueryToken: string;
17
+ }
18
+
19
+ /**
20
+ * A Parameter value to apply to a Liveboard when it loads.
21
+ * @version SDK: 1.54.0 | ThoughtSpot Cloud: 26.11.0.cl
22
+ * @group Embed components
23
+ */
24
+ export interface LiveboardParameterQuery {
25
+ /** Name of the Liveboard tab the Parameter applies to. */
26
+ tabName?: string;
27
+ /** Name of the group within the tab that the Parameter applies to. */
28
+ groupName?: string;
29
+ /** The Parameter assignment. */
30
+ parameterQueryToken: string;
31
+ }
32
+
33
+ /**
34
+ * Filter and Parameter overrides for one data source on the Liveboard.
35
+ * @version SDK: 1.54.0 | ThoughtSpot Cloud: 26.11.0.cl
36
+ * @group Embed components
37
+ */
38
+ export interface LiveboardDataSourceOverride {
39
+ /** The data source (Model) the overrides apply to. */
40
+ dataSourceIdentifier?: string;
41
+ /** Filters to apply for this data source. */
42
+ filterQuery?: LiveboardFilterQuery[];
43
+ /** Parameter values to apply for this data source. */
44
+ parameterQuery?: LiveboardParameterQuery[];
45
+ }
46
+
47
+ /**
48
+ * Filter and Parameter overrides applied to a Liveboard when it loads.
49
+ * @version SDK: 1.54.0 | ThoughtSpot Cloud: 26.11.0.cl
50
+ * @group Embed components
51
+ */
52
+ export interface LiveboardOverride {
53
+ /** One entry per data source to override. */
54
+ dataSourceOverride: LiveboardDataSourceOverride[];
55
+ }
56
+
57
+ export function buildLiveboardOverrideAppInitData<T extends DefaultAppInitData>(
58
+ initData: T,
59
+ viewConfig: { liveboardOverride?: LiveboardOverride },
60
+ ): T & { embedParams?: { liveboardOverride?: LiveboardOverride } } {
61
+ const { liveboardOverride } = viewConfig;
62
+ // ThoughtSpot ignores an override with no data source entries; skip it.
63
+ if (!liveboardOverride?.dataSourceOverride?.length) return initData;
64
+ return {
65
+ ...initData,
66
+ embedParams: {
67
+ ...((initData as T & { embedParams?: Record<string, unknown> }).embedParams || {}),
68
+ liveboardOverride,
69
+ },
70
+ };
71
+ }