@constructor-io/constructorio-ui-asa 1.7.2 → 1.9.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 (76) hide show
  1. package/README.md +21 -17
  2. package/dist/constructorio-ui-asa-bundled.js +17 -17
  3. package/lib/cjs/components/Button/Button.js +22 -3
  4. package/lib/cjs/components/Chat/AiMessage.js +2 -3
  5. package/lib/cjs/components/Chat/Chat.js +50 -9
  6. package/lib/cjs/components/CioAsa/CioAsa.js +6 -2
  7. package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +44 -3
  8. package/lib/cjs/components/ResultsBlock/ResultsGroup.js +5 -5
  9. package/lib/cjs/hooks/agentStream.js +113 -0
  10. package/lib/cjs/hooks/asaStreamHandlers.js +2 -1
  11. package/lib/cjs/hooks/persistence/backgroundTurns.js +121 -0
  12. package/lib/cjs/hooks/persistence/useChatPersistence.js +485 -0
  13. package/lib/cjs/hooks/persistence/useForeignStream.js +34 -0
  14. package/lib/cjs/hooks/persistence/useThreadList.js +45 -0
  15. package/lib/cjs/hooks/persistence/useWriteQueue.js +17 -0
  16. package/lib/cjs/hooks/useAsaResults.js +119 -159
  17. package/lib/cjs/hooks/useAsaTracking.js +33 -18
  18. package/lib/cjs/hooks/useIsMounted.js +15 -0
  19. package/lib/cjs/hooks/useLatest.js +10 -0
  20. package/lib/cjs/hooks/useViewportTracking.js +4 -3
  21. package/lib/cjs/index.js +3 -1
  22. package/lib/cjs/utils/chatRecordValidation.js +54 -0
  23. package/lib/cjs/utils/chatSession.js +98 -0
  24. package/lib/cjs/utils/chatThreads.js +248 -0
  25. package/lib/cjs/utils/clientOptions.js +11 -0
  26. package/lib/cjs/utils/localStoragePersistence.js +391 -0
  27. package/lib/cjs/utils/translate.js +1 -0
  28. package/lib/cjs/version.js +1 -1
  29. package/lib/mjs/components/Button/Button.js +26 -3
  30. package/lib/mjs/components/Chat/AiMessage.js +2 -3
  31. package/lib/mjs/components/Chat/Chat.js +48 -9
  32. package/lib/mjs/components/CioAsa/CioAsa.js +6 -2
  33. package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +44 -4
  34. package/lib/mjs/components/ResultsBlock/ResultsGroup.js +2 -1
  35. package/lib/mjs/hooks/agentStream.js +106 -0
  36. package/lib/mjs/hooks/asaStreamHandlers.js +1 -1
  37. package/lib/mjs/hooks/persistence/backgroundTurns.js +120 -0
  38. package/lib/mjs/hooks/persistence/useChatPersistence.js +474 -0
  39. package/lib/mjs/hooks/persistence/useForeignStream.js +31 -0
  40. package/lib/mjs/hooks/persistence/useThreadList.js +41 -0
  41. package/lib/mjs/hooks/persistence/useWriteQueue.js +14 -0
  42. package/lib/mjs/hooks/useAsaResults.js +113 -156
  43. package/lib/mjs/hooks/useAsaTracking.js +42 -17
  44. package/lib/mjs/hooks/useIsMounted.js +12 -0
  45. package/lib/mjs/hooks/useLatest.js +7 -0
  46. package/lib/mjs/hooks/useViewportTracking.js +3 -3
  47. package/lib/mjs/index.js +1 -0
  48. package/lib/mjs/utils/chatRecordValidation.js +51 -0
  49. package/lib/mjs/utils/chatSession.js +86 -0
  50. package/lib/mjs/utils/chatThreads.js +219 -0
  51. package/lib/mjs/utils/clientOptions.js +7 -0
  52. package/lib/mjs/utils/localStoragePersistence.js +373 -0
  53. package/lib/mjs/utils/translate.js +1 -0
  54. package/lib/mjs/version.js +1 -1
  55. package/lib/types/components/Button/Button.d.ts +3 -2
  56. package/lib/types/components/Chat/Chat.d.ts +20 -2
  57. package/lib/types/components/CioAsa/CioAsa.d.ts +1 -1
  58. package/lib/types/hooks/agentStream.d.ts +48 -0
  59. package/lib/types/hooks/asaStreamHandlers.d.ts +1 -0
  60. package/lib/types/hooks/persistence/backgroundTurns.d.ts +43 -0
  61. package/lib/types/hooks/persistence/useChatPersistence.d.ts +34 -0
  62. package/lib/types/hooks/persistence/useForeignStream.d.ts +9 -0
  63. package/lib/types/hooks/persistence/useThreadList.d.ts +9 -0
  64. package/lib/types/hooks/persistence/useWriteQueue.d.ts +5 -0
  65. package/lib/types/hooks/useAsaTracking.d.ts +6 -2
  66. package/lib/types/hooks/useIsMounted.d.ts +3 -0
  67. package/lib/types/hooks/useLatest.d.ts +3 -0
  68. package/lib/types/index.d.ts +3 -2
  69. package/lib/types/types.d.ts +170 -5
  70. package/lib/types/utils/chatRecordValidation.d.ts +2 -0
  71. package/lib/types/utils/chatSession.d.ts +45 -0
  72. package/lib/types/utils/chatThreads.d.ts +63 -0
  73. package/lib/types/utils/clientOptions.d.ts +5 -0
  74. package/lib/types/utils/localStoragePersistence.d.ts +26 -0
  75. package/lib/types/version.d.ts +1 -1
  76. package/package.json +2 -2
@@ -3,189 +3,143 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = require("react");
5
5
  const useCioAsaContext_1 = require("./useCioAsaContext");
6
- const asaStreamHandlers_1 = require("./asaStreamHandlers");
7
6
  const useAsaTracking_1 = tslib_1.__importDefault(require("./useAsaTracking"));
7
+ const useChatPersistence_1 = tslib_1.__importDefault(require("./persistence/useChatPersistence"));
8
+ const agentStream_1 = require("./agentStream");
9
+ const chatSession_1 = require("../utils/chatSession");
10
+ const productNormalizer_1 = require("../utils/productNormalizer");
11
+ const chatThreads_1 = require("../utils/chatThreads");
12
+ const useLatest_1 = tslib_1.__importDefault(require("./useLatest"));
8
13
  function useAsaResults(options) {
9
- var _a, _b;
10
- const [messages, setMessages] = (0, react_1.useState)([]);
11
- const [isStreaming, setIsStreaming] = (0, react_1.useState)(false);
12
- const isStreamingRef = (0, react_1.useRef)(false);
13
- const killSwitchRef = (0, react_1.useRef)(false);
14
- const readerRef = (0, react_1.useRef)(null);
15
- const threadIdRef = (0, react_1.useRef)((_a = options === null || options === void 0 ? void 0 : options.initialThreadId) !== null && _a !== void 0 ? _a : null);
16
- const idCounterRef = (0, react_1.useRef)(0);
17
- // The assistant message the in-flight stream is writing into, so `abort` can
18
- // settle it without having to guess which message that is.
19
- const activeAssistantIdRef = (0, react_1.useRef)(null);
14
+ var _a;
20
15
  const contextValue = (0, useCioAsaContext_1.useCioAsaContext)();
21
16
  if (!contextValue) {
22
17
  throw new Error('useAsaResults must be used within a CioAsaProvider.');
23
18
  }
24
- const { cioClient, staticRequestConfigs, callbacks, section } = contextValue;
19
+ const { cioClient, staticRequestConfigs, callbacks, section, persistence, persistenceScope, persistenceIndex, } = contextValue;
25
20
  const { domain } = staticRequestConfigs || {};
26
21
  if (!cioClient || !domain) {
27
22
  throw new Error('useAsaResults requires a configured cioClient and domain. Check your CioAsaProvider props.');
28
23
  }
24
+ const [messages, setMessages] = (0, react_1.useState)([]);
25
+ const [isStreaming, setIsStreaming] = (0, react_1.useState)(false);
26
+ const normalizeItemRef = (0, useLatest_1.default)(options === null || options === void 0 ? void 0 : options.normalizeItem);
27
+ const sessionRef = (0, react_1.useRef)(null);
28
+ if (!sessionRef.current) {
29
+ sessionRef.current = (0, chatSession_1.createChatSession)(options === null || options === void 0 ? void 0 : options.initialThreadId, (item) => { var _a; return ((_a = normalizeItemRef.current) !== null && _a !== void 0 ? _a : productNormalizer_1.normalizeItemToProduct)(item); });
30
+ }
31
+ const session = sessionRef.current;
32
+ const turnRef = (0, react_1.useRef)(null);
29
33
  const tracking = (0, useAsaTracking_1.default)({
30
34
  tracker: cioClient.tracker,
31
35
  section,
32
- threadId: (_b = threadIdRef.current) !== null && _b !== void 0 ? _b : undefined,
36
+ threadId: (_a = session.serverThreadId) !== null && _a !== void 0 ? _a : undefined,
33
37
  });
34
- const trackingRef = (0, react_1.useRef)(tracking);
35
- trackingRef.current = tracking;
36
- const callbacksRef = (0, react_1.useRef)(callbacks);
37
- callbacksRef.current = callbacks;
38
- const staticRequestConfigsRef = (0, react_1.useRef)(staticRequestConfigs);
39
- staticRequestConfigsRef.current = staticRequestConfigs;
40
- /**
41
- * Stops the in-flight stream. The kill switch is raised *before* cancelling because
42
- * cancelling makes the pending `reader.read()` resolve `{ done: true }` — identical
43
- * to a stream that ended on its own. Without the flag already set, the read loop
44
- * would take the end-of-stream branch and report a load that never finished.
45
- *
46
- * Deliberately touches no state, so the unmount cleanup can share it.
47
- */
48
- const teardownStream = (0, react_1.useCallback)(() => {
49
- killSwitchRef.current = true;
50
- if (readerRef.current) {
51
- readerRef.current.cancel();
52
- readerRef.current = null;
53
- }
38
+ const trackingRef = (0, useLatest_1.default)(tracking);
39
+ const callbacksRef = (0, useLatest_1.default)(callbacks);
40
+ const staticRequestConfigsRef = (0, useLatest_1.default)(staticRequestConfigs);
41
+ /** Stops this tab's stream, if any. `readAgentStream` settles its `done` without touching state. */
42
+ const cancelStream = (0, react_1.useCallback)(() => {
43
+ var _a;
44
+ (_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.cancel();
45
+ turnRef.current = null;
54
46
  }, []);
55
- const nextMessageId = (0, react_1.useCallback)(() => {
56
- idCounterRef.current += 1;
57
- return `msg-${idCounterRef.current}-${Date.now()}`;
58
- }, []);
59
- const sendMessage = (0, react_1.useCallback)((text, source = 'input') => {
47
+ const store = (0, useChatPersistence_1.default)({
48
+ store: persistence,
49
+ scope: persistenceScope,
50
+ index: persistenceIndex,
51
+ session,
52
+ initialThreadId: options === null || options === void 0 ? void 0 : options.initialThreadId,
53
+ messages,
54
+ setMessages,
55
+ isStreaming,
56
+ setIsStreaming,
57
+ turnRef,
58
+ cancelStream,
59
+ });
60
+ const { beginTurn, screenOwner } = store;
61
+ (0, react_1.useEffect)(() => cancelStream, [cancelStream]);
62
+ const sendToCurrentThread = (0, react_1.useCallback)((text, source) => {
60
63
  var _a, _b;
61
64
  const intent = text.trim();
62
- if (!intent || isStreamingRef.current)
65
+ if (!intent || session.isStreaming || session.foreignInFlight)
63
66
  return;
64
- trackingRef.current.trackSubmit(intent);
67
+ beginTurn();
68
+ trackingRef.current.trackSubmit(intent, source);
65
69
  (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAssistantSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, { intent, source });
66
70
  const userMessage = {
67
- id: nextMessageId(),
71
+ id: (0, chatSession_1.nextMessageId)(session),
68
72
  role: 'user',
69
73
  text: intent,
70
74
  status: 'done',
71
75
  };
72
76
  const assistantMessage = {
73
- id: nextMessageId(),
77
+ id: (0, chatSession_1.nextMessageId)(session),
74
78
  role: 'assistant',
75
79
  text: '',
76
80
  groups: [],
77
81
  status: 'loading',
78
82
  intent,
79
83
  };
80
- activeAssistantIdRef.current = assistantMessage.id;
81
84
  setMessages((prev) => [...prev, userMessage, assistantMessage]);
82
85
  setIsStreaming(true);
83
- isStreamingRef.current = true;
84
- killSwitchRef.current = false;
85
- // `intent` is passed as the first argument and `threadId` is managed per-stream
86
- // below, so both are stripped here. Everything else configured on the provider
87
- // (filters, guard, numResultsPerEvent, numResultEvents, qsParam,
88
- // preFilterExpression, fmtOptions, ...) is forwarded to the agent as-is.
86
+ session.isStreaming = true;
89
87
  const agentParams = Object.assign({}, staticRequestConfigsRef.current);
90
88
  delete agentParams.intent;
91
89
  delete agentParams.threadId;
92
- const stream = cioClient.agent.getAgentResultsStream(intent, Object.assign(Object.assign(Object.assign({}, agentParams), { domain }), (threadIdRef.current && { threadId: threadIdRef.current })));
93
- const reader = stream.getReader();
94
- readerRef.current = reader;
95
- // Mutable per-stream state kept on an object so the closures below don't capture
96
- // reassigned loop-locals (which eslint's no-loop-func forbids).
97
- const streamState = {
98
- pendingGroup: null,
99
- intentResultId: undefined,
100
- loadStartFired: false,
101
- groupCount: 0,
102
- };
103
- const patchAssistant = (patch) => {
104
- setMessages((prev) => prev.map((m) => (m.id === assistantMessage.id ? Object.assign(Object.assign({}, m), patch) : m)));
105
- };
106
- const fireLoadStart = () => {
107
- var _a, _b;
108
- if (streamState.loadStartFired)
109
- return;
110
- streamState.loadStartFired = true;
111
- const args = { intent, intentResultId: streamState.intentResultId };
112
- trackingRef.current.trackResultLoadStarted(args);
113
- (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onResultLoadStart) === null || _b === void 0 ? void 0 : _b.call(_a, args);
114
- };
90
+ const stream = cioClient.agent.getAgentResultsStream(intent, Object.assign(Object.assign(Object.assign({}, agentParams), { domain }), (session.serverThreadId && { threadId: session.serverThreadId })));
91
+ turnRef.current = (0, agentStream_1.startTurn)(stream, assistantMessage, screenOwner, {
92
+ onLoadStart: (intentResultId) => {
93
+ var _a, _b;
94
+ const args = { intent, intentResultId };
95
+ trackingRef.current.trackResultLoadStarted(args);
96
+ (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onResultLoadStart) === null || _b === void 0 ? void 0 : _b.call(_a, args);
97
+ },
98
+ onFinish: (_a) => {
99
+ var _b, _c;
100
+ var { threadId } = _a, result = tslib_1.__rest(_a, ["threadId"]);
101
+ const args = Object.assign({ intent }, result);
102
+ // The turn may have finished in the background, after the chat moved to another thread.
103
+ trackingRef.current.trackResultLoadFinished(Object.assign(Object.assign({}, args), { threadId }));
104
+ (_c = (_b = callbacksRef.current) === null || _b === void 0 ? void 0 : _b.onResultLoadFinish) === null || _c === void 0 ? void 0 : _c.call(_b, args);
105
+ },
106
+ });
107
+ }, [
108
+ cioClient,
109
+ domain,
110
+ session,
111
+ beginTurn,
112
+ screenOwner,
113
+ trackingRef,
114
+ callbacksRef,
115
+ staticRequestConfigsRef,
116
+ ]);
117
+ const { newThread, switchThread } = store;
118
+ /**
119
+ * Sends `text`. With `threadId`, first moves to that conversation, loading it from storage
120
+ * when persistence has it and otherwise continuing it on the server with an empty screen.
121
+ */
122
+ const sendMessage = (0, react_1.useCallback)((text, source = 'input', { threadId } = {}) => {
123
+ if (!threadId ||
124
+ threadId === session.serverThreadId ||
125
+ threadId === session.storageThreadId) {
126
+ sendToCurrentThread(text, source);
127
+ return;
128
+ }
115
129
  (() => tslib_1.__awaiter(this, void 0, void 0, function* () {
116
- var _c, _d, _e, _f, _g, _h;
117
- try {
118
- // eslint-disable-next-line no-await-in-loop
119
- while (!killSwitchRef.current) {
120
- // eslint-disable-next-line no-await-in-loop
121
- const res = yield reader.read();
122
- if (killSwitchRef.current)
123
- break;
124
- if (res.done) {
125
- (0, asaStreamHandlers_1.handleStreamEnd)(assistantMessage.id, setMessages);
126
- const finishArgs = {
127
- intent,
128
- searchResultCount: streamState.groupCount,
129
- intentResultId: streamState.intentResultId,
130
- };
131
- trackingRef.current.trackResultLoadFinished(finishArgs);
132
- (_d = (_c = callbacksRef.current) === null || _c === void 0 ? void 0 : _c.onResultLoadFinish) === null || _d === void 0 ? void 0 : _d.call(_c, finishArgs);
133
- break;
134
- }
135
- const { type, data } = res.value;
136
- // `intent_result_id` is shared across the stream; capture it from the first
137
- // event that carries it so load-start/finish can attribute correctly.
138
- if (!streamState.intentResultId && (data === null || data === void 0 ? void 0 : data.intent_result_id)) {
139
- streamState.intentResultId = data.intent_result_id;
140
- patchAssistant({ intentResultId: streamState.intentResultId });
141
- }
142
- if (type === 'start') {
143
- if (data === null || data === void 0 ? void 0 : data.thread_id) {
144
- threadIdRef.current = data.thread_id;
145
- patchAssistant({ threadId: data.thread_id });
146
- }
147
- fireLoadStart();
148
- }
149
- else if (type === 'group') {
150
- streamState.pendingGroup = {
151
- display_name: (_f = (_e = data === null || data === void 0 ? void 0 : data.display_name) !== null && _e !== void 0 ? _e : data === null || data === void 0 ? void 0 : data.group) !== null && _f !== void 0 ? _f : '',
152
- value: (_h = (_g = data === null || data === void 0 ? void 0 : data.value) !== null && _g !== void 0 ? _g : data === null || data === void 0 ? void 0 : data.group) !== null && _h !== void 0 ? _h : '',
153
- };
154
- }
155
- else if (type === 'search_result') {
156
- fireLoadStart();
157
- streamState.groupCount += 1;
158
- streamState.pendingGroup = (0, asaStreamHandlers_1.handleSearchResult)(data, streamState.pendingGroup, assistantMessage.id, setMessages);
159
- }
160
- else if (type === 'message') {
161
- fireLoadStart();
162
- (0, asaStreamHandlers_1.handleMessage)(data, assistantMessage.id, setMessages);
163
- }
164
- else if (type === 'follow_up_refinement') {
165
- fireLoadStart();
166
- (0, asaStreamHandlers_1.handleFollowUpRefinement)(data, assistantMessage.id, setMessages);
167
- }
168
- else if (type === 'server_error') {
169
- (0, asaStreamHandlers_1.handleServerError)(assistantMessage.id, setMessages);
170
- break;
171
- }
172
- }
173
- }
174
- catch (_j) {
175
- (0, asaStreamHandlers_1.handleStreamError)(assistantMessage.id, setMessages);
176
- }
177
- finally {
178
- if (readerRef.current === reader) {
179
- reader.cancel();
180
- readerRef.current = null;
181
- activeAssistantIdRef.current = null;
182
- setIsStreaming(false);
183
- isStreamingRef.current = false;
184
- }
130
+ if (persistence)
131
+ yield switchThread(threadId);
132
+ else
133
+ newThread();
134
+ // A turn that started meanwhile (or one still streaming in the background) owns the screen.
135
+ if (session.isStreaming || session.foreignInFlight)
136
+ return;
137
+ if (!session.serverThreadId && !(0, chatThreads_1.isLocalThreadId)(threadId)) {
138
+ session.serverThreadId = threadId;
185
139
  }
140
+ sendToCurrentThread(text, source);
186
141
  }))();
187
- }, [cioClient, domain, nextMessageId]);
188
- (0, react_1.useEffect)(() => () => teardownStream(), [teardownStream]);
142
+ }, [session, persistence, newThread, switchThread, sendToCurrentThread]);
189
143
  /**
190
144
  * Cancels the in-flight request while keeping the conversation: the partial reply is
191
145
  * settled as `done` (so the typing indicator stops and streamed text survives) and the
@@ -202,13 +156,13 @@ function useAsaResults(options) {
202
156
  * `assistant_result_load_started` with no matching finished event.
203
157
  */
204
158
  const abort = (0, react_1.useCallback)(() => {
205
- if (!isStreamingRef.current)
159
+ var _a;
160
+ if (!session.isStreaming)
206
161
  return;
207
- teardownStream();
162
+ const abortedId = (_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.assistantId;
163
+ cancelStream();
164
+ session.isStreaming = false;
208
165
  setIsStreaming(false);
209
- isStreamingRef.current = false;
210
- const abortedId = activeAssistantIdRef.current;
211
- activeAssistantIdRef.current = null;
212
166
  if (abortedId) {
213
167
  setMessages((prev) => prev.flatMap((msg) => {
214
168
  var _a;
@@ -219,15 +173,21 @@ function useAsaResults(options) {
219
173
  return [Object.assign(Object.assign({}, msg), { status: 'done' })];
220
174
  }));
221
175
  }
222
- }, [teardownStream]);
223
- const clearHistory = (0, react_1.useCallback)(() => {
224
- teardownStream();
225
- threadIdRef.current = null;
226
- activeAssistantIdRef.current = null;
227
- setMessages([]);
228
- setIsStreaming(false);
229
- isStreamingRef.current = false;
230
- }, [teardownStream]);
231
- return { messages, sendMessage, isStreaming, abort, clearHistory };
176
+ // With persistence on, the cancelled turn is stored as it stands.
177
+ session.dirty = true;
178
+ }, [session, cancelStream]);
179
+ return {
180
+ messages,
181
+ sendMessage,
182
+ isStreaming: isStreaming || store.foreignInFlight,
183
+ canAbort: isStreaming,
184
+ abort,
185
+ clearHistory: store.clearHistory,
186
+ isHydrating: store.isHydrating,
187
+ threads: store.threads,
188
+ activeThreadId: store.activeThreadId,
189
+ newThread,
190
+ switchThread,
191
+ };
232
192
  }
233
193
  exports.default = useAsaResults;
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
+ /** Beacon `source` values, aligned with the ones the behavioral-actions API documents. */
5
+ const BEACON_SUBMIT_SOURCES = {
6
+ input: 'input',
7
+ suggestion: 'suggestion',
8
+ refinement: 'follow_up',
9
+ // A prompt the host page sent, typically a suggested question clicked outside the chat.
10
+ external: 'suggestion',
11
+ };
4
12
  const NOOP_TRACKING = {
13
+ trackAgentButtonClick: () => { },
5
14
  trackSubmit: () => { },
6
15
  trackResultLoadStarted: () => { },
7
16
  trackResultLoadFinished: () => { },
@@ -15,37 +24,42 @@ const NOOP_TRACKING = {
15
24
  * Mirrors the tracking-hook pattern used by constructorio-ui-pia.
16
25
  */
17
26
  function useAsaTracking({ tracker, section, threadId, }) {
18
- const assistant = tracker;
19
27
  const base = (0, react_1.useMemo)(() => (Object.assign(Object.assign({}, (section && { section })), (threadId && { threadId }))), [section, threadId]);
20
- const trackSubmit = (0, react_1.useCallback)((intent) => {
21
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantSubmit(Object.assign({ intent }, base));
22
- }, [assistant, base]);
28
+ const trackAgentButtonClick = (0, react_1.useCallback)(({ mode, agentDomain, positionOnPage, pageType, instanceId }) => {
29
+ var _a;
30
+ (_a = tracker === null || tracker === void 0 ? void 0 : tracker.trackAgentButtonClick) === null || _a === void 0 ? void 0 : _a.call(tracker, Object.assign(Object.assign(Object.assign(Object.assign({ mode,
31
+ agentDomain }, (positionOnPage && { positionOnPage })), (pageType && { pageType })), (instanceId && { instanceId })), (section && { section })));
32
+ }, [tracker, section]);
33
+ const trackSubmit = (0, react_1.useCallback)((intent, source) => {
34
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantSubmit(Object.assign(Object.assign({ intent }, (source && { source: BEACON_SUBMIT_SOURCES[source] })), base));
35
+ }, [tracker, base]);
23
36
  const trackResultLoadStarted = (0, react_1.useCallback)(({ intent, intentResultId }) => {
24
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadStarted(Object.assign(Object.assign({ intent }, (intentResultId && { intentResultId })), base));
25
- }, [assistant, base]);
26
- const trackResultLoadFinished = (0, react_1.useCallback)(({ intent, searchResultCount, intentResultId }) => {
27
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadFinished(Object.assign(Object.assign({ intent,
28
- searchResultCount }, (intentResultId && { intentResultId })), base));
29
- }, [assistant, base]);
37
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultLoadStarted(Object.assign(Object.assign({ intent }, (intentResultId && { intentResultId })), base));
38
+ }, [tracker, base]);
39
+ const trackResultLoadFinished = (0, react_1.useCallback)(({ intent, searchResultCount, intentResultId, threadId: turnThreadId, }) => {
40
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultLoadFinished(Object.assign(Object.assign(Object.assign({ intent,
41
+ searchResultCount }, (intentResultId && { intentResultId })), base), (turnThreadId && { threadId: turnThreadId })));
42
+ }, [tracker, base]);
30
43
  const trackResultClick = (0, react_1.useCallback)(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
31
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultClick(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ intent,
44
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultClick(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ intent,
32
45
  searchResultId }, (intentResultId && { intentResultId })), (itemId && { itemId })), (itemName && { itemName })), (variationId && { variationId })), base));
33
- }, [assistant, base]);
46
+ }, [tracker, base]);
34
47
  const trackResultView = (0, react_1.useCallback)(({ intent, searchResultId, numResultsViewed, intentResultId, items }) => {
35
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultView(Object.assign(Object.assign(Object.assign({ intent,
48
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultView(Object.assign(Object.assign(Object.assign({ intent,
36
49
  searchResultId,
37
50
  numResultsViewed }, (intentResultId && { intentResultId })), (items && items.length > 0 && { items })), base));
38
- }, [assistant, base]);
51
+ }, [tracker, base]);
39
52
  const trackSearchSubmit = (0, react_1.useCallback)(({ intent, searchTerm, userInput, searchResultId, intentResultId, groupId, }) => {
40
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantSearchSubmit(Object.assign(Object.assign(Object.assign({ intent,
53
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantSearchSubmit(Object.assign(Object.assign(Object.assign({ intent,
41
54
  searchTerm,
42
55
  userInput,
43
56
  searchResultId }, (intentResultId && { intentResultId })), (groupId && { groupId })), base));
44
- }, [assistant, base]);
57
+ }, [tracker, base]);
45
58
  return (0, react_1.useMemo)(() => {
46
- if (!assistant)
59
+ if (!tracker)
47
60
  return NOOP_TRACKING;
48
61
  return {
62
+ trackAgentButtonClick,
49
63
  trackSubmit,
50
64
  trackResultLoadStarted,
51
65
  trackResultLoadFinished,
@@ -54,7 +68,8 @@ function useAsaTracking({ tracker, section, threadId, }) {
54
68
  trackSearchSubmit,
55
69
  };
56
70
  }, [
57
- assistant,
71
+ tracker,
72
+ trackAgentButtonClick,
58
73
  trackSubmit,
59
74
  trackResultLoadStarted,
60
75
  trackResultLoadFinished,
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ /** A ref that is `true` while mounted, for async work that must not land after unmount. */
5
+ function useIsMounted() {
6
+ const ref = (0, react_1.useRef)(true);
7
+ (0, react_1.useEffect)(() => {
8
+ ref.current = true;
9
+ return () => {
10
+ ref.current = false;
11
+ };
12
+ }, []);
13
+ return ref;
14
+ }
15
+ exports.default = useIsMounted;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ /** A ref that always holds the latest `value`, for async code that must not read stale props. */
5
+ function useLatest(value) {
6
+ const ref = (0, react_1.useRef)(value);
7
+ ref.current = value;
8
+ return ref;
9
+ }
10
+ exports.default = useLatest;
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
3
4
  const react_1 = require("react");
5
+ const useLatest_1 = tslib_1.__importDefault(require("./useLatest"));
4
6
  /**
5
7
  * Fires `onView` once when the observed element scrolls into the viewport, modeling a
6
8
  * per-pod `assistant_search_result_view` impression. Unlike the PIA widget-level view
7
9
  * tracker, this fires a single impression per pod (not repeated enter/exit cycles).
8
10
  */
9
11
  function useViewportTracking({ onView, enabled = true, }) {
10
- const onViewRef = (0, react_1.useRef)(onView);
11
- onViewRef.current = onView;
12
+ const onViewRef = (0, useLatest_1.default)(onView);
12
13
  const firedRef = (0, react_1.useRef)(false);
13
14
  const cleanupRef = (0, react_1.useRef)(null);
14
15
  const ref = (0, react_1.useCallback)((node) => {
@@ -29,7 +30,7 @@ function useViewportTracking({ onView, enabled = true, }) {
29
30
  }, { threshold: 0.5 });
30
31
  observer.observe(node);
31
32
  cleanupRef.current = () => observer.disconnect();
32
- }, [enabled]);
33
+ }, [enabled, onViewRef]);
33
34
  (0, react_1.useEffect)(() => () => {
34
35
  if (cleanupRef.current)
35
36
  cleanupRef.current();
package/lib/cjs/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ConstructorIOClient = exports.normalizeItemToProduct = exports.useCioAsaContext = exports.useAsaTracking = exports.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = exports.CioAsa = void 0;
6
+ exports.ConstructorIOClient = exports.clearPersistedConversations = exports.normalizeItemToProduct = exports.useCioAsaContext = exports.useAsaTracking = exports.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = exports.CioAsa = void 0;
7
7
  // Components
8
8
  var CioAsa_1 = require("./components/CioAsa/CioAsa");
9
9
  Object.defineProperty(exports, "CioAsa", { enumerable: true, get: function () { return __importDefault(CioAsa_1).default; } });
@@ -25,6 +25,8 @@ Object.defineProperty(exports, "useCioAsaContext", { enumerable: true, get: func
25
25
  // Utils
26
26
  var productNormalizer_1 = require("./utils/productNormalizer");
27
27
  Object.defineProperty(exports, "normalizeItemToProduct", { enumerable: true, get: function () { return productNormalizer_1.normalizeItemToProduct; } });
28
+ var localStoragePersistence_1 = require("./utils/localStoragePersistence");
29
+ Object.defineProperty(exports, "clearPersistedConversations", { enumerable: true, get: function () { return localStoragePersistence_1.clearPersistedConversations; } });
28
30
  // Re-exported client value from the JS client (used to construct/type a cioClient)
29
31
  var types_1 = require("./types");
30
32
  Object.defineProperty(exports, "ConstructorIOClient", { enumerable: true, get: function () { return types_1.ConstructorIOClient; } });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chatThreads_1 = require("./chatThreads");
4
+ const MESSAGE_ROLES = new Set(['user', 'assistant']);
5
+ const MESSAGE_STATUSES = new Set(['idle', 'loading', 'streaming', 'done', 'error']);
6
+ function isProduct(value) {
7
+ if (!value || typeof value !== 'object')
8
+ return false;
9
+ const p = value;
10
+ return typeof p.id === 'string' && typeof p.name === 'string';
11
+ }
12
+ function isResultGroup(value) {
13
+ if (!value || typeof value !== 'object')
14
+ return false;
15
+ const g = value;
16
+ return (Boolean(g.group) &&
17
+ typeof g.group === 'object' &&
18
+ typeof g.group.display_name === 'string' &&
19
+ Array.isArray(g.searchResults) &&
20
+ g.searchResults.every((r) => Boolean(r) && typeof r === 'object') &&
21
+ (g.products === undefined || (Array.isArray(g.products) && g.products.every(isProduct))));
22
+ }
23
+ function isRefinement(value) {
24
+ if (!value || typeof value !== 'object')
25
+ return false;
26
+ const r = value;
27
+ return (typeof r.question === 'string' &&
28
+ Array.isArray(r.options) &&
29
+ r.options.every((o) => typeof o === 'string'));
30
+ }
31
+ function isChatMessage(value) {
32
+ if (!value || typeof value !== 'object')
33
+ return false;
34
+ const m = value;
35
+ return (typeof m.id === 'string' &&
36
+ MESSAGE_ROLES.has(m.role) &&
37
+ typeof m.text === 'string' &&
38
+ MESSAGE_STATUSES.has(m.status) &&
39
+ (m.groups === undefined || (Array.isArray(m.groups) && m.groups.every(isResultGroup))) &&
40
+ (m.refinement === undefined || isRefinement(m.refinement)));
41
+ }
42
+ function isPersistedChat(value) {
43
+ if (!value || typeof value !== 'object')
44
+ return false;
45
+ const chat = value;
46
+ return (chat.version === chatThreads_1.PERSISTED_CHAT_VERSION &&
47
+ typeof chat.threadId === 'string' &&
48
+ Array.isArray(chat.messages) &&
49
+ chat.messages.every(isChatMessage) &&
50
+ typeof chat.createdAt === 'number' &&
51
+ typeof chat.updatedAt === 'number' &&
52
+ (chat.owner === undefined || typeof chat.owner === 'string'));
53
+ }
54
+ exports.default = isPersistedChat;