@constructor-io/constructorio-ui-asa 1.0.0 → 1.1.1

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 (36) hide show
  1. package/lib/cjs/components/Chat/Chat.js +10 -1
  2. package/lib/cjs/components/Chat/ChatMessageList.js +1 -1
  3. package/lib/cjs/components/Chat/WelcomeScreen.js +3 -2
  4. package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +4 -2
  5. package/lib/cjs/components/ResultsBlock/ResultsBlock.js +13 -38
  6. package/lib/cjs/components/ResultsBlock/ResultsGroup.js +109 -0
  7. package/lib/cjs/hooks/asaStreamHandlers.js +3 -1
  8. package/lib/cjs/hooks/useAsaResults.js +78 -14
  9. package/lib/cjs/hooks/useAsaTracking.js +66 -0
  10. package/lib/cjs/hooks/useViewportTracking.js +39 -0
  11. package/lib/cjs/index.js +3 -1
  12. package/lib/cjs/utils/productNormalizer.js +2 -11
  13. package/lib/cjs/version.js +1 -1
  14. package/lib/mjs/components/Chat/Chat.js +10 -1
  15. package/lib/mjs/components/Chat/ChatMessageList.js +1 -1
  16. package/lib/mjs/components/Chat/WelcomeScreen.js +3 -2
  17. package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +4 -2
  18. package/lib/mjs/components/ResultsBlock/ResultsBlock.js +12 -45
  19. package/lib/mjs/components/ResultsBlock/ResultsGroup.js +117 -0
  20. package/lib/mjs/hooks/asaStreamHandlers.js +8 -1
  21. package/lib/mjs/hooks/useAsaResults.js +72 -10
  22. package/lib/mjs/hooks/useAsaTracking.js +91 -0
  23. package/lib/mjs/hooks/useViewportTracking.js +36 -0
  24. package/lib/mjs/index.js +1 -0
  25. package/lib/mjs/utils/productNormalizer.js +1 -0
  26. package/lib/mjs/version.js +1 -1
  27. package/lib/types/components/Chat/WelcomeScreen.d.ts +2 -2
  28. package/lib/types/components/ResultsBlock/ResultsBlock.d.ts +4 -1
  29. package/lib/types/components/ResultsBlock/ResultsGroup.d.ts +36 -0
  30. package/lib/types/hooks/useAsaTracking.d.ts +54 -0
  31. package/lib/types/hooks/useViewportTracking.d.ts +15 -0
  32. package/lib/types/index.d.ts +3 -1
  33. package/lib/types/types.d.ts +63 -1
  34. package/lib/types/utils/productNormalizer.d.ts +1 -0
  35. package/lib/types/version.d.ts +1 -1
  36. package/package.json +5 -5
@@ -20,7 +20,16 @@ const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions,
20
20
  (0, react_1.useEffect)(() => {
21
21
  const root = isWelcome ? containerRef.current : chatViewRef.current;
22
22
  const input = root === null || root === void 0 ? void 0 : root.querySelector('input');
23
- input === null || input === void 0 ? void 0 : input.focus();
23
+ if (input) {
24
+ // preventScroll: focusing must not scroll the page — the host may render Chat
25
+ // below the fold (or, as in Storybook docs, several instances on one page).
26
+ try {
27
+ input.focus({ preventScroll: true });
28
+ }
29
+ catch (_a) {
30
+ input.focus();
31
+ }
32
+ }
24
33
  }, [isWelcome]);
25
34
  (0, useFocusTrap_1.default)(containerRef, { onEscape: onClose });
26
35
  return (react_1.default.createElement("div", { className: ['cio-asa', 'cio-asa-chat', className].filter(Boolean).join(' '), ref: containerRef, role: 'dialog', "aria-labelledby": 'cio-asa-chat-title' },
@@ -39,7 +39,7 @@ function ChatMessageList({ messages, onProductClick, onViewMore, onAddToCart, as
39
39
  const hasGroups = !!((_a = message.groups) === null || _a === void 0 ? void 0 : _a.length);
40
40
  return (react_1.default.createElement("div", { key: message.id, className: 'cio-asa-ai-message-group' },
41
41
  react_1.default.createElement(AiMessage_1.default, { message: message, componentOverrides: aiMessageOverrides, translations: translations }),
42
- hasGroups && (react_1.default.createElement(ResultsBlock_1.default, { groups: message.groups, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: addToCartText, viewMoreText: viewMoreText, saleBadgeText: (0, translate_1.default)('CioAsa.results.saleBadge', translations), componentOverrides: resultsBlockOverrides }))));
42
+ hasGroups && (react_1.default.createElement(ResultsBlock_1.default, { groups: message.groups, intent: message.intent, intentResultId: message.intentResultId, threadId: message.threadId, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: addToCartText, viewMoreText: viewMoreText, saleBadgeText: (0, translate_1.default)('CioAsa.results.saleBadge', translations), componentOverrides: resultsBlockOverrides }))));
43
43
  })));
44
44
  }
45
45
  exports.default = ChatMessageList;
@@ -20,9 +20,10 @@ function WelcomeScreen({ suggestions = DEFAULT_SUGGESTIONS, onSend, onClose, ter
20
20
  placeholder: (0, translate_1.default)('CioAsa.welcome.placeholder', translations),
21
21
  isDisabled: disabled,
22
22
  };
23
+ const handleSuggestionClick = (suggestion) => onSend(suggestion, 'suggestion');
23
24
  const suggestionsRenderProps = {
24
25
  suggestions,
25
- onSuggestionClick: onSend,
26
+ onSuggestionClick: handleSuggestionClick,
26
27
  };
27
28
  return (react_1.default.createElement("div", { className: 'cio-asa-welcome-screen' },
28
29
  onClose && (react_1.default.createElement("button", { type: 'button', className: 'cio-asa-welcome-screen__close', onClick: onClose, "aria-label": (0, translate_1.default)('CioAsa.header.close', translations) }, "\u2715")),
@@ -36,7 +37,7 @@ function WelcomeScreen({ suggestions = DEFAULT_SUGGESTIONS, onSend, onClose, ter
36
37
  (0, translate_1.default)('CioAsa.welcome.sendButton', translations),
37
38
  react_1.default.createElement(icons_1.SendPlaneIcon, null)))),
38
39
  suggestions.length > 0 && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { override: (_c = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions) === null || _c === void 0 ? void 0 : _c.reactNode, props: suggestionsRenderProps },
39
- react_1.default.createElement("nav", { className: 'cio-asa-welcome-screen__suggestions', "aria-label": (0, translate_1.default)('CioAsa.welcome.suggestionsAriaLabel', translations) }, suggestions.map((suggestion) => (react_1.default.createElement("button", { key: suggestion, type: 'button', className: 'cio-asa-welcome-screen__suggestion-chip', disabled: disabled, onClick: () => onSend(suggestion) }, suggestion))))))),
40
+ react_1.default.createElement("nav", { className: 'cio-asa-welcome-screen__suggestions', "aria-label": (0, translate_1.default)('CioAsa.welcome.suggestionsAriaLabel', translations) }, suggestions.map((suggestion) => (react_1.default.createElement("button", { key: suggestion, type: 'button', className: 'cio-asa-welcome-screen__suggestion-chip', disabled: disabled, onClick: () => handleSuggestionClick(suggestion) }, suggestion))))))),
40
41
  termsText && react_1.default.createElement("div", { className: 'cio-asa-welcome-screen__terms' }, termsText)));
41
42
  }
42
43
  exports.default = WelcomeScreen;
@@ -7,7 +7,7 @@ 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
9
  function CioAsaProvider(props) {
10
- const { apiKey, formatters, urlHelpers, staticRequestConfigs = { domain: 'chatbot' }, cioClient: customCioClient, children, } = props;
10
+ const { apiKey, formatters, urlHelpers, staticRequestConfigs = { domain: 'chatbot' }, cioClient: customCioClient, callbacks, section = 'Products', children, } = props;
11
11
  const [cioClientOptions, setCioClientOptions] = (0, react_1.useState)({});
12
12
  const cioClient = (0, useCioClient_1.default)({ apiKey, cioClient: customCioClient, cioClientOptions });
13
13
  const contextValue = (0, react_1.useMemo)(() => ({
@@ -17,7 +17,9 @@ function CioAsaProvider(props) {
17
17
  staticRequestConfigs,
18
18
  formatters: Object.assign(Object.assign({}, defaultFormatters), formatters),
19
19
  urlHelpers: Object.assign(Object.assign({}, defaultUrlHelpers), urlHelpers),
20
- }), [cioClient, cioClientOptions, formatters, urlHelpers, staticRequestConfigs]);
20
+ callbacks,
21
+ section,
22
+ }), [cioClient, cioClientOptions, formatters, urlHelpers, staticRequestConfigs, callbacks, section]);
21
23
  return (react_1.default.createElement(useCioAsaContext_1.AsaContext.Provider, { value: contextValue }, typeof children === 'function' ? children(contextValue) : children));
22
24
  }
23
25
  exports.default = CioAsaProvider;
@@ -2,22 +2,23 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importStar(require("react"));
5
- const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
5
  const productNormalizer_1 = require("../../utils/productNormalizer");
7
- const icons_1 = require("../icons");
6
+ const useCioAsaContext_1 = require("../../hooks/useCioAsaContext");
7
+ const useAsaTracking_1 = tslib_1.__importDefault(require("../../hooks/useAsaTracking"));
8
8
  const constants_1 = require("./constants");
9
- const renderProductCard_1 = tslib_1.__importDefault(require("./renderProductCard"));
9
+ const ResultsGroup_1 = tslib_1.__importDefault(require("./ResultsGroup"));
10
10
  require("./ResultsBlock.css");
11
- function PreviousButton() {
12
- return null;
13
- }
14
- function NextButton() {
15
- return null;
16
- }
17
- function ResultsBlock({ groups, aspectRatio = '3:4', minCardWidth = 150, gap = 12, showTitle = true, viewMoreText = 'View more products', addToCartText = 'Add to cart', saleBadgeText = 'Sale', currency, normalizeItem = productNormalizer_1.normalizeItemToProduct, onProductClick, onAddToCart, onViewMore, componentOverrides, }) {
11
+ function ResultsBlock({ groups, aspectRatio = '3:4', minCardWidth = 150, gap = 12, showTitle = true, viewMoreText = 'View more products', addToCartText = 'Add to cart', saleBadgeText = 'Sale', currency, intent, intentResultId, threadId, normalizeItem = productNormalizer_1.normalizeItemToProduct, onProductClick, onAddToCart, onViewMore, componentOverrides, }) {
18
12
  var _a, _b;
19
13
  const containerRef = (0, react_1.useRef)(null);
20
14
  const [slidesToShow, setSlidesToShow] = (0, react_1.useState)(3 + constants_1.PEEK_FRACTION);
15
+ const context = (0, useCioAsaContext_1.useCioAsaContext)();
16
+ const tracking = (0, useAsaTracking_1.default)({
17
+ tracker: (_b = (_a = context === null || context === void 0 ? void 0 : context.cioClient) === null || _a === void 0 ? void 0 : _a.tracker) !== null && _b !== void 0 ? _b : undefined,
18
+ section: context === null || context === void 0 ? void 0 : context.section,
19
+ threadId,
20
+ });
21
+ const callbacks = context === null || context === void 0 ? void 0 : context.callbacks;
21
22
  (0, react_1.useEffect)(() => {
22
23
  const el = containerRef.current;
23
24
  if (!el)
@@ -36,37 +37,11 @@ function ResultsBlock({ groups, aspectRatio = '3:4', minCardWidth = 150, gap = 1
36
37
  }, [minCardWidth, gap]);
37
38
  if (!groups || groups.length === 0)
38
39
  return null;
39
- const renderProductCard = (0, renderProductCard_1.default)({
40
- currency,
41
- addToCartText,
42
- onProductClick,
43
- onAddToCart,
44
- 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,
45
- });
46
40
  return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-asa-results-block', style: { '--cio-asa-image-ratio': constants_1.aspectRatioMap[aspectRatio] } }, groups.map((groupData, index) => {
47
- var _a, _b, _c, _d, _e;
41
+ var _a, _b, _c;
48
42
  const label = ((_a = groupData.group) === null || _a === void 0 ? void 0 : _a.display_name) || ((_b = groupData.group) === null || _b === void 0 ? void 0 : _b.value) || '';
49
43
  const groupKey = `${((_c = groupData.group) === null || _c === void 0 ? void 0 : _c.value) || label}-${index}`;
50
- const products = groupData.searchResults.map((item) => normalizeItem(item, { saleBadgeText }));
51
- const titleRenderProps = { label };
52
- const viewMoreRenderProps = {
53
- group: groupData.group,
54
- onClick: () => onViewMore === null || onViewMore === void 0 ? void 0 : onViewMore(groupData.group),
55
- };
56
- const userCarousel = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel;
57
- const carouselOverrides = Object.assign(Object.assign({ previous: { reactNode: PreviousButton }, next: { reactNode: NextButton } }, userCarousel), { item: Object.assign(Object.assign({}, userCarousel === null || userCarousel === void 0 ? void 0 : userCarousel.item), { productCard: {
58
- reactNode: renderProductCard,
59
- } }) });
60
- return (react_1.default.createElement("div", { className: 'cio-asa-results-group', key: groupKey },
61
- 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 },
62
- react_1.default.createElement("h3", { className: 'cio-asa-results-group__label' }, label))),
63
- react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: products, loop: false, responsive: {
64
- 0: { gap, slidesToShow },
65
- }, componentOverrides: carouselOverrides }),
66
- 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 },
67
- react_1.default.createElement("button", { type: 'button', className: 'cio-asa-results-group__view-more', onClick: () => onViewMore(groupData.group) },
68
- react_1.default.createElement("span", { className: 'cio-asa-results-group__view-more-text' }, viewMoreText),
69
- react_1.default.createElement(icons_1.ArrowRightIcon, null))))));
44
+ return (react_1.default.createElement(ResultsGroup_1.default, { key: groupKey, groupData: groupData, label: label, slidesToShow: slidesToShow, gap: gap, showTitle: showTitle, viewMoreText: viewMoreText, addToCartText: addToCartText, saleBadgeText: saleBadgeText, currency: currency, intent: intent, intentResultId: intentResultId, normalizeItem: normalizeItem, onProductClick: onProductClick, onAddToCart: onAddToCart, onViewMore: onViewMore, componentOverrides: componentOverrides, tracking: tracking, callbacks: callbacks }));
70
45
  })));
71
46
  }
72
47
  ResultsBlock.displayName = 'ResultsBlock';
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const react_1 = tslib_1.__importDefault(require("react"));
5
+ const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
+ const useViewportTracking_1 = tslib_1.__importDefault(require("../../hooks/useViewportTracking"));
7
+ const icons_1 = require("../icons");
8
+ const renderProductCard_1 = tslib_1.__importDefault(require("./renderProductCard"));
9
+ function PreviousButton() {
10
+ return null;
11
+ }
12
+ function NextButton() {
13
+ return null;
14
+ }
15
+ /**
16
+ * Renders a single search_result pod (one carousel) and fires its pod-level behavioral
17
+ * events: `assistant_search_result_view` (on scroll into view), `assistant_search_result_click`
18
+ * (product click), and `assistant_search_submit` ("view more"). Each SDK beacon is paired
19
+ * with its optional consumer callback. Events only fire when both `intent` and the pod's
20
+ * `searchResultId` are known.
21
+ */
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;
24
+ const { searchResultId } = groupData;
25
+ const products = groupData.searchResults.map((item) => normalizeItem(item, { saleBadgeText }));
26
+ const canTrack = !!intent && !!searchResultId;
27
+ const { ref: viewRef } = (0, useViewportTracking_1.default)({
28
+ enabled: canTrack,
29
+ onView: () => {
30
+ var _a;
31
+ if (!intent || !searchResultId)
32
+ return;
33
+ const items = products.map((p) => (Object.assign({ itemId: p.id, itemName: p.name }, (p.variationId && { variationId: p.variationId }))));
34
+ const payload = {
35
+ intent,
36
+ searchResultId,
37
+ intentResultId,
38
+ numResultsViewed: products.length,
39
+ items,
40
+ };
41
+ tracking.trackResultView(payload);
42
+ (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onResultView) === null || _a === void 0 ? void 0 : _a.call(callbacks, payload);
43
+ },
44
+ });
45
+ const handleProductClick = (product) => {
46
+ var _a;
47
+ if (intent && searchResultId) {
48
+ tracking.trackResultClick({
49
+ intent,
50
+ searchResultId,
51
+ intentResultId,
52
+ itemId: product.id,
53
+ itemName: product.name,
54
+ variationId: product.variationId,
55
+ });
56
+ (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onResultClick) === null || _a === void 0 ? void 0 : _a.call(callbacks, {
57
+ intent,
58
+ searchResultId,
59
+ intentResultId,
60
+ item: Object.assign({ itemId: product.id, itemName: product.name }, (product.variationId && { variationId: product.variationId })),
61
+ });
62
+ }
63
+ onProductClick === null || onProductClick === void 0 ? void 0 : onProductClick(product);
64
+ };
65
+ const handleViewMore = () => {
66
+ var _a, _b, _c;
67
+ if (intent && searchResultId) {
68
+ const searchTerm = ((_a = groupData.group) === null || _a === void 0 ? void 0 : _a.value) || ((_b = groupData.group) === null || _b === void 0 ? void 0 : _b.display_name) || '';
69
+ const payload = {
70
+ intent,
71
+ searchTerm,
72
+ userInput: searchTerm,
73
+ searchResultId,
74
+ intentResultId,
75
+ };
76
+ tracking.trackSearchSubmit(payload);
77
+ (_c = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onSearchSubmit) === null || _c === void 0 ? void 0 : _c.call(callbacks, payload);
78
+ }
79
+ onViewMore === null || onViewMore === void 0 ? void 0 : onViewMore(groupData.group);
80
+ };
81
+ const renderProductCard = (0, renderProductCard_1.default)({
82
+ currency,
83
+ addToCartText,
84
+ onProductClick: handleProductClick,
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,
87
+ });
88
+ const titleRenderProps = { label };
89
+ const viewMoreRenderProps = {
90
+ group: groupData.group,
91
+ onClick: handleViewMore,
92
+ };
93
+ const userCarousel = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel;
94
+ const carouselOverrides = Object.assign(Object.assign({ previous: { reactNode: PreviousButton }, next: { reactNode: NextButton } }, userCarousel), { item: Object.assign(Object.assign({}, userCarousel === null || userCarousel === void 0 ? void 0 : userCarousel.item), { productCard: {
95
+ reactNode: renderProductCard,
96
+ } }) });
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 },
99
+ react_1.default.createElement("h3", { className: 'cio-asa-results-group__label' }, label))),
100
+ react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: products, loop: false, responsive: {
101
+ 0: { gap, slidesToShow },
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 },
104
+ react_1.default.createElement("button", { type: 'button', className: 'cio-asa-results-group__view-more', onClick: handleViewMore },
105
+ react_1.default.createElement("span", { className: 'cio-asa-results-group__view-more-text' }, viewMoreText),
106
+ react_1.default.createElement(icons_1.ArrowRightIcon, null))))));
107
+ }
108
+ exports.default = ResultsGroup;
109
+ ResultsGroup.displayName = 'ResultsGroup';
@@ -11,7 +11,9 @@ function handleSearchResult(data, pendingGroup, assistantId, setMessages) {
11
11
  value: (_h = (_g = (_f = (_e = data === null || data === void 0 ? void 0 : data.response) === null || _e === void 0 ? void 0 : _e.search_request) === null || _f === void 0 ? void 0 : _f.search_term) !== null && _g !== void 0 ? _g : data === null || data === void 0 ? void 0 : data.title) !== null && _h !== void 0 ? _h : '',
12
12
  };
13
13
  const results = (_k = (_j = data === null || data === void 0 ? void 0 : data.response) === null || _j === void 0 ? void 0 : _j.results) !== null && _k !== void 0 ? _k : ((data === null || data === void 0 ? void 0 : data.results) ? data.results : [data]);
14
- const newGroup = { group: resolvedGroup, searchResults: results };
14
+ // `result_id` is unique per search_result event; `intent_result_id` is shared across
15
+ // the stream. Both sit at the top level of the SSE event payload.
16
+ const newGroup = Object.assign(Object.assign({ group: resolvedGroup, searchResults: results }, ((data === null || data === void 0 ? void 0 : data.result_id) && { searchResultId: data.result_id })), ((data === null || data === void 0 ? void 0 : data.intent_result_id) && { intentResultId: data.intent_result_id }));
15
17
  updateMessageById(setMessages, assistantId, (msg) => (Object.assign(Object.assign({}, msg), { status: 'streaming', groups: [...(msg.groups || []), newGroup] })));
16
18
  return null;
17
19
  }
@@ -4,7 +4,9 @@ const tslib_1 = require("tslib");
4
4
  const react_1 = require("react");
5
5
  const useCioAsaContext_1 = require("./useCioAsaContext");
6
6
  const asaStreamHandlers_1 = require("./asaStreamHandlers");
7
+ const useAsaTracking_1 = tslib_1.__importDefault(require("./useAsaTracking"));
7
8
  function useAsaResults() {
9
+ var _a;
8
10
  const [messages, setMessages] = (0, react_1.useState)([]);
9
11
  const [isStreaming, setIsStreaming] = (0, react_1.useState)(false);
10
12
  const isStreamingRef = (0, react_1.useRef)(false);
@@ -16,22 +18,37 @@ function useAsaResults() {
16
18
  if (!contextValue) {
17
19
  throw new Error('useAsaResults must be used within a CioAsaProvider.');
18
20
  }
19
- const { cioClient, staticRequestConfigs } = contextValue;
21
+ const { cioClient, staticRequestConfigs, callbacks, section } = contextValue;
20
22
  const { domain } = staticRequestConfigs || {};
21
23
  if (!cioClient || !domain) {
22
24
  throw new Error('useAsaResults requires a configured cioClient and domain. Check your CioAsaProvider props.');
23
25
  }
26
+ const tracking = (0, useAsaTracking_1.default)({
27
+ tracker: cioClient.tracker,
28
+ section,
29
+ threadId: (_a = threadIdRef.current) !== null && _a !== void 0 ? _a : undefined,
30
+ });
31
+ const trackingRef = (0, react_1.useRef)(tracking);
32
+ trackingRef.current = tracking;
33
+ const callbacksRef = (0, react_1.useRef)(callbacks);
34
+ callbacksRef.current = callbacks;
35
+ const staticRequestConfigsRef = (0, react_1.useRef)(staticRequestConfigs);
36
+ staticRequestConfigsRef.current = staticRequestConfigs;
24
37
  const nextMessageId = (0, react_1.useCallback)(() => {
25
38
  idCounterRef.current += 1;
26
39
  return `msg-${idCounterRef.current}-${Date.now()}`;
27
40
  }, []);
28
- const sendMessage = (0, react_1.useCallback)((text) => {
29
- if (!text.trim() || isStreamingRef.current)
41
+ const sendMessage = (0, react_1.useCallback)((text, source = 'input') => {
42
+ var _a, _b;
43
+ const intent = text.trim();
44
+ if (!intent || isStreamingRef.current)
30
45
  return;
46
+ trackingRef.current.trackSubmit(intent);
47
+ (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAssistantSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, { intent, source });
31
48
  const userMessage = {
32
49
  id: nextMessageId(),
33
50
  role: 'user',
34
- text: text.trim(),
51
+ text: intent,
35
52
  status: 'done',
36
53
  };
37
54
  const assistantMessage = {
@@ -40,17 +57,44 @@ function useAsaResults() {
40
57
  text: '',
41
58
  groups: [],
42
59
  status: 'loading',
60
+ intent,
43
61
  };
44
62
  setMessages((prev) => [...prev, userMessage, assistantMessage]);
45
63
  setIsStreaming(true);
46
64
  isStreamingRef.current = true;
47
65
  killSwitchRef.current = false;
48
- const stream = cioClient.agent.getAgentResultsStream(text.trim(), Object.assign({ domain }, (threadIdRef.current && { threadId: threadIdRef.current })));
66
+ // `intent` is passed as the first argument and `threadId` is managed per-stream
67
+ // below, so both are stripped here. Everything else configured on the provider
68
+ // (filters, guard, numResultsPerEvent, numResultEvents, qsParam,
69
+ // preFilterExpression, fmtOptions, ...) is forwarded to the agent as-is.
70
+ const agentParams = Object.assign({}, staticRequestConfigsRef.current);
71
+ delete agentParams.intent;
72
+ delete agentParams.threadId;
73
+ const stream = cioClient.agent.getAgentResultsStream(intent, Object.assign(Object.assign(Object.assign({}, agentParams), { domain }), (threadIdRef.current && { threadId: threadIdRef.current })));
49
74
  const reader = stream.getReader();
50
75
  readerRef.current = reader;
76
+ // Mutable per-stream state kept on an object so the closures below don't capture
77
+ // reassigned loop-locals (which eslint's no-loop-func forbids).
78
+ const streamState = {
79
+ pendingGroup: null,
80
+ intentResultId: undefined,
81
+ loadStartFired: false,
82
+ groupCount: 0,
83
+ };
84
+ const patchAssistant = (patch) => {
85
+ setMessages((prev) => prev.map((m) => (m.id === assistantMessage.id ? Object.assign(Object.assign({}, m), patch) : m)));
86
+ };
87
+ const fireLoadStart = () => {
88
+ var _a, _b;
89
+ if (streamState.loadStartFired)
90
+ return;
91
+ streamState.loadStartFired = true;
92
+ const args = { intent, intentResultId: streamState.intentResultId };
93
+ trackingRef.current.trackResultLoadStarted(args);
94
+ (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onResultLoadStart) === null || _b === void 0 ? void 0 : _b.call(_a, args);
95
+ };
51
96
  (() => tslib_1.__awaiter(this, void 0, void 0, function* () {
52
- var _a, _b, _c, _d;
53
- let pendingGroup = null;
97
+ var _c, _d, _e, _f, _g, _h;
54
98
  try {
55
99
  // eslint-disable-next-line no-await-in-loop
56
100
  while (!killSwitchRef.current) {
@@ -60,22 +104,42 @@ function useAsaResults() {
60
104
  break;
61
105
  if (res.done) {
62
106
  (0, asaStreamHandlers_1.handleStreamEnd)(assistantMessage.id, setMessages);
107
+ const finishArgs = {
108
+ intent,
109
+ searchResultCount: streamState.groupCount,
110
+ intentResultId: streamState.intentResultId,
111
+ };
112
+ trackingRef.current.trackResultLoadFinished(finishArgs);
113
+ (_d = (_c = callbacksRef.current) === null || _c === void 0 ? void 0 : _c.onResultLoadFinish) === null || _d === void 0 ? void 0 : _d.call(_c, finishArgs);
63
114
  break;
64
115
  }
65
116
  const { type, data } = res.value;
66
- if (type === 'start' && (data === null || data === void 0 ? void 0 : data.thread_id)) {
67
- threadIdRef.current = data.thread_id;
117
+ // `intent_result_id` is shared across the stream; capture it from the first
118
+ // event that carries it so load-start/finish can attribute correctly.
119
+ if (!streamState.intentResultId && (data === null || data === void 0 ? void 0 : data.intent_result_id)) {
120
+ streamState.intentResultId = data.intent_result_id;
121
+ patchAssistant({ intentResultId: streamState.intentResultId });
122
+ }
123
+ if (type === 'start') {
124
+ if (data === null || data === void 0 ? void 0 : data.thread_id) {
125
+ threadIdRef.current = data.thread_id;
126
+ patchAssistant({ threadId: data.thread_id });
127
+ }
128
+ fireLoadStart();
68
129
  }
69
130
  else if (type === 'group') {
70
- pendingGroup = {
71
- 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 : '',
72
- 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 : '',
131
+ streamState.pendingGroup = {
132
+ 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 : '',
133
+ 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 : '',
73
134
  };
74
135
  }
75
136
  else if (type === 'search_result') {
76
- pendingGroup = (0, asaStreamHandlers_1.handleSearchResult)(data, pendingGroup, assistantMessage.id, setMessages);
137
+ fireLoadStart();
138
+ streamState.groupCount += 1;
139
+ streamState.pendingGroup = (0, asaStreamHandlers_1.handleSearchResult)(data, streamState.pendingGroup, assistantMessage.id, setMessages);
77
140
  }
78
141
  else if (type === 'message') {
142
+ fireLoadStart();
79
143
  (0, asaStreamHandlers_1.handleMessage)(data, assistantMessage.id, setMessages);
80
144
  }
81
145
  else if (type === 'server_error') {
@@ -84,7 +148,7 @@ function useAsaResults() {
84
148
  }
85
149
  }
86
150
  }
87
- catch (_e) {
151
+ catch (_j) {
88
152
  (0, asaStreamHandlers_1.handleStreamError)(assistantMessage.id, setMessages);
89
153
  }
90
154
  finally {
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ const NOOP_TRACKING = {
5
+ trackSubmit: () => { },
6
+ trackResultLoadStarted: () => { },
7
+ trackResultLoadFinished: () => { },
8
+ trackResultClick: () => { },
9
+ trackResultView: () => { },
10
+ trackSearchSubmit: () => { },
11
+ };
12
+ /**
13
+ * Fires ASA behavioral tracking events through the Constructor client. Each returned
14
+ * function maps to one `trackAssistant*` beacon and merges in `section` + `threadId`.
15
+ * Mirrors the tracking-hook pattern used by constructorio-ui-pia.
16
+ */
17
+ function useAsaTracking({ tracker, section, threadId, }) {
18
+ const assistant = tracker;
19
+ 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]);
23
+ 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]);
30
+ 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,
32
+ searchResultId }, (intentResultId && { intentResultId })), (itemId && { itemId })), (itemName && { itemName })), (variationId && { variationId })), base));
33
+ }, [assistant, base]);
34
+ 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,
36
+ searchResultId,
37
+ numResultsViewed }, (intentResultId && { intentResultId })), (items && items.length > 0 && { items })), base));
38
+ }, [assistant, base]);
39
+ 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,
41
+ searchTerm,
42
+ userInput,
43
+ searchResultId }, (intentResultId && { intentResultId })), (groupId && { groupId })), base));
44
+ }, [assistant, base]);
45
+ return (0, react_1.useMemo)(() => {
46
+ if (!assistant)
47
+ return NOOP_TRACKING;
48
+ return {
49
+ trackSubmit,
50
+ trackResultLoadStarted,
51
+ trackResultLoadFinished,
52
+ trackResultClick,
53
+ trackResultView,
54
+ trackSearchSubmit,
55
+ };
56
+ }, [
57
+ assistant,
58
+ trackSubmit,
59
+ trackResultLoadStarted,
60
+ trackResultLoadFinished,
61
+ trackResultClick,
62
+ trackResultView,
63
+ trackSearchSubmit,
64
+ ]);
65
+ }
66
+ exports.default = useAsaTracking;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ /**
5
+ * Fires `onView` once when the observed element scrolls into the viewport, modeling a
6
+ * per-pod `assistant_search_result_view` impression. Unlike the PIA widget-level view
7
+ * tracker, this fires a single impression per pod (not repeated enter/exit cycles).
8
+ */
9
+ function useViewportTracking({ onView, enabled = true, }) {
10
+ const onViewRef = (0, react_1.useRef)(onView);
11
+ onViewRef.current = onView;
12
+ const firedRef = (0, react_1.useRef)(false);
13
+ const cleanupRef = (0, react_1.useRef)(null);
14
+ const ref = (0, react_1.useCallback)((node) => {
15
+ if (cleanupRef.current) {
16
+ cleanupRef.current();
17
+ cleanupRef.current = null;
18
+ }
19
+ if (!node || !enabled || firedRef.current)
20
+ return;
21
+ if (typeof IntersectionObserver === 'undefined')
22
+ return;
23
+ const observer = new IntersectionObserver(([entry]) => {
24
+ if (entry.isIntersecting && !firedRef.current) {
25
+ firedRef.current = true;
26
+ onViewRef.current();
27
+ observer.disconnect();
28
+ }
29
+ }, { threshold: 0.5 });
30
+ observer.observe(node);
31
+ cleanupRef.current = () => observer.disconnect();
32
+ }, [enabled]);
33
+ (0, react_1.useEffect)(() => () => {
34
+ if (cleanupRef.current)
35
+ cleanupRef.current();
36
+ }, []);
37
+ return { ref };
38
+ }
39
+ exports.default = useViewportTracking;
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.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = void 0;
6
+ exports.ConstructorIOClient = exports.normalizeItemToProduct = exports.useAsaTracking = exports.useAsaResults = exports.Button = exports.ResultsBlock = exports.Chat = exports.CioAsaProvider = void 0;
7
7
  // Components
8
8
  var CioAsaProvider_1 = require("./components/CioAsaProvider/CioAsaProvider");
9
9
  Object.defineProperty(exports, "CioAsaProvider", { enumerable: true, get: function () { return __importDefault(CioAsaProvider_1).default; } });
@@ -16,6 +16,8 @@ Object.defineProperty(exports, "Button", { enumerable: true, get: function () {
16
16
  // Hooks
17
17
  var useAsaResults_1 = require("./hooks/useAsaResults");
18
18
  Object.defineProperty(exports, "useAsaResults", { enumerable: true, get: function () { return __importDefault(useAsaResults_1).default; } });
19
+ var useAsaTracking_1 = require("./hooks/useAsaTracking");
20
+ Object.defineProperty(exports, "useAsaTracking", { enumerable: true, get: function () { return __importDefault(useAsaTracking_1).default; } });
19
21
  // Utils
20
22
  var productNormalizer_1 = require("./utils/productNormalizer");
21
23
  Object.defineProperty(exports, "normalizeItemToProduct", { enumerable: true, get: function () { return productNormalizer_1.normalizeItemToProduct; } });
@@ -4,17 +4,8 @@ exports.normalizeItemToProduct = void 0;
4
4
  function normalizeItemToProduct(item, options) {
5
5
  var _a, _b, _c, _d, _e, _f, _g, _h;
6
6
  const data = item.data || {};
7
- return {
8
- id: String((_c = (_b = (_a = data.id) !== null && _a !== void 0 ? _a : data.result_id) !== null && _b !== void 0 ? _b : item.value) !== null && _c !== void 0 ? _c : ''),
9
- name: String((_e = (_d = item.value) !== null && _d !== void 0 ? _d : data.item_name) !== null && _e !== void 0 ? _e : ''),
10
- url: data.url,
11
- imageUrl: (_f = data.image_url) !== null && _f !== void 0 ? _f : data.imageUrl,
12
- price: data.price,
13
- salePrice: (_g = data.sale_price) !== null && _g !== void 0 ? _g : data.salePrice,
14
- description: data.description,
15
- badge: data.sale_price != null || data.salePrice != null
7
+ return Object.assign(Object.assign({ id: String((_c = (_b = (_a = data.id) !== null && _a !== void 0 ? _a : data.result_id) !== null && _b !== void 0 ? _b : item.value) !== null && _c !== void 0 ? _c : ''), name: String((_e = (_d = item.value) !== null && _d !== void 0 ? _d : data.item_name) !== null && _e !== void 0 ? _e : '') }, (data.variation_id != null && { variationId: String(data.variation_id) })), { url: data.url, imageUrl: (_f = data.image_url) !== null && _f !== void 0 ? _f : data.imageUrl, price: data.price, salePrice: (_g = data.sale_price) !== null && _g !== void 0 ? _g : data.salePrice, description: data.description, badge: data.sale_price != null || data.salePrice != null
16
8
  ? (_h = options === null || options === void 0 ? void 0 : options.saleBadgeText) !== null && _h !== void 0 ? _h : 'Sale'
17
- : undefined,
18
- };
9
+ : undefined });
19
10
  }
20
11
  exports.normalizeItemToProduct = normalizeItemToProduct;
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.0.0';
3
+ exports.default = '1.1.1';
@@ -17,7 +17,16 @@ const Chat = forwardRef(({ onClose, className, initialSuggestions, termsText, on
17
17
  useEffect(() => {
18
18
  const root = isWelcome ? containerRef.current : chatViewRef.current;
19
19
  const input = root?.querySelector('input');
20
- input?.focus();
20
+ if (input) {
21
+ // preventScroll: focusing must not scroll the page — the host may render Chat
22
+ // below the fold (or, as in Storybook docs, several instances on one page).
23
+ try {
24
+ input.focus({ preventScroll: true });
25
+ }
26
+ catch {
27
+ input.focus();
28
+ }
29
+ }
21
30
  }, [isWelcome]);
22
31
  useFocusTrap(containerRef, { onEscape: onClose });
23
32
  return (React.createElement("div", { className: ['cio-asa', 'cio-asa-chat', className].filter(Boolean).join(' '), ref: containerRef, role: 'dialog', "aria-labelledby": 'cio-asa-chat-title' },
@@ -35,7 +35,7 @@ export default function ChatMessageList({ messages, onProductClick, onViewMore,
35
35
  const hasGroups = !!message.groups?.length;
36
36
  return (React.createElement("div", { key: message.id, className: 'cio-asa-ai-message-group' },
37
37
  React.createElement(AiMessage, { message: message, componentOverrides: aiMessageOverrides, translations: translations }),
38
- hasGroups && (React.createElement(ResultsBlock, { groups: message.groups, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: addToCartText, viewMoreText: viewMoreText, saleBadgeText: translate('CioAsa.results.saleBadge', translations), componentOverrides: resultsBlockOverrides }))));
38
+ hasGroups && (React.createElement(ResultsBlock, { groups: message.groups, intent: message.intent, intentResultId: message.intentResultId, threadId: message.threadId, onProductClick: onProductClick, onViewMore: onViewMore, onAddToCart: onAddToCart, aspectRatio: aspectRatio, currency: currency, normalizeItem: normalizeItem, addToCartText: addToCartText, viewMoreText: viewMoreText, saleBadgeText: translate('CioAsa.results.saleBadge', translations), componentOverrides: resultsBlockOverrides }))));
39
39
  })));
40
40
  }
41
41
  ChatMessageList.displayName = 'ChatMessageList';