@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
@@ -12,9 +12,8 @@ function AiMessage({ message, componentOverrides, translations }) {
12
12
  const hasGroups = !!((_a = message.groups) === null || _a === void 0 ? void 0 : _a.length);
13
13
  // On error, fall back to the (translatable) error message when the agent
14
14
  // returned no partial text of its own.
15
- const text = isError
16
- ? message.text || (0, translate_1.default)('CioAsa.error.message', translations)
17
- : message.text || '';
15
+ const errorKey = message.interrupted ? 'CioAsa.error.interrupted' : 'CioAsa.error.message';
16
+ const text = isError ? message.text || (0, translate_1.default)(errorKey, translations) : message.text || '';
18
17
  const hasText = text.length > 0;
19
18
  const loaderRenderProps = { translations };
20
19
  const textRenderProps = { text };
@@ -3,7 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importStar(require("react"));
5
5
  const useAsaResults_1 = tslib_1.__importDefault(require("../../hooks/useAsaResults"));
6
+ const useCioAsaContext_1 = require("../../hooks/useCioAsaContext");
6
7
  const useFocusTrap_1 = tslib_1.__importDefault(require("../../hooks/useFocusTrap"));
8
+ const useLatest_1 = tslib_1.__importDefault(require("../../hooks/useLatest"));
9
+ const productNormalizer_1 = require("../../utils/productNormalizer");
7
10
  const translate_1 = tslib_1.__importDefault(require("../../utils/translate"));
8
11
  const ChatHeader_1 = tslib_1.__importDefault(require("./ChatHeader"));
9
12
  const WelcomeScreen_1 = tslib_1.__importDefault(require("./WelcomeScreen"));
@@ -26,20 +29,43 @@ function getAnnouncement(messages, translations) {
26
29
  }
27
30
  return '';
28
31
  }
29
- const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, showStopButton = false, }, ref) => {
30
- const { messages, sendMessage, isStreaming, abort, clearHistory } = (0, useAsaResults_1.default)({
32
+ const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, onThreadsChange, showStopButton = false, }, ref) => {
33
+ var _a;
34
+ const { messages, sendMessage, isStreaming, canAbort, abort, clearHistory, isHydrating, threads, activeThreadId, newThread, switchThread, } = (0, useAsaResults_1.default)({
31
35
  initialThreadId,
36
+ normalizeItem: (item) => (normalizeItem !== null && normalizeItem !== void 0 ? normalizeItem : productNormalizer_1.normalizeItemToProduct)(item, {
37
+ saleBadgeText: (0, translate_1.default)('CioAsa.results.saleBadge', translations),
38
+ }),
32
39
  });
33
40
  const chatViewRef = (0, react_1.useRef)(null);
34
41
  const containerRef = (0, react_1.useRef)(null);
35
42
  const isWelcome = messages.length === 0;
43
+ // While another thread loads, keep the chat frame (header, close button) instead of a blank body.
44
+ const lastViewRef = (0, react_1.useRef)(null);
45
+ if (!isHydrating)
46
+ lastViewRef.current = isWelcome ? 'welcome' : 'chat';
47
+ const showWelcome = !isHydrating && isWelcome;
48
+ const showChat = !isHydrating && !isWelcome;
49
+ const showChatFrame = showChat || (isHydrating && lastViewRef.current === 'chat');
36
50
  const isModal = typeof onClose === 'function';
37
51
  const announcement = getAnnouncement(messages, translations);
38
- (0, react_1.useImperativeHandle)(ref, () => ({
52
+ (0, react_1.useImperativeHandle)(ref, () => ({ abort, clearHistory, newThread, switchThread }), [
39
53
  abort,
40
54
  clearHistory,
41
- }));
55
+ newThread,
56
+ switchThread,
57
+ ]);
58
+ const hasPersistence = Boolean((_a = (0, useCioAsaContext_1.useCioAsaContext)()) === null || _a === void 0 ? void 0 : _a.persistence);
59
+ const onThreadsChangeRef = (0, useLatest_1.default)(onThreadsChange);
42
60
  (0, react_1.useEffect)(() => {
61
+ var _a;
62
+ if (!hasPersistence || isHydrating)
63
+ return;
64
+ (_a = onThreadsChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onThreadsChangeRef, threads, activeThreadId);
65
+ }, [threads, activeThreadId, isHydrating, hasPersistence, onThreadsChangeRef]);
66
+ (0, react_1.useEffect)(() => {
67
+ if (isHydrating)
68
+ return;
43
69
  const root = isWelcome ? containerRef.current : chatViewRef.current;
44
70
  const input = root === null || root === void 0 ? void 0 : root.querySelector('input');
45
71
  if (input) {
@@ -52,15 +78,17 @@ const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions,
52
78
  input.focus();
53
79
  }
54
80
  }
55
- }, [isWelcome]);
81
+ }, [isWelcome, isHydrating]);
56
82
  (0, useFocusTrap_1.default)(containerRef, { onEscape: onClose, trapFocus: isModal });
57
83
  return (react_1.default.createElement("div", { className: ['cio-asa', 'cio-asa-chat', className].filter(Boolean).join(' '), ref: containerRef, role: 'dialog', "aria-modal": isModal ? 'true' : undefined, "aria-label": (0, translate_1.default)(isWelcome ? 'CioAsa.welcome.title' : 'CioAsa.header.title', translations) },
58
84
  react_1.default.createElement("div", { className: 'cio-sr-only', role: 'status' }, announcement),
59
- react_1.default.createElement("div", { className: 'cio-asa-chat-body' }, isWelcome ? (react_1.default.createElement("div", { className: 'cio-asa-chat-view cio-asa-chat-view--welcome' },
60
- react_1.default.createElement(WelcomeScreen_1.default, { suggestions: initialSuggestions, onSend: sendMessage, onClose: onClose, termsText: termsText, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.welcomeScreen }))) : (react_1.default.createElement("div", { className: 'cio-asa-chat-view cio-asa-chat-view--chat', ref: chatViewRef },
61
- react_1.default.createElement(ChatHeader_1.default, { onClose: onClose, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.header }),
62
- react_1.default.createElement(ChatMessageList_1.default, { messages: messages, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: (0, translate_1.default)('CioAsa.results.addToCart', translations), viewMoreText: (0, translate_1.default)('CioAsa.results.viewMore', translations), aiMessageOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.aiMessage, userMessageOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.userMessage, resultsBlockOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.resultsBlock, translations: translations, onSend: sendMessage, isStreaming: isStreaming }),
63
- react_1.default.createElement(ChatInput_1.default, { onSubmit: sendMessage, isDisabled: isStreaming, isStreaming: isStreaming, onAbort: abort, showStopButton: showStopButton, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }))))));
85
+ react_1.default.createElement("div", { className: 'cio-asa-chat-body' },
86
+ showWelcome && (react_1.default.createElement("div", { className: 'cio-asa-chat-view cio-asa-chat-view--welcome' },
87
+ react_1.default.createElement(WelcomeScreen_1.default, { suggestions: initialSuggestions, onSend: sendMessage, onClose: onClose, termsText: termsText, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.welcomeScreen }))),
88
+ showChatFrame && (react_1.default.createElement("div", { className: 'cio-asa-chat-view cio-asa-chat-view--chat', ref: chatViewRef },
89
+ react_1.default.createElement(ChatHeader_1.default, { onClose: onClose, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.header }),
90
+ react_1.default.createElement(ChatMessageList_1.default, { messages: messages, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: (0, translate_1.default)('CioAsa.results.addToCart', translations), viewMoreText: (0, translate_1.default)('CioAsa.results.viewMore', translations), aiMessageOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.aiMessage, userMessageOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.userMessage, resultsBlockOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.resultsBlock, translations: translations, onSend: sendMessage, isStreaming: isStreaming }),
91
+ react_1.default.createElement(ChatInput_1.default, { onSubmit: sendMessage, isDisabled: isStreaming || isHydrating, isStreaming: isStreaming, onAbort: canAbort ? abort : undefined, showStopButton: showStopButton, translations: translations, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }))))));
64
92
  });
65
93
  Chat.displayName = 'Chat';
66
94
  exports.default = Chat;
@@ -15,8 +15,12 @@ const Chat_1 = tslib_1.__importDefault(require("../Chat/Chat"));
15
15
  const CioAsa = react_1.default.forwardRef((props, ref) => {
16
16
  const {
17
17
  // Provider configuration — everything else belongs to Chat.
18
- apiKey, cioClient, testCells, staticRequestConfigs, formatters, urlHelpers, callbacks, section } = props, chatProps = tslib_1.__rest(props, ["apiKey", "cioClient", "testCells", "staticRequestConfigs", "formatters", "urlHelpers", "callbacks", "section"]);
19
- return (react_1.default.createElement(CioAsaProvider_1.default, { apiKey: apiKey, cioClient: cioClient, testCells: testCells, staticRequestConfigs: staticRequestConfigs, formatters: formatters, urlHelpers: urlHelpers, callbacks: callbacks, section: section },
18
+ apiKey, cioClient, testCells, staticRequestConfigs, formatters, urlHelpers, callbacks, section, persistConversation, userId = null } = props, chatProps = tslib_1.__rest(props, ["apiKey", "cioClient", "testCells", "staticRequestConfigs", "formatters", "urlHelpers", "callbacks", "section", "persistConversation", "userId"]);
19
+ // Split so TypeScript can match the pair against the provider's props.
20
+ const persistenceProps = persistConversation
21
+ ? { persistConversation, userId }
22
+ : { userId };
23
+ return (react_1.default.createElement(CioAsaProvider_1.default, Object.assign({ apiKey: apiKey, cioClient: cioClient, testCells: testCells, staticRequestConfigs: staticRequestConfigs, formatters: formatters, urlHelpers: urlHelpers, callbacks: callbacks, section: section }, persistenceProps),
20
24
  react_1.default.createElement(Chat_1.default, Object.assign({}, chatProps, { ref: ref }))));
21
25
  });
22
26
  CioAsa.displayName = 'CioAsa';
@@ -6,15 +6,42 @@ const useCioClient_1 = tslib_1.__importDefault(require("../../hooks/useCioClient
6
6
  const useCioAsaContext_1 = require("../../hooks/useCioAsaContext");
7
7
  const defaultFormatters = tslib_1.__importStar(require("../../utils/formatters"));
8
8
  const defaultUrlHelpers = tslib_1.__importStar(require("../../utils/urlHelpers"));
9
+ const clientOptions_1 = require("../../utils/clientOptions");
10
+ const localStoragePersistence_1 = require("../../utils/localStoragePersistence");
9
11
  function CioAsaProvider(props) {
10
- const { apiKey, formatters, urlHelpers, staticRequestConfigs = { domain: 'chatbot' }, cioClient: customCioClient, testCells, callbacks, section = 'Products', children, } = props;
12
+ var _a;
13
+ const { apiKey, formatters, urlHelpers, staticRequestConfigs = { domain: 'chatbot' }, cioClient: customCioClient, testCells, callbacks, section = 'Products', persistConversation: persistOptions, userId: userIdProp, children, } = props;
14
+ // Read field by field: an inline options object is a new reference on every render.
15
+ const persistenceEnabled = Boolean(persistOptions === null || persistOptions === void 0 ? void 0 : persistOptions.enabled);
11
16
  const [cioClientOptions, setCioClientOptions] = (0, react_1.useState)({});
17
+ const initialUserId = (0, react_1.useRef)(userIdProp).current;
18
+ const clientInit = (0, react_1.useMemo)(() => initialUserId === undefined
19
+ ? cioClientOptions
20
+ : Object.assign(Object.assign({}, cioClientOptions), { userId: (0, localStoragePersistence_1.shopperId)(initialUserId) }), [cioClientOptions, initialUserId]);
12
21
  const cioClient = (0, useCioClient_1.default)({
13
22
  apiKey,
14
23
  cioClient: customCioClient,
15
- cioClientOptions,
24
+ cioClientOptions: clientInit,
16
25
  testCells,
17
26
  });
27
+ const userId = (0, localStoragePersistence_1.shopperId)(userIdProp);
28
+ const syncUserId = persistenceEnabled || userIdProp !== undefined;
29
+ (0, react_1.useEffect)(() => {
30
+ if (customCioClient || !cioClient || !syncUserId)
31
+ return;
32
+ cioClient.setClientOptions({ userId });
33
+ }, [cioClient, customCioClient, syncUserId, userId]);
34
+ const resolvedApiKey = apiKey !== null && apiKey !== void 0 ? apiKey : (_a = (0, clientOptions_1.readClientOptions)(cioClient)) === null || _a === void 0 ? void 0 : _a.apiKey;
35
+ const { domain } = staticRequestConfigs;
36
+ const persistence = (0, react_1.useMemo)(() => {
37
+ if (!persistenceEnabled || resolvedApiKey === undefined)
38
+ return undefined;
39
+ return (0, localStoragePersistence_1.sharedLocalStoragePersistence)((0, localStoragePersistence_1.persistenceNamespace)({ apiKey: resolvedApiKey, domain, userId }), (0, localStoragePersistence_1.storageAreaFor)(userId));
40
+ }, [persistenceEnabled, resolvedApiKey, domain, userId]);
41
+ const persistenceScope = persistence && (userId === undefined ? 'guest' : 'user');
42
+ const persistenceIndex = persistence && resolvedApiKey !== undefined
43
+ ? (0, localStoragePersistence_1.persistenceNamespace)({ apiKey: resolvedApiKey, domain })
44
+ : undefined;
18
45
  const contextValue = (0, react_1.useMemo)(() => ({
19
46
  cioClient,
20
47
  cioClientOptions,
@@ -24,7 +51,21 @@ function CioAsaProvider(props) {
24
51
  urlHelpers: Object.assign(Object.assign({}, defaultUrlHelpers), urlHelpers),
25
52
  callbacks,
26
53
  section,
27
- }), [cioClient, cioClientOptions, formatters, urlHelpers, staticRequestConfigs, callbacks, section]);
54
+ persistence,
55
+ persistenceScope,
56
+ persistenceIndex,
57
+ }), [
58
+ cioClient,
59
+ cioClientOptions,
60
+ formatters,
61
+ urlHelpers,
62
+ staticRequestConfigs,
63
+ callbacks,
64
+ section,
65
+ persistence,
66
+ persistenceScope,
67
+ persistenceIndex,
68
+ ]);
28
69
  return (react_1.default.createElement(useCioAsaContext_1.AsaContext.Provider, { value: contextValue }, typeof children === 'function' ? children(contextValue) : children));
29
70
  }
30
71
  exports.default = CioAsaProvider;
@@ -20,9 +20,9 @@ function NextButton() {
20
20
  * `searchResultId` are known.
21
21
  */
22
22
  function ResultsGroup({ groupData, label, slidesToShow, gap, showTitle, viewMoreText, addToCartText, saleBadgeText, currency, intent, intentResultId, normalizeItem, onProductClick, onAddToCart, onViewMore, componentOverrides, tracking, callbacks, }) {
23
- var _a, _b, _c, _d;
23
+ var _a, _b, _c, _d, _e;
24
24
  const { searchResultId } = groupData;
25
- const products = groupData.searchResults.map((item) => normalizeItem(item, { saleBadgeText }));
25
+ const products = (_a = groupData.products) !== null && _a !== void 0 ? _a : groupData.searchResults.map((item) => normalizeItem(item, { saleBadgeText }));
26
26
  const canTrack = !!intent && !!searchResultId;
27
27
  const { ref: viewRef } = (0, useViewportTracking_1.default)({
28
28
  enabled: canTrack,
@@ -83,7 +83,7 @@ function ResultsGroup({ groupData, label, slidesToShow, gap, showTitle, viewMore
83
83
  addToCartText,
84
84
  onProductClick: handleProductClick,
85
85
  onAddToCart,
86
- componentOverrides: (_b = (_a = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel) === null || _a === void 0 ? void 0 : _a.item) === null || _b === void 0 ? void 0 : _b.productCard,
86
+ componentOverrides: (_c = (_b = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel) === null || _b === void 0 ? void 0 : _b.item) === null || _c === void 0 ? void 0 : _c.productCard,
87
87
  });
88
88
  const titleRenderProps = { label };
89
89
  const viewMoreRenderProps = {
@@ -95,12 +95,12 @@ function ResultsGroup({ groupData, label, slidesToShow, gap, showTitle, viewMore
95
95
  reactNode: renderProductCard,
96
96
  } }) });
97
97
  return (react_1.default.createElement("div", { className: 'cio-asa-results-group', ref: viewRef },
98
- showTitle && label && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: (_c = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.groupTitle) === null || _c === void 0 ? void 0 : _c.reactNode, props: titleRenderProps },
98
+ showTitle && label && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: (_d = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.groupTitle) === null || _d === void 0 ? void 0 : _d.reactNode, props: titleRenderProps },
99
99
  react_1.default.createElement("h3", { className: 'cio-asa-results-group__label' }, label))),
100
100
  react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: products, loop: false, responsive: {
101
101
  0: { gap, slidesToShow },
102
102
  }, componentOverrides: carouselOverrides }),
103
- onViewMore && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: (_d = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.viewMore) === null || _d === void 0 ? void 0 : _d.reactNode, props: viewMoreRenderProps },
103
+ onViewMore && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: (_e = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.viewMore) === null || _e === void 0 ? void 0 : _e.reactNode, props: viewMoreRenderProps },
104
104
  react_1.default.createElement("button", { type: 'button', className: 'cio-asa-results-group__view-more', onClick: handleViewMore },
105
105
  react_1.default.createElement("span", { className: 'cio-asa-results-group__view-more-text' }, viewMoreText),
106
106
  react_1.default.createElement(icons_1.ArrowRightIcon, null))))));
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.startTurn = exports.readAgentStream = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const asaStreamHandlers_1 = require("./asaStreamHandlers");
6
+ /** Drains an agent stream into the assistant message identified by `assistantId`. */
7
+ function readAgentStream(stream, assistantId, setMessages, callbacks) {
8
+ const reader = stream.getReader();
9
+ let cancelled = false;
10
+ let pendingGroup = null;
11
+ let intentResultId;
12
+ let loadStartFired = false;
13
+ let groupCount = 0;
14
+ const patchAssistant = (patch) => {
15
+ (0, asaStreamHandlers_1.updateMessageById)(setMessages, assistantId, (m) => (Object.assign(Object.assign({}, m), patch)));
16
+ };
17
+ const fireLoadStart = () => {
18
+ if (loadStartFired)
19
+ return;
20
+ loadStartFired = true;
21
+ callbacks.onLoadStart(intentResultId);
22
+ };
23
+ const done = (() => tslib_1.__awaiter(this, void 0, void 0, function* () {
24
+ var _a, _b, _c, _d;
25
+ try {
26
+ while (!cancelled) {
27
+ // eslint-disable-next-line no-await-in-loop
28
+ const res = yield reader.read();
29
+ if (cancelled)
30
+ break;
31
+ if (res.done) {
32
+ (0, asaStreamHandlers_1.handleStreamEnd)(assistantId, setMessages);
33
+ callbacks.onFinish({ searchResultCount: groupCount, intentResultId });
34
+ break;
35
+ }
36
+ const { type, data } = res.value;
37
+ if (!intentResultId && (data === null || data === void 0 ? void 0 : data.intent_result_id)) {
38
+ intentResultId = data.intent_result_id;
39
+ patchAssistant({ intentResultId });
40
+ }
41
+ if (type === 'start') {
42
+ if (data === null || data === void 0 ? void 0 : data.thread_id)
43
+ patchAssistant({ threadId: data.thread_id });
44
+ callbacks.onStart(data === null || data === void 0 ? void 0 : data.thread_id);
45
+ fireLoadStart();
46
+ }
47
+ else if (type === 'group') {
48
+ pendingGroup = {
49
+ display_name: (_b = (_a = data === null || data === void 0 ? void 0 : data.display_name) !== null && _a !== void 0 ? _a : data === null || data === void 0 ? void 0 : data.group) !== null && _b !== void 0 ? _b : '',
50
+ value: (_d = (_c = data === null || data === void 0 ? void 0 : data.value) !== null && _c !== void 0 ? _c : data === null || data === void 0 ? void 0 : data.group) !== null && _d !== void 0 ? _d : '',
51
+ };
52
+ }
53
+ else if (type === 'search_result') {
54
+ fireLoadStart();
55
+ groupCount += 1;
56
+ pendingGroup = (0, asaStreamHandlers_1.handleSearchResult)(data, pendingGroup, assistantId, setMessages);
57
+ }
58
+ else if (type === 'message') {
59
+ fireLoadStart();
60
+ (0, asaStreamHandlers_1.handleMessage)(data, assistantId, setMessages);
61
+ }
62
+ else if (type === 'follow_up_refinement') {
63
+ fireLoadStart();
64
+ (0, asaStreamHandlers_1.handleFollowUpRefinement)(data, assistantId, setMessages);
65
+ }
66
+ else if (type === 'server_error') {
67
+ (0, asaStreamHandlers_1.handleServerError)(assistantId, setMessages);
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ catch (_e) {
73
+ if (!cancelled)
74
+ (0, asaStreamHandlers_1.handleStreamError)(assistantId, setMessages);
75
+ }
76
+ finally {
77
+ // Cancelling an errored stream rejects with the stored error; nothing here needs it.
78
+ if (!cancelled)
79
+ reader.cancel().catch(() => { });
80
+ }
81
+ }))();
82
+ return {
83
+ assistantId,
84
+ done,
85
+ cancel: () => {
86
+ cancelled = true;
87
+ reader.cancel().catch(() => { });
88
+ },
89
+ };
90
+ }
91
+ exports.readAgentStream = readAgentStream;
92
+ /** Streams into `assistant`, reporting to the owner `createOwner` returns for the new turn. */
93
+ function startTurn(stream, assistant, createOwner, callbacks) {
94
+ const turn = { assistant };
95
+ turn.owner = createOwner(turn);
96
+ const handle = readAgentStream(stream, assistant.id, (action) => {
97
+ if (typeof action === 'function')
98
+ [turn.assistant] = action([turn.assistant]);
99
+ turn.owner.setMessages(action);
100
+ }, {
101
+ onLoadStart: callbacks.onLoadStart,
102
+ onFinish: (result) => callbacks.onFinish(Object.assign(Object.assign({}, result), { threadId: turn.serverThreadId })),
103
+ onStart: (threadId) => {
104
+ if (threadId)
105
+ turn.serverThreadId = threadId;
106
+ turn.owner.onStart(threadId);
107
+ },
108
+ });
109
+ Object.assign(turn, handle);
110
+ handle.done.then(() => turn.owner.onDone());
111
+ return turn;
112
+ }
113
+ exports.startTurn = startTurn;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.handleStreamError = exports.handleStreamEnd = exports.handleServerError = exports.handleFollowUpRefinement = exports.handleMessage = exports.handleSearchResult = void 0;
3
+ exports.handleStreamError = exports.handleStreamEnd = exports.handleServerError = exports.handleFollowUpRefinement = exports.handleMessage = exports.handleSearchResult = exports.updateMessageById = void 0;
4
4
  /**
5
5
  * Internal A/B configuration echoed back on the request. Stripped before the request reaches
6
6
  * consumers: it is unstable (the backend omits it on some pods within a single response) and
@@ -14,6 +14,7 @@ function omitInternalRequestFields(request) {
14
14
  function updateMessageById(setMessages, id, patch) {
15
15
  setMessages((prev) => prev.map((msg) => (msg.id === id ? patch(msg) : msg)));
16
16
  }
17
+ exports.updateMessageById = updateMessageById;
17
18
  function handleSearchResult(data, pendingGroup, assistantId, setMessages) {
18
19
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
19
20
  const baseGroup = pendingGroup !== null && pendingGroup !== void 0 ? pendingGroup : {
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribeBackgroundTurns = exports.moveBackgroundTurns = exports.resumeBackgroundTurn = exports.continueInBackground = exports.BackgroundTurn = void 0;
4
+ const chatSession_1 = require("../../utils/chatSession");
5
+ const chatThreads_1 = require("../../utils/chatThreads");
6
+ const turns = new Set();
7
+ const listeners = new Set();
8
+ const notify = () => listeners.forEach((listener) => listener());
9
+ /** An answer that went on after its conversation left the screen, until it is stored or shown again. */
10
+ class BackgroundTurn {
11
+ constructor(turn, params) {
12
+ this.streaming = true;
13
+ /** Whether the latest snapshot is known to be stored. */
14
+ this.saved = false;
15
+ /** Every id the conversation was stored under, so a list read before a re-key still finds it. */
16
+ this.ids = new Set();
17
+ this.turn = turn;
18
+ this.store = params.store;
19
+ this.session = params.session;
20
+ this.messages = params.messages;
21
+ this.writes = params.after;
22
+ if (this.session.storageThreadId)
23
+ this.ids.add(this.session.storageThreadId);
24
+ this.turn.owner = {
25
+ setMessages: (action) => {
26
+ this.messages = typeof action === 'function' ? action(this.messages) : action;
27
+ },
28
+ onStart: (threadId) => {
29
+ if (threadId)
30
+ this.session.serverThreadId = threadId;
31
+ this.save().then(notify);
32
+ },
33
+ onDone: () => {
34
+ this.streaming = false;
35
+ this.messages = (0, chatThreads_1.normalizeHydratedMessages)(this.messages);
36
+ this.save().then(() => {
37
+ if (this.saved)
38
+ turns.delete(this);
39
+ notify();
40
+ });
41
+ },
42
+ };
43
+ }
44
+ save() {
45
+ const { snapshot, staleIds } = (0, chatSession_1.prepareSnapshot)(this.session, this.messages);
46
+ const { store } = this;
47
+ this.ids.add(snapshot.threadId);
48
+ this.saved = false;
49
+ this.writes = this.writes
50
+ .then(() => (0, chatThreads_1.saveThreadAndRetireStale)(store, snapshot, staleIds))
51
+ .then(({ saved, leftover }) => {
52
+ if (leftover.length > 0) {
53
+ this.session.orphanIds = Array.from(new Set([...this.session.orphanIds, ...leftover]));
54
+ }
55
+ if (this.session.lastSyncedAt === snapshot.updatedAt)
56
+ this.saved = saved;
57
+ })
58
+ .catch(() => { });
59
+ return this.writes;
60
+ }
61
+ /** Later writes go into `store`. */
62
+ moveTo(store) {
63
+ this.store = store;
64
+ }
65
+ /** Stores the conversation settled, without awaiting: the page is going away. */
66
+ flush() {
67
+ if (!this.streaming && this.saved)
68
+ return;
69
+ const { snapshot, staleIds } = (0, chatSession_1.prepareSnapshot)(this.session, (0, chatThreads_1.normalizeHydratedMessages)(this.messages));
70
+ if (this.store.saveThreadSync)
71
+ this.store.saveThreadSync(snapshot, staleIds);
72
+ else
73
+ this.store.saveThread(snapshot).catch(() => { });
74
+ }
75
+ }
76
+ exports.BackgroundTurn = BackgroundTurn;
77
+ let listeningForPagehide = false;
78
+ /**
79
+ * Keeps `turn` streaming with no chat showing it. It is saved into `store` at once, on `start`
80
+ * and when done, each write queued behind `after`, and is dropped from here once the last one landed.
81
+ */
82
+ function continueInBackground(turn, params) {
83
+ const bg = new BackgroundTurn(turn, params);
84
+ turns.add(bg);
85
+ if (!listeningForPagehide && typeof window !== 'undefined') {
86
+ listeningForPagehide = true;
87
+ window.addEventListener('pagehide', () => turns.forEach((t) => t.flush()));
88
+ }
89
+ bg.save().then(notify);
90
+ }
91
+ exports.continueInBackground = continueInBackground;
92
+ /**
93
+ * Takes back the background turn stored in `store` under `threadId` for a chat to show, handing an
94
+ * unfinished one to `createOwner`; `null` if there is none.
95
+ */
96
+ function resumeBackgroundTurn(store, threadId, createOwner) {
97
+ const bg = Array.from(turns).find((t) => t.store === store && t.ids.has(threadId));
98
+ if (!bg)
99
+ return null;
100
+ turns.delete(bg);
101
+ if (bg.streaming)
102
+ bg.turn.owner = createOwner(bg.turn);
103
+ return bg;
104
+ }
105
+ exports.resumeBackgroundTurn = resumeBackgroundTurn;
106
+ /** Points the background turns of `from` at `to`; resolves once their writes into `from` settled. */
107
+ function moveBackgroundTurns(from, to) {
108
+ const moving = Array.from(turns).filter((bg) => bg.store === from);
109
+ const settled = Promise.all(moving.map((bg) => bg.writes)).then(() => { });
110
+ moving.forEach((bg) => bg.moveTo(to));
111
+ return settled;
112
+ }
113
+ exports.moveBackgroundTurns = moveBackgroundTurns;
114
+ /** Called whenever a background turn is stored or finishes. */
115
+ function subscribeBackgroundTurns(listener) {
116
+ listeners.add(listener);
117
+ return () => {
118
+ listeners.delete(listener);
119
+ };
120
+ }
121
+ exports.subscribeBackgroundTurns = subscribeBackgroundTurns;