@constructor-io/constructorio-ui-asa 1.7.2 → 1.8.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 (73) hide show
  1. package/README.md +21 -17
  2. package/dist/constructorio-ui-asa-bundled.js +17 -17
  3. package/lib/cjs/components/Chat/AiMessage.js +2 -3
  4. package/lib/cjs/components/Chat/Chat.js +38 -10
  5. package/lib/cjs/components/CioAsa/CioAsa.js +6 -2
  6. package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +44 -3
  7. package/lib/cjs/components/ResultsBlock/ResultsGroup.js +5 -5
  8. package/lib/cjs/hooks/agentStream.js +113 -0
  9. package/lib/cjs/hooks/asaStreamHandlers.js +2 -1
  10. package/lib/cjs/hooks/persistence/backgroundTurns.js +121 -0
  11. package/lib/cjs/hooks/persistence/useChatPersistence.js +485 -0
  12. package/lib/cjs/hooks/persistence/useForeignStream.js +34 -0
  13. package/lib/cjs/hooks/persistence/useThreadList.js +45 -0
  14. package/lib/cjs/hooks/persistence/useWriteQueue.js +17 -0
  15. package/lib/cjs/hooks/useAsaResults.js +93 -160
  16. package/lib/cjs/hooks/useAsaTracking.js +3 -3
  17. package/lib/cjs/hooks/useIsMounted.js +15 -0
  18. package/lib/cjs/hooks/useLatest.js +10 -0
  19. package/lib/cjs/hooks/useViewportTracking.js +4 -3
  20. package/lib/cjs/index.js +3 -1
  21. package/lib/cjs/utils/chatRecordValidation.js +54 -0
  22. package/lib/cjs/utils/chatSession.js +98 -0
  23. package/lib/cjs/utils/chatThreads.js +248 -0
  24. package/lib/cjs/utils/clientOptions.js +11 -0
  25. package/lib/cjs/utils/localStoragePersistence.js +391 -0
  26. package/lib/cjs/utils/translate.js +1 -0
  27. package/lib/cjs/version.js +1 -1
  28. package/lib/mjs/components/Chat/AiMessage.js +2 -3
  29. package/lib/mjs/components/Chat/Chat.js +36 -10
  30. package/lib/mjs/components/CioAsa/CioAsa.js +6 -2
  31. package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +44 -4
  32. package/lib/mjs/components/ResultsBlock/ResultsGroup.js +2 -1
  33. package/lib/mjs/hooks/agentStream.js +106 -0
  34. package/lib/mjs/hooks/asaStreamHandlers.js +1 -1
  35. package/lib/mjs/hooks/persistence/backgroundTurns.js +120 -0
  36. package/lib/mjs/hooks/persistence/useChatPersistence.js +474 -0
  37. package/lib/mjs/hooks/persistence/useForeignStream.js +31 -0
  38. package/lib/mjs/hooks/persistence/useThreadList.js +41 -0
  39. package/lib/mjs/hooks/persistence/useWriteQueue.js +14 -0
  40. package/lib/mjs/hooks/useAsaResults.js +87 -157
  41. package/lib/mjs/hooks/useAsaTracking.js +2 -1
  42. package/lib/mjs/hooks/useIsMounted.js +12 -0
  43. package/lib/mjs/hooks/useLatest.js +7 -0
  44. package/lib/mjs/hooks/useViewportTracking.js +3 -3
  45. package/lib/mjs/index.js +1 -0
  46. package/lib/mjs/utils/chatRecordValidation.js +51 -0
  47. package/lib/mjs/utils/chatSession.js +86 -0
  48. package/lib/mjs/utils/chatThreads.js +219 -0
  49. package/lib/mjs/utils/clientOptions.js +7 -0
  50. package/lib/mjs/utils/localStoragePersistence.js +373 -0
  51. package/lib/mjs/utils/translate.js +1 -0
  52. package/lib/mjs/version.js +1 -1
  53. package/lib/types/components/Chat/Chat.d.ts +8 -2
  54. package/lib/types/components/CioAsa/CioAsa.d.ts +1 -1
  55. package/lib/types/hooks/agentStream.d.ts +48 -0
  56. package/lib/types/hooks/asaStreamHandlers.d.ts +1 -0
  57. package/lib/types/hooks/persistence/backgroundTurns.d.ts +43 -0
  58. package/lib/types/hooks/persistence/useChatPersistence.d.ts +34 -0
  59. package/lib/types/hooks/persistence/useForeignStream.d.ts +9 -0
  60. package/lib/types/hooks/persistence/useThreadList.d.ts +9 -0
  61. package/lib/types/hooks/persistence/useWriteQueue.d.ts +5 -0
  62. package/lib/types/hooks/useAsaTracking.d.ts +2 -0
  63. package/lib/types/hooks/useIsMounted.d.ts +3 -0
  64. package/lib/types/hooks/useLatest.d.ts +3 -0
  65. package/lib/types/index.d.ts +2 -1
  66. package/lib/types/types.d.ts +134 -2
  67. package/lib/types/utils/chatRecordValidation.d.ts +2 -0
  68. package/lib/types/utils/chatSession.d.ts +45 -0
  69. package/lib/types/utils/chatThreads.d.ts +63 -0
  70. package/lib/types/utils/clientOptions.d.ts +5 -0
  71. package/lib/types/utils/localStoragePersistence.d.ts +26 -0
  72. package/lib/types/version.d.ts +1 -1
  73. package/package.json +1 -1
@@ -3,189 +3,116 @@ 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 useLatest_1 = tslib_1.__importDefault(require("./useLatest"));
8
12
  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);
13
+ var _a;
20
14
  const contextValue = (0, useCioAsaContext_1.useCioAsaContext)();
21
15
  if (!contextValue) {
22
16
  throw new Error('useAsaResults must be used within a CioAsaProvider.');
23
17
  }
24
- const { cioClient, staticRequestConfigs, callbacks, section } = contextValue;
18
+ const { cioClient, staticRequestConfigs, callbacks, section, persistence, persistenceScope, persistenceIndex, } = contextValue;
25
19
  const { domain } = staticRequestConfigs || {};
26
20
  if (!cioClient || !domain) {
27
21
  throw new Error('useAsaResults requires a configured cioClient and domain. Check your CioAsaProvider props.');
28
22
  }
23
+ const [messages, setMessages] = (0, react_1.useState)([]);
24
+ const [isStreaming, setIsStreaming] = (0, react_1.useState)(false);
25
+ const normalizeItemRef = (0, useLatest_1.default)(options === null || options === void 0 ? void 0 : options.normalizeItem);
26
+ const sessionRef = (0, react_1.useRef)(null);
27
+ if (!sessionRef.current) {
28
+ 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); });
29
+ }
30
+ const session = sessionRef.current;
31
+ const turnRef = (0, react_1.useRef)(null);
29
32
  const tracking = (0, useAsaTracking_1.default)({
30
33
  tracker: cioClient.tracker,
31
34
  section,
32
- threadId: (_b = threadIdRef.current) !== null && _b !== void 0 ? _b : undefined,
35
+ threadId: (_a = session.serverThreadId) !== null && _a !== void 0 ? _a : undefined,
33
36
  });
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
- }
54
- }, []);
55
- const nextMessageId = (0, react_1.useCallback)(() => {
56
- idCounterRef.current += 1;
57
- return `msg-${idCounterRef.current}-${Date.now()}`;
37
+ const trackingRef = (0, useLatest_1.default)(tracking);
38
+ const callbacksRef = (0, useLatest_1.default)(callbacks);
39
+ const staticRequestConfigsRef = (0, useLatest_1.default)(staticRequestConfigs);
40
+ /** Stops this tab's stream, if any. `readAgentStream` settles its `done` without touching state. */
41
+ const cancelStream = (0, react_1.useCallback)(() => {
42
+ var _a;
43
+ (_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.cancel();
44
+ turnRef.current = null;
58
45
  }, []);
46
+ const store = (0, useChatPersistence_1.default)({
47
+ store: persistence,
48
+ scope: persistenceScope,
49
+ index: persistenceIndex,
50
+ session,
51
+ initialThreadId: options === null || options === void 0 ? void 0 : options.initialThreadId,
52
+ messages,
53
+ setMessages,
54
+ isStreaming,
55
+ setIsStreaming,
56
+ turnRef,
57
+ cancelStream,
58
+ });
59
+ const { beginTurn, screenOwner } = store;
60
+ (0, react_1.useEffect)(() => cancelStream, [cancelStream]);
59
61
  const sendMessage = (0, react_1.useCallback)((text, source = 'input') => {
60
62
  var _a, _b;
61
63
  const intent = text.trim();
62
- if (!intent || isStreamingRef.current)
64
+ if (!intent || session.isStreaming || session.foreignInFlight)
63
65
  return;
66
+ beginTurn();
64
67
  trackingRef.current.trackSubmit(intent);
65
68
  (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAssistantSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, { intent, source });
66
69
  const userMessage = {
67
- id: nextMessageId(),
70
+ id: (0, chatSession_1.nextMessageId)(session),
68
71
  role: 'user',
69
72
  text: intent,
70
73
  status: 'done',
71
74
  };
72
75
  const assistantMessage = {
73
- id: nextMessageId(),
76
+ id: (0, chatSession_1.nextMessageId)(session),
74
77
  role: 'assistant',
75
78
  text: '',
76
79
  groups: [],
77
80
  status: 'loading',
78
81
  intent,
79
82
  };
80
- activeAssistantIdRef.current = assistantMessage.id;
81
83
  setMessages((prev) => [...prev, userMessage, assistantMessage]);
82
84
  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.
85
+ session.isStreaming = true;
89
86
  const agentParams = Object.assign({}, staticRequestConfigsRef.current);
90
87
  delete agentParams.intent;
91
88
  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
- };
115
- (() => 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
- }
185
- }
186
- }))();
187
- }, [cioClient, domain, nextMessageId]);
188
- (0, react_1.useEffect)(() => () => teardownStream(), [teardownStream]);
89
+ const stream = cioClient.agent.getAgentResultsStream(intent, Object.assign(Object.assign(Object.assign({}, agentParams), { domain }), (session.serverThreadId && { threadId: session.serverThreadId })));
90
+ turnRef.current = (0, agentStream_1.startTurn)(stream, assistantMessage, screenOwner, {
91
+ onLoadStart: (intentResultId) => {
92
+ var _a, _b;
93
+ const args = { intent, intentResultId };
94
+ trackingRef.current.trackResultLoadStarted(args);
95
+ (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onResultLoadStart) === null || _b === void 0 ? void 0 : _b.call(_a, args);
96
+ },
97
+ onFinish: (_a) => {
98
+ var _b, _c;
99
+ var { threadId } = _a, result = tslib_1.__rest(_a, ["threadId"]);
100
+ const args = Object.assign({ intent }, result);
101
+ // The turn may have finished in the background, after the chat moved to another thread.
102
+ trackingRef.current.trackResultLoadFinished(Object.assign(Object.assign({}, args), { threadId }));
103
+ (_c = (_b = callbacksRef.current) === null || _b === void 0 ? void 0 : _b.onResultLoadFinish) === null || _c === void 0 ? void 0 : _c.call(_b, args);
104
+ },
105
+ });
106
+ }, [
107
+ cioClient,
108
+ domain,
109
+ session,
110
+ beginTurn,
111
+ screenOwner,
112
+ trackingRef,
113
+ callbacksRef,
114
+ staticRequestConfigsRef,
115
+ ]);
189
116
  /**
190
117
  * Cancels the in-flight request while keeping the conversation: the partial reply is
191
118
  * settled as `done` (so the typing indicator stops and streamed text survives) and the
@@ -202,13 +129,13 @@ function useAsaResults(options) {
202
129
  * `assistant_result_load_started` with no matching finished event.
203
130
  */
204
131
  const abort = (0, react_1.useCallback)(() => {
205
- if (!isStreamingRef.current)
132
+ var _a;
133
+ if (!session.isStreaming)
206
134
  return;
207
- teardownStream();
135
+ const abortedId = (_a = turnRef.current) === null || _a === void 0 ? void 0 : _a.assistantId;
136
+ cancelStream();
137
+ session.isStreaming = false;
208
138
  setIsStreaming(false);
209
- isStreamingRef.current = false;
210
- const abortedId = activeAssistantIdRef.current;
211
- activeAssistantIdRef.current = null;
212
139
  if (abortedId) {
213
140
  setMessages((prev) => prev.flatMap((msg) => {
214
141
  var _a;
@@ -219,15 +146,21 @@ function useAsaResults(options) {
219
146
  return [Object.assign(Object.assign({}, msg), { status: 'done' })];
220
147
  }));
221
148
  }
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 };
149
+ // With persistence on, the cancelled turn is stored as it stands.
150
+ session.dirty = true;
151
+ }, [session, cancelStream]);
152
+ return {
153
+ messages,
154
+ sendMessage,
155
+ isStreaming: isStreaming || store.foreignInFlight,
156
+ canAbort: isStreaming,
157
+ abort,
158
+ clearHistory: store.clearHistory,
159
+ isHydrating: store.isHydrating,
160
+ threads: store.threads,
161
+ activeThreadId: store.activeThreadId,
162
+ newThread: store.newThread,
163
+ switchThread: store.switchThread,
164
+ };
232
165
  }
233
166
  exports.default = useAsaResults;
@@ -23,9 +23,9 @@ function useAsaTracking({ tracker, section, threadId, }) {
23
23
  const trackResultLoadStarted = (0, react_1.useCallback)(({ intent, intentResultId }) => {
24
24
  assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadStarted(Object.assign(Object.assign({ intent }, (intentResultId && { intentResultId })), base));
25
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));
26
+ const trackResultLoadFinished = (0, react_1.useCallback)(({ intent, searchResultCount, intentResultId, threadId: turnThreadId, }) => {
27
+ assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadFinished(Object.assign(Object.assign(Object.assign({ intent,
28
+ searchResultCount }, (intentResultId && { intentResultId })), base), (turnThreadId && { threadId: turnThreadId })));
29
29
  }, [assistant, base]);
30
30
  const trackResultClick = (0, react_1.useCallback)(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
31
31
  assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultClick(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ intent,
@@ -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;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareSnapshot = exports.adoptSession = exports.forkSession = exports.adoptStoredChat = exports.isOwnMessage = exports.nextMessageId = exports.resetConversation = exports.createChatSession = void 0;
4
+ const chatThreads_1 = require("./chatThreads");
5
+ const productNormalizer_1 = require("./productNormalizer");
6
+ function createChatSession(initialThreadId, normalizeItem = productNormalizer_1.normalizeItemToProduct) {
7
+ return {
8
+ normalizeItem,
9
+ serverThreadId: initialThreadId && !(0, chatThreads_1.isLocalThreadId)(initialThreadId) ? initialThreadId : null,
10
+ storageThreadId: null,
11
+ orphanIds: [],
12
+ createdAt: null,
13
+ lastSyncedAt: 0,
14
+ dirty: false,
15
+ interacted: false,
16
+ loadRequest: 0,
17
+ isStreaming: false,
18
+ foreignInFlight: false,
19
+ idCounter: 0,
20
+ idSuffix: (0, chatThreads_1.randomId)().slice(0, 6),
21
+ };
22
+ }
23
+ exports.createChatSession = createChatSession;
24
+ /** Forget the current conversation. Returns the id it was stored under, if any. */
25
+ function resetConversation(session) {
26
+ const storedId = session.storageThreadId;
27
+ session.serverThreadId = null;
28
+ session.storageThreadId = null;
29
+ session.orphanIds = [];
30
+ session.createdAt = null;
31
+ session.lastSyncedAt = 0;
32
+ session.dirty = false;
33
+ session.loadRequest += 1;
34
+ session.isStreaming = false;
35
+ session.foreignInFlight = false;
36
+ return storedId;
37
+ }
38
+ exports.resetConversation = resetConversation;
39
+ function nextMessageId(session) {
40
+ session.idCounter += 1;
41
+ return `msg-${session.idCounter}-${Date.now()}-${session.idSuffix}`;
42
+ }
43
+ exports.nextMessageId = nextMessageId;
44
+ /** Whether `messageId` was minted by this session rather than by another tab. */
45
+ function isOwnMessage(session, messageId) {
46
+ return messageId.endsWith(`-${session.idSuffix}`);
47
+ }
48
+ exports.isOwnMessage = isOwnMessage;
49
+ /** Makes a stored conversation the one this session continues. */
50
+ function adoptStoredChat(session, chat) {
51
+ session.storageThreadId = chat.threadId;
52
+ session.createdAt = chat.createdAt;
53
+ session.serverThreadId = (0, chatThreads_1.isLocalThreadId)(chat.threadId) ? null : chat.threadId;
54
+ session.idCounter = (0, chatThreads_1.nextMessageCounter)(chat.messages);
55
+ session.lastSyncedAt = chat.updatedAt;
56
+ }
57
+ exports.adoptStoredChat = adoptStoredChat;
58
+ /** A copy that keeps the conversation `session` holds while `session` itself moves on. */
59
+ function forkSession(session) {
60
+ return Object.assign(Object.assign({}, session), { orphanIds: [...session.orphanIds] });
61
+ }
62
+ exports.forkSession = forkSession;
63
+ /** Makes `session` continue the conversation `from` kept, showing `messages`. */
64
+ function adoptSession(session, from, messages) {
65
+ session.serverThreadId = from.serverThreadId;
66
+ session.storageThreadId = from.storageThreadId;
67
+ session.orphanIds = [...from.orphanIds];
68
+ session.createdAt = from.createdAt;
69
+ session.lastSyncedAt = from.lastSyncedAt;
70
+ session.idCounter = (0, chatThreads_1.nextMessageCounter)(messages);
71
+ }
72
+ exports.adoptSession = adoptSession;
73
+ /** Builds the record to write and moves the session onto its key; `staleIds` are the records to retire once stored. */
74
+ function prepareSnapshot(session, messages, now = Date.now()) {
75
+ var _a, _b, _c;
76
+ const previousId = session.storageThreadId;
77
+ const threadId = (_b = (_a = session.serverThreadId) !== null && _a !== void 0 ? _a : previousId) !== null && _b !== void 0 ? _b : (0, chatThreads_1.createLocalThreadId)();
78
+ const staleIds = [previousId !== threadId ? previousId : null, ...session.orphanIds].filter((id) => id !== null && id !== threadId);
79
+ // Strictly increasing so other tabs read a same-millisecond save as newer.
80
+ const updatedAt = Math.max(now, session.lastSyncedAt + 1);
81
+ const createdAt = (_c = session.createdAt) !== null && _c !== void 0 ? _c : updatedAt;
82
+ session.storageThreadId = threadId;
83
+ session.orphanIds = [];
84
+ session.createdAt = createdAt;
85
+ session.lastSyncedAt = updatedAt;
86
+ return {
87
+ staleIds: Array.from(new Set(staleIds)),
88
+ snapshot: {
89
+ version: chatThreads_1.PERSISTED_CHAT_VERSION,
90
+ threadId,
91
+ messages: (0, chatThreads_1.compactMessages)(messages, session.normalizeItem),
92
+ createdAt,
93
+ updatedAt,
94
+ owner: (0, chatThreads_1.getTabId)(),
95
+ },
96
+ };
97
+ }
98
+ exports.prepareSnapshot = prepareSnapshot;