@constructor-io/constructorio-ui-pia 2.10.4 → 2.12.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 (85) hide show
  1. package/README.md +7 -0
  2. package/dist/constructorio-ui-pia-bundled.js +55 -55
  3. package/lib/cjs/components/CioPia/CioPia.js +8 -96
  4. package/lib/cjs/components/CioPia/CioPiaQna.js +111 -0
  5. package/lib/cjs/components/CioPia/CioPiaStyles.css +19 -0
  6. package/lib/cjs/components/CioPia/PiaCustomCarousel.js +24 -29
  7. package/lib/cjs/components/CioPiaRecs/CioPiaRecs.js +73 -0
  8. package/lib/cjs/components/CioPiaRecs/CioPiaRecsStyles.css +138 -0
  9. package/lib/cjs/components/CioPiaRecs/RecsCarouselSkeleton.js +58 -0
  10. package/lib/cjs/components/CioPiaRecs/RecsOptionsSkeleton.js +18 -0
  11. package/lib/cjs/components/CioPiaRecs/RecsPodRefinement.js +27 -0
  12. package/lib/cjs/components/Input/Input.js +27 -9
  13. package/lib/cjs/components/Input/InputStyles.css +27 -0
  14. package/lib/cjs/components/SuggestedQuestion/SuggestedQuestion.js +2 -3
  15. package/lib/cjs/components/SuggestedQuestionsContainer/SuggestedQuestionsContainerStyles.css +1 -1
  16. package/lib/cjs/constants.js +12 -1
  17. package/lib/cjs/hooks/useAnswerResults.js +4 -14
  18. package/lib/cjs/hooks/useCioPia.js +48 -18
  19. package/lib/cjs/hooks/usePiaClient.js +21 -0
  20. package/lib/cjs/hooks/useRecsPod.js +134 -0
  21. package/lib/cjs/hooks/useSuggestedQuestions.js +10 -26
  22. package/lib/cjs/hooks/useViewportTracking.js +3 -2
  23. package/lib/cjs/styles.css +1 -0
  24. package/lib/cjs/types.js +0 -2
  25. package/lib/cjs/utils/classNames.js +13 -0
  26. package/lib/cjs/utils/recs.js +13 -0
  27. package/lib/cjs/utils/translate.js +5 -0
  28. package/lib/cjs/version.js +1 -1
  29. package/lib/mjs/components/CioPia/CioPia.js +7 -95
  30. package/lib/mjs/components/CioPia/CioPiaQna.js +106 -0
  31. package/lib/mjs/components/CioPia/CioPiaStyles.css +19 -0
  32. package/lib/mjs/components/CioPia/PiaCustomCarousel.js +43 -30
  33. package/lib/mjs/components/CioPiaRecs/CioPiaRecs.js +69 -0
  34. package/lib/mjs/components/CioPiaRecs/CioPiaRecsStyles.css +138 -0
  35. package/lib/mjs/components/CioPiaRecs/RecsCarouselSkeleton.js +54 -0
  36. package/lib/mjs/components/CioPiaRecs/RecsOptionsSkeleton.js +14 -0
  37. package/lib/mjs/components/CioPiaRecs/RecsPodRefinement.js +23 -0
  38. package/lib/mjs/components/Input/Input.js +27 -9
  39. package/lib/mjs/components/Input/InputStyles.css +27 -0
  40. package/lib/mjs/components/SuggestedQuestion/SuggestedQuestion.js +2 -3
  41. package/lib/mjs/components/SuggestedQuestionsContainer/SuggestedQuestionsContainerStyles.css +1 -1
  42. package/lib/mjs/constants.js +11 -0
  43. package/lib/mjs/hooks/useAnswerResults.js +4 -13
  44. package/lib/mjs/hooks/useCioPia.js +49 -18
  45. package/lib/mjs/hooks/usePiaClient.js +17 -0
  46. package/lib/mjs/hooks/useRecsPod.js +129 -0
  47. package/lib/mjs/hooks/useSuggestedQuestions.js +12 -24
  48. package/lib/mjs/hooks/useViewportTracking.js +3 -2
  49. package/lib/mjs/styles.css +1 -0
  50. package/lib/mjs/types.js +0 -1
  51. package/lib/mjs/utils/classNames.js +9 -0
  52. package/lib/mjs/utils/recs.js +9 -0
  53. package/lib/mjs/utils/translate.js +6 -1
  54. package/lib/mjs/version.js +1 -1
  55. package/lib/types/components/CioPia/CioPia.d.ts +1 -36
  56. package/lib/types/components/CioPia/CioPiaQna.d.ts +4 -0
  57. package/lib/types/components/CioPia/index.d.ts +1 -0
  58. package/lib/types/components/CioPia/types.d.ts +55 -0
  59. package/lib/types/components/CioPiaRecs/CioPiaRecs.d.ts +13 -0
  60. package/lib/types/components/CioPiaRecs/RecsCarouselSkeleton.d.ts +16 -0
  61. package/lib/types/components/CioPiaRecs/RecsOptionsSkeleton.d.ts +16 -0
  62. package/lib/types/components/CioPiaRecs/RecsPodRefinement.d.ts +21 -0
  63. package/lib/types/components/Input/Input.d.ts +9 -1
  64. package/lib/types/components/SuggestedQuestion/SuggestedQuestion.d.ts +4 -2
  65. package/lib/types/constants.d.ts +11 -0
  66. package/lib/types/hooks/useAnswerResults.d.ts +4 -4
  67. package/lib/types/hooks/useCioPia.d.ts +20 -5
  68. package/lib/types/hooks/usePiaClient.d.ts +12 -0
  69. package/lib/types/hooks/useRecsPod.d.ts +52 -0
  70. package/lib/types/hooks/useSuggestedQuestions.d.ts +3 -4
  71. package/lib/types/hooks/useViewportTracking.d.ts +2 -1
  72. package/lib/types/types.d.ts +119 -7
  73. package/lib/types/utils/classNames.d.ts +9 -0
  74. package/lib/types/utils/recs.d.ts +6 -0
  75. package/lib/types/version.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/lib/cjs/hooks/mocks/MockConstructorIOClient.js +0 -24
  78. package/lib/cjs/hooks/mocks/agent.js +0 -166
  79. package/lib/mjs/hooks/mocks/MockConstructorIOClient.js +0 -38
  80. package/lib/mjs/hooks/mocks/agent.js +0 -158
  81. package/lib/types/hooks/mocks/MockConstructorIOClient.d.ts +0 -14
  82. package/lib/types/hooks/mocks/agent.d.ts +0 -10
  83. package/lib/types/hooks/mocks/types.d.ts +0 -77
  84. /package/lib/cjs/{hooks/mocks → components/CioPia}/types.js +0 -0
  85. /package/lib/mjs/{hooks/mocks → components/CioPia}/types.js +0 -0
@@ -1,103 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- const react_1 = tslib_1.__importStar(require("react"));
5
- const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
- const Input_1 = tslib_1.__importDefault(require("../Input/Input"));
7
- const SuggestedQuestionsContainer_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsContainer"));
8
- const useCioPia_1 = tslib_1.__importDefault(require("../../hooks/useCioPia"));
9
- const useConversation_1 = tslib_1.__importDefault(require("../../hooks/useConversation"));
10
- const useTracking_1 = tslib_1.__importDefault(require("../../hooks/useTracking"));
11
- const useViewportTracking_1 = tslib_1.__importDefault(require("../../hooks/useViewportTracking"));
12
- const useViewportCallbacks_1 = tslib_1.__importDefault(require("../../hooks/useViewportCallbacks"));
13
- const ErrorBlock_1 = tslib_1.__importDefault(require("../Error/ErrorBlock"));
14
- const LoadingSkeleton_1 = tslib_1.__importDefault(require("../LoadingSkeleton/LoadingSkeleton"));
15
- const translate_1 = require("../../utils/translate");
16
- const PiaInlineAnswer_1 = tslib_1.__importDefault(require("../PiaInlineAnswer/PiaInlineAnswer"));
17
- const PiaModal_1 = tslib_1.__importDefault(require("../PiaConversation/PiaModal"));
18
- const PiaConversation_1 = tslib_1.__importDefault(require("../PiaConversation/PiaConversation"));
19
- // eslint-disable-next-line complexity
4
+ const react_1 = tslib_1.__importDefault(require("react"));
5
+ const CioPiaQna_1 = tslib_1.__importDefault(require("./CioPiaQna"));
6
+ const CioPiaRecs_1 = tslib_1.__importDefault(require("../CioPiaRecs/CioPiaRecs"));
20
7
  function CioPia(props) {
21
- var _a;
22
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, parameters, } = props;
23
- const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
24
- const isConversation = mode === 'conversation' || type === 'modal';
25
- const { priceCurrency } = productCardProps || {};
26
- const pia = (0, useCioPia_1.default)({
27
- apiKey,
28
- itemId,
29
- threadId,
30
- variationId,
31
- cioClient,
32
- suggestedQuestionsParameters,
33
- parameters,
34
- formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
35
- });
36
- const tracking = (0, useTracking_1.default)({
37
- cioClient: pia.cioClient,
38
- itemId,
39
- itemName,
40
- variationId,
41
- threadId: pia.threadId,
42
- });
43
- const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, handleFeedback, resetState, } = (0, useConversation_1.default)({ pia, itemId, isConversation, callbacks, tracking });
44
- const { containerRef: viewportContainerRef } = (0, useViewportTracking_1.default)({
45
- tracking,
46
- questions: displayedQuestions,
47
- });
48
- const { containerRef: callbackContainerRef } = (0, useViewportCallbacks_1.default)({ callbacks, context });
49
- const containerRef = (0, react_1.useCallback)((node) => {
50
- viewportContainerRef(node);
51
- callbackContainerRef(node);
52
- }, [viewportContainerRef, callbackContainerRef]);
53
- const qnaResultId = (_a = pia.answers.data) === null || _a === void 0 ? void 0 : _a.qna_result_id;
54
- const renderProps = {
55
- items: currentItems,
56
- isLoading,
57
- error,
58
- currentAnswer,
59
- currentQuestion,
60
- displayedQuestions,
61
- handleSubmitQuestion,
62
- conversationHistory,
63
- };
64
- const conversationHistoryProps = {
65
- conversationHistory,
66
- isLoading,
67
- error,
68
- currentItems,
69
- showFeedback,
70
- showPreviousItems,
71
- learnMoreUrl,
72
- disclaimerPosition,
73
- translations,
74
- callbacks,
75
- componentOverrides,
76
- displayedQuestions,
77
- handleSubmitQuestion,
78
- handleQuestionClick,
79
- handleFeedback,
80
- onResultClick: tracking.trackResultClick,
81
- qnaResultId,
82
- containerRef,
83
- onInputFocus: handleInputFocus,
84
- priceCurrency,
85
- };
86
- if (type === 'modal') {
87
- return (react_1.default.createElement(PiaModal_1.default, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
88
- react_1.default.createElement(PiaConversation_1.default, Object.assign({}, conversationHistoryProps))));
8
+ var _a, _b;
9
+ const mode = (_b = (_a = props.displayConfigs) === null || _a === void 0 ? void 0 : _a.mode) !== null && _b !== void 0 ? _b : 'default';
10
+ if (mode === 'recommendations') {
11
+ return react_1.default.createElement(CioPiaRecs_1.default, Object.assign({}, props));
89
12
  }
90
- if (isConversation)
91
- return react_1.default.createElement(PiaConversation_1.default, Object.assign({}, conversationHistoryProps));
92
- // Default inline mode
93
- return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
94
- react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: renderProps, override: children || (componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.reactNode) },
95
- react_1.default.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, (0, translate_1.translate)('Any questions about this product?', translations)),
96
- react_1.default.createElement(Input_1.default, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }),
97
- isLoading && react_1.default.createElement(LoadingSkeleton_1.default, { componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.loading }),
98
- !isLoading && error && react_1.default.createElement(ErrorBlock_1.default, { message: (error === null || error === void 0 ? void 0 : error.message) || 'Unexpected error' }),
99
- !isLoading && !error && (react_1.default.createElement(react_1.default.Fragment, null,
100
- currentAnswer && (react_1.default.createElement(PiaInlineAnswer_1.default, { currentAnswer: currentAnswer, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, onFeedback: handleFeedback, onResultClick: tracking.trackResultClick, question: currentQuestion, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
101
- react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions }))))));
13
+ return react_1.default.createElement(CioPiaQna_1.default, Object.assign({}, props));
102
14
  }
103
15
  exports.default = CioPia;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const react_1 = tslib_1.__importStar(require("react"));
5
+ const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
+ const Input_1 = tslib_1.__importDefault(require("../Input/Input"));
7
+ const SuggestedQuestionsContainer_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsContainer"));
8
+ const SuggestedQuestionsSkeleton_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton"));
9
+ const useCioPia_1 = tslib_1.__importDefault(require("../../hooks/useCioPia"));
10
+ const useConversation_1 = tslib_1.__importDefault(require("../../hooks/useConversation"));
11
+ const useTracking_1 = tslib_1.__importDefault(require("../../hooks/useTracking"));
12
+ const useViewportTracking_1 = tslib_1.__importDefault(require("../../hooks/useViewportTracking"));
13
+ const useViewportCallbacks_1 = tslib_1.__importDefault(require("../../hooks/useViewportCallbacks"));
14
+ const ErrorBlock_1 = tslib_1.__importDefault(require("../Error/ErrorBlock"));
15
+ const LoadingSkeleton_1 = tslib_1.__importDefault(require("../LoadingSkeleton/LoadingSkeleton"));
16
+ const translate_1 = require("../../utils/translate");
17
+ const PiaInlineAnswer_1 = tslib_1.__importDefault(require("../PiaInlineAnswer/PiaInlineAnswer"));
18
+ const PiaModal_1 = tslib_1.__importDefault(require("../PiaConversation/PiaModal"));
19
+ const PiaConversation_1 = tslib_1.__importDefault(require("../PiaConversation/PiaConversation"));
20
+ /** The question-and-answer experience: `mode: 'default'`, `mode: 'conversation'`, and the modal. */
21
+ // eslint-disable-next-line complexity
22
+ function CioPiaQna(props) {
23
+ var _a;
24
+ const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
25
+ const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
26
+ const isConversation = mode === 'conversation' || type === 'modal';
27
+ const { priceCurrency } = productCardProps || {};
28
+ const pia = (0, useCioPia_1.default)({
29
+ apiKey,
30
+ itemId,
31
+ threadId,
32
+ variationId,
33
+ cioClient,
34
+ suggestedQuestionsParameters,
35
+ answerParameters,
36
+ parameters,
37
+ formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
38
+ });
39
+ const tracking = (0, useTracking_1.default)({
40
+ cioClient: pia.cioClient,
41
+ itemId,
42
+ itemName,
43
+ variationId,
44
+ threadId: pia.threadId,
45
+ });
46
+ const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, handleFeedback, resetState, } = (0, useConversation_1.default)({ pia, itemId, isConversation, callbacks, tracking });
47
+ const { containerRef: viewportContainerRef } = (0, useViewportTracking_1.default)({
48
+ tracking,
49
+ questions: displayedQuestions,
50
+ viewThreshold: trackingConfigs === null || trackingConfigs === void 0 ? void 0 : trackingConfigs.viewThreshold,
51
+ });
52
+ const { containerRef: callbackContainerRef } = (0, useViewportCallbacks_1.default)({ callbacks, context });
53
+ const containerRef = (0, react_1.useCallback)((node) => {
54
+ viewportContainerRef(node);
55
+ callbackContainerRef(node);
56
+ }, [viewportContainerRef, callbackContainerRef]);
57
+ const qnaResultId = (_a = pia.answers.data) === null || _a === void 0 ? void 0 : _a.qna_result_id;
58
+ // The text bars stand in for the answer alone, so they track the answer request only. The question
59
+ // row keeps the combined `isLoading`: the follow-up questions that refill it arrive with the
60
+ // answer, so the row is waiting on either request.
61
+ const isAnswerLoading = pia.answers.isLoading;
62
+ const renderProps = {
63
+ items: currentItems,
64
+ isLoading,
65
+ error,
66
+ currentAnswer,
67
+ currentQuestion,
68
+ displayedQuestions,
69
+ handleSubmitQuestion,
70
+ conversationHistory,
71
+ };
72
+ const conversationHistoryProps = {
73
+ conversationHistory,
74
+ isLoading,
75
+ error,
76
+ currentItems,
77
+ showFeedback,
78
+ showPreviousItems,
79
+ learnMoreUrl,
80
+ disclaimerPosition,
81
+ translations,
82
+ callbacks,
83
+ componentOverrides,
84
+ displayedQuestions,
85
+ handleSubmitQuestion,
86
+ handleQuestionClick,
87
+ handleFeedback,
88
+ onResultClick: tracking.trackResultClick,
89
+ qnaResultId,
90
+ containerRef,
91
+ onInputFocus: handleInputFocus,
92
+ priceCurrency,
93
+ };
94
+ if (type === 'modal') {
95
+ return (react_1.default.createElement(PiaModal_1.default, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
96
+ react_1.default.createElement(PiaConversation_1.default, Object.assign({}, conversationHistoryProps))));
97
+ }
98
+ if (isConversation)
99
+ return react_1.default.createElement(PiaConversation_1.default, Object.assign({}, conversationHistoryProps));
100
+ // Default inline mode
101
+ return (react_1.default.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
102
+ react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: renderProps, override: children || (componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.reactNode) },
103
+ react_1.default.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, (0, translate_1.translate)('Any questions about this product?', translations)),
104
+ react_1.default.createElement(Input_1.default, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input }),
105
+ isAnswerLoading && react_1.default.createElement(LoadingSkeleton_1.default, { componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.loading }),
106
+ !isAnswerLoading && error && react_1.default.createElement(ErrorBlock_1.default, { message: (error === null || error === void 0 ? void 0 : error.message) || 'Unexpected error' }),
107
+ !isAnswerLoading && !error && currentAnswer && (react_1.default.createElement(PiaInlineAnswer_1.default, { currentAnswer: currentAnswer, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, onFeedback: handleFeedback, onResultClick: tracking.trackResultClick, question: currentQuestion, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
108
+ isLoading && react_1.default.createElement(SuggestedQuestionsSkeleton_1.default, null),
109
+ !isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions })))));
110
+ }
111
+ exports.default = CioPiaQna;
@@ -30,3 +30,22 @@
30
30
  .cio-pia-learn-more {
31
31
  color: inherit;
32
32
  }
33
+
34
+ /* Neutral placeholder shown until the product image itself paints over it.
35
+ Set on the image element so it inherits the card's own size and corner radius. */
36
+ .cio-pia-answer-container .cio-product-card-image {
37
+ background-color: rgba(0, 0, 0, 0.05);
38
+ }
39
+
40
+ /* Restores the clamped, muted look of the stock ProductCard description, which the
41
+ custom HTML description rendering does not inherit. */
42
+ .cio-pia-answer-container .cio-product-card-description {
43
+ display: -webkit-box;
44
+ -webkit-line-clamp: 3;
45
+ -webkit-box-orient: vertical;
46
+ line-clamp: 3;
47
+ overflow: hidden;
48
+ font-size: 14px;
49
+ line-height: 20px;
50
+ color: #6b7280;
51
+ }
@@ -46,12 +46,10 @@ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
46
46
  return Object.assign(Object.assign({}, componentOverrides), { item: Object.assign(Object.assign({}, componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item), { productCard: Object.assign(Object.assign({}, (_c = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _c === void 0 ? void 0 : _c.productCard), { content: Object.assign(Object.assign({}, content), defaults) }) }) });
47
47
  }
48
48
  function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
49
- const wrapperRef = (0, react_1.useRef)(null);
50
49
  const { onAddToCart } = callbacks || {};
51
50
  const addToCartHandler = (0, react_1.useCallback)((event, product) => {
52
51
  onAddToCart === null || onAddToCart === void 0 ? void 0 : onAddToCart(product, event);
53
52
  }, [onAddToCart]);
54
- // Determine to use user-defined click handler or default behavior
55
53
  const productClickHandler = (0, react_1.useCallback)((item) => {
56
54
  const position = items.findIndex((i) => i.id === item.id);
57
55
  if (onResultClick && question) {
@@ -65,40 +63,37 @@ function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick
65
63
  window.open(item.url, '_blank', 'noopener,noreferrer');
66
64
  }
67
65
  }, [callbacks, items, onResultClick, question, qnaResultId]);
68
- // Set up event listener for product card clicks
69
- (0, react_1.useEffect)(() => {
70
- const el = wrapperRef.current;
71
- if (!el)
72
- return undefined;
73
- const handleClick = (e) => {
74
- const { product } = e.detail;
75
- if (product) {
76
- productClickHandler(product);
77
- }
78
- };
79
- el.addEventListener(constructorio_ui_components_1.CIO_EVENTS.productCard.click, handleClick);
80
- return () => {
81
- el.removeEventListener(constructorio_ui_components_1.CIO_EVENTS.productCard.click, handleClick);
82
- };
83
- }, [productClickHandler]);
84
66
  const priceSectionOverride = (0, react_1.useMemo)(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
67
+ const clickHandlerProps = (0, react_1.useMemo)(() => ({
68
+ onProductClick: productClickHandler,
69
+ onAddToCart: onAddToCart ? addToCartHandler : undefined,
70
+ }), [productClickHandler, onAddToCart, addToCartHandler]);
85
71
  const mergedOverrides = (0, react_1.useMemo)(() => {
86
- var _a;
72
+ var _a, _b;
87
73
  const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
74
+ // Override priority: carousel.reactNode → item.reactNode → item.productCard.reactNode → default ProductCard.
75
+ // The casts are required because the override signatures don't declare these extra props,
76
+ // but consumers receive them as part of the render-props contract documented in CioPiaProps.
77
+ const carouselOverride = baseOverrides.reactNode;
78
+ if (typeof carouselOverride === 'function') {
79
+ return Object.assign(Object.assign({}, baseOverrides), { reactNode: (props) => carouselOverride(Object.assign(Object.assign({}, props), clickHandlerProps)) });
80
+ }
88
81
  const cardOverrides = (_a = baseOverrides.item) === null || _a === void 0 ? void 0 : _a.productCard;
89
- // The carousel never passes onAddToCart down and the card hides the button without it, so
90
- // re-render the card with the handler. A full card override owns its own layout - skip it.
91
- if (!onAddToCart || (cardOverrides === null || cardOverrides === void 0 ? void 0 : cardOverrides.reactNode)) {
92
- return baseOverrides;
82
+ const itemOverride = (_b = baseOverrides.item) === null || _b === void 0 ? void 0 : _b.reactNode;
83
+ if (typeof itemOverride === 'function') {
84
+ return Object.assign(Object.assign({}, baseOverrides), { item: Object.assign(Object.assign({}, baseOverrides.item), { reactNode: (props) => itemOverride(Object.assign(Object.assign({}, props), clickHandlerProps)) }) });
85
+ }
86
+ if (typeof (cardOverrides === null || cardOverrides === void 0 ? void 0 : cardOverrides.reactNode) === 'function') {
87
+ const originalReactNode = cardOverrides.reactNode;
88
+ return Object.assign(Object.assign({}, baseOverrides), { item: Object.assign(Object.assign({}, baseOverrides.item), { productCard: Object.assign(Object.assign({}, cardOverrides), { reactNode: (props) => originalReactNode(Object.assign(Object.assign({}, props), clickHandlerProps)) }) }) });
93
89
  }
94
- const addToCartText = (0, translate_1.translate)('Add to Cart', translations);
95
- return Object.assign(Object.assign({}, baseOverrides), { item: Object.assign(Object.assign({}, baseOverrides.item), { productCard: Object.assign(Object.assign({}, cardOverrides), { reactNode: ({ product }) => (react_1.default.createElement(constructorio_ui_components_1.ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: addToCartHandler, componentOverrides: cardOverrides })) }) }) });
96
- }, [componentOverrides, priceSectionOverride, onAddToCart, translations, addToCartHandler]);
97
- // If there are no items, do not render the carousel
90
+ // Default: render ProductCard with handlers directly
91
+ const addToCartText = onAddToCart ? (0, translate_1.translate)('Add to Cart', translations) : undefined;
92
+ return Object.assign(Object.assign({}, baseOverrides), { item: Object.assign(Object.assign({}, baseOverrides.item), { productCard: Object.assign(Object.assign({}, cardOverrides), { reactNode: ({ product }) => (react_1.default.createElement(constructorio_ui_components_1.ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: clickHandlerProps.onAddToCart, onProductClick: clickHandlerProps.onProductClick, componentOverrides: cardOverrides })) }) }) });
93
+ }, [componentOverrides, priceSectionOverride, clickHandlerProps, onAddToCart, translations]);
98
94
  if (items.length === 0) {
99
95
  return null;
100
96
  }
101
- return (react_1.default.createElement("div", { ref: wrapperRef },
102
- react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: items, componentOverrides: mergedOverrides })));
97
+ return react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: items, componentOverrides: mergedOverrides });
103
98
  }
104
99
  exports.default = PiaCustomCarousel;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const react_1 = tslib_1.__importStar(require("react"));
5
+ const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
+ const PiaCustomCarousel_1 = tslib_1.__importDefault(require("../CioPia/PiaCustomCarousel"));
7
+ const RecsPodRefinement_1 = tslib_1.__importDefault(require("./RecsPodRefinement"));
8
+ const RecsCarouselSkeleton_1 = tslib_1.__importDefault(require("./RecsCarouselSkeleton"));
9
+ const usePiaClient_1 = tslib_1.__importDefault(require("../../hooks/usePiaClient"));
10
+ const useRecsPod_1 = tslib_1.__importDefault(require("../../hooks/useRecsPod"));
11
+ const useViewportCallbacks_1 = tslib_1.__importDefault(require("../../hooks/useViewportCallbacks"));
12
+ const classNames_1 = require("../../utils/classNames");
13
+ /**
14
+ * A pod of AI-curated product suggestions: a personalized title, a row of products, and short
15
+ * options for narrowing them down. It asks no questions and makes no Q&A requests, so a retailer
16
+ * can use it on its own.
17
+ *
18
+ * Renders nothing at all once a request has settled with no products, so whatever the retailer
19
+ * already had in that slot shows through instead of an empty shell. That is the default: a caller
20
+ * that supplied its own root — `children` or `componentOverrides.reactNode` — still gets it in that
21
+ * state, with `error` available, and decides what belongs there.
22
+ */
23
+ function CioPiaRecs(props) {
24
+ const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, } = props;
25
+ const { priceCurrency } = productCardProps || {};
26
+ const { showInput = true, numResults } = recsPodParameters || {};
27
+ const { answer: answerOverride, carousel: carouselOverride, loading: loadingOverride, reactNode: rootOverride, } = componentOverrides || {};
28
+ const { cioClient: client, threadId: resolvedThreadId } = (0, usePiaClient_1.default)({
29
+ apiKey,
30
+ threadId,
31
+ cioClient,
32
+ });
33
+ const { title, items, refinement, isLoading, error, inputError, lastShopperInput, refine } = (0, useRecsPod_1.default)({
34
+ itemId,
35
+ variationId,
36
+ threadId: resolvedThreadId,
37
+ cioClient: client,
38
+ parameters: recsPodParameters,
39
+ formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
40
+ translations,
41
+ });
42
+ const context = (0, react_1.useMemo)(() => ({ itemId, threadId: resolvedThreadId }), [itemId, resolvedThreadId]);
43
+ const { containerRef } = (0, useViewportCallbacks_1.default)({ callbacks, context });
44
+ const handleInputFocus = (0, react_1.useCallback)(() => { var _a; return (_a = callbacks === null || callbacks === void 0 ? void 0 : callbacks.onFocus) === null || _a === void 0 ? void 0 : _a.call(callbacks, context); }, [callbacks, context]);
45
+ const refineFromOption = (0, react_1.useCallback)((text) => refine(text, 'option'), [refine]);
46
+ const refineFromInput = (0, react_1.useCallback)((text) => refine(text, 'input'), [refine]);
47
+ const renderProps = {
48
+ items,
49
+ isLoading,
50
+ error,
51
+ currentAnswer: title,
52
+ currentQuestion: lastShopperInput,
53
+ displayedQuestions: ((refinement === null || refinement === void 0 ? void 0 : refinement.options) || []).map((value) => ({ value })),
54
+ // A caller's own root gets the free-text behavior: whatever it submits is the shopper's, so a
55
+ // rejection belongs under their input.
56
+ handleSubmitQuestion: refineFromInput,
57
+ conversationHistory: [],
58
+ };
59
+ // Nothing of ours to show. Rendering no markup at all is the default, so whatever the retailer
60
+ // already had in that slot shows through instead of an empty shell. TODO: an interim, not a
61
+ // settled decision — the empty-state design and the shape of a hard failure are both still open.
62
+ // A caller who supplied its own root still gets it, with `error` in hand, so a consumer can put
63
+ // its own message here.
64
+ if (!isLoading && !items && !children && !rootOverride)
65
+ return null;
66
+ return (react_1.default.createElement("div", { ref: containerRef, className: (0, classNames_1.cx)('cio-pia-container', 'cio-pia-recs-pod', isLoading && 'cio-pia-recs-pod--loading'), "data-testid": 'cio-pia-recs-pod' },
67
+ react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: renderProps, override: children || rootOverride },
68
+ title && (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: { text: title }, override: answerOverride === null || answerOverride === void 0 ? void 0 : answerOverride.reactNode },
69
+ react_1.default.createElement("h3", { key: title, className: 'cio-pia-recs-pod__title' }, title))),
70
+ react_1.default.createElement("div", { className: 'cio-pia-recs-pod__products', "data-testid": 'cio-pia-recs-pod-products' }, isLoading || !items ? (react_1.default.createElement(RecsCarouselSkeleton_1.default, { count: numResults !== null && numResults !== void 0 ? numResults : items === null || items === void 0 ? void 0 : items.length, componentOverride: loadingOverride })) : (react_1.default.createElement(PiaCustomCarousel_1.default, { items: items, componentOverrides: carouselOverride, callbacks: callbacks, translations: translations, priceCurrency: priceCurrency }))),
71
+ react_1.default.createElement(RecsPodRefinement_1.default, { refinement: refinement, isLoading: isLoading, inputError: inputError, showInput: showInput, translations: translations, componentOverrides: componentOverrides, onRefine: refineFromOption, onSubmit: refineFromInput, onInputFocus: handleInputFocus }))));
72
+ }
73
+ exports.default = CioPiaRecs;
@@ -0,0 +1,138 @@
1
+ .cio-pia-recs-pod {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 12px;
5
+ width: 100%;
6
+ }
7
+
8
+ /*
9
+ * The title is the one part of the pod that changes on a refinement, so it fades in rather than
10
+ * snapping. `CioPiaRecs` keys the node on the title, which remounts it and replays this.
11
+ *
12
+ * This is a fade-in on the arriving title, not a cross-fade: React removes the outgoing text
13
+ * synchronously, so there is nothing left on screen to fade out.
14
+ */
15
+ .cio-pia-recs-pod__title {
16
+ min-height: 20px;
17
+ margin: 0;
18
+ font-size: 16px;
19
+ font-weight: 500;
20
+ color: #14151a;
21
+ text-align: start;
22
+ animation: recs-pod-title-fade-in 200ms ease-in;
23
+ }
24
+
25
+ @keyframes recs-pod-title-fade-in {
26
+ from {
27
+ opacity: 0;
28
+ }
29
+
30
+ to {
31
+ opacity: 1;
32
+ }
33
+ }
34
+
35
+ /* Holds the products, and the placeholders that stand in for them while a request is in flight */
36
+ .cio-pia-recs-pod__products {
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* Options and the free-text box share one row, wrapping on narrow screens */
42
+ .cio-pia-recs-pod__refinement {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ align-items: center;
46
+ gap: 8px;
47
+ }
48
+
49
+ .cio-pia-recs-pod__refinement-label {
50
+ font-size: 12px;
51
+ font-weight: 500;
52
+ color: #0f1324;
53
+ }
54
+
55
+ .cio-pia-recs-pod__input {
56
+ display: flex;
57
+ flex-direction: column;
58
+ flex: 1 1 200px;
59
+ min-width: 200px;
60
+ }
61
+
62
+ /* Nothing in the row is clickable while its contents are being replaced */
63
+ .cio-pia-recs-pod--loading .cio-pia-recs-pod__refinement {
64
+ pointer-events: none;
65
+ }
66
+
67
+ /* Placeholders, shaped like the real content so the page does not jump when data lands */
68
+ .cio-pia-recs-skeleton {
69
+ display: flex;
70
+ gap: 8px;
71
+ width: 100%;
72
+ }
73
+
74
+ /*
75
+ * This one holds the real carousel, arrows and all, so nothing in it is clickable while its
76
+ * contents are being replaced.
77
+ */
78
+ .cio-pia-recs-skeleton--carousel {
79
+ flex: 1 1 auto;
80
+ pointer-events: none;
81
+ }
82
+
83
+ /*
84
+ * The only two measurements taken from the package, both from the card's own image
85
+ * (`min-h-[224px] rounded-2xl`). The image is what gives a card its height, so these are what stop
86
+ * the refinement row moving when the products land. Everything else about the card - its
87
+ * background, radius, shadow, padding, and how many fit across - comes from the carousel itself.
88
+ */
89
+ .cio-pia-recs-skeleton__image {
90
+ min-height: 224px;
91
+ border-radius: 16px;
92
+ }
93
+
94
+ /* Bar heights are the line boxes of the text they stand in for, so the card totals the same */
95
+ .cio-pia-recs-skeleton__price {
96
+ width: 40%;
97
+ height: 28px;
98
+ }
99
+
100
+ .cio-pia-recs-skeleton__title {
101
+ width: 80%;
102
+ height: 24px;
103
+ }
104
+
105
+ .cio-pia-recs-skeleton__desc {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 4px;
109
+ }
110
+
111
+ .cio-pia-recs-skeleton__desc .skeleton-bar {
112
+ height: 18px;
113
+ }
114
+
115
+ /* Short last line, the way a wrapped sentence ends */
116
+ .cio-pia-recs-skeleton__desc .skeleton-bar:last-child {
117
+ width: 60%;
118
+ }
119
+
120
+ .cio-pia-recs-skeleton--options {
121
+ width: auto;
122
+ }
123
+
124
+ .cio-pia-recs-skeleton--options .cio-pia-recs-skeleton__block {
125
+ height: 26px;
126
+ width: 96px;
127
+ }
128
+
129
+ @media (prefers-reduced-motion: reduce) {
130
+ .cio-pia-recs-skeleton__block,
131
+ .cio-pia-recs-skeleton__image,
132
+ .cio-pia-recs-skeleton__price,
133
+ .cio-pia-recs-skeleton__title,
134
+ .cio-pia-recs-skeleton__desc .skeleton-bar,
135
+ .cio-pia-recs-pod__title {
136
+ animation: none;
137
+ }
138
+ }
@@ -0,0 +1,58 @@
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
+ /** The widest band the carousel lays out. Narrower ones need no count: its viewport clips them. */
7
+ const DEFAULT_COUNT = 6;
8
+ /**
9
+ * Grey bars standing in for a card's own sections. The card around them - white background,
10
+ * rounded corners, shadow, padding - and the row it sits in are the real ones, so nothing here has
11
+ * to know their measurements.
12
+ *
13
+ * `RenderPropsWrapper` draws an override in place of a section rather than alongside it, so these
14
+ * appear even though the placeholder carries no image, price or name of its own.
15
+ *
16
+ * A caller's own `productCard` overrides are deliberately not merged in, unlike the real row in
17
+ * `PiaCustomCarousel`: the placeholders below are empty objects, so a caller's section renderer
18
+ * would be handed a product with every field `undefined`.
19
+ */
20
+ const CARD_SKELETON = {
21
+ item: {
22
+ productCard: {
23
+ image: {
24
+ reactNode: () => (react_1.default.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__image', "data-testid": 'cio-pia-recs-skeleton-card' })),
25
+ },
26
+ content: {
27
+ price: { reactNode: () => react_1.default.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__price' }) },
28
+ title: { reactNode: () => react_1.default.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__title' }) },
29
+ description: {
30
+ // Two lines, not one. The card clamps its description at three lines and stretches every
31
+ // card in a row to the tallest of them, so a single line lands short of where real
32
+ // products settle.
33
+ reactNode: () => (react_1.default.createElement("div", { className: 'cio-pia-recs-skeleton__desc' },
34
+ react_1.default.createElement("div", { className: 'skeleton-bar' }),
35
+ react_1.default.createElement("div", { className: 'skeleton-bar' }))),
36
+ },
37
+ },
38
+ },
39
+ },
40
+ };
41
+ /**
42
+ * Placeholders exist only to give the carousel something to lay out. They deliberately carry no
43
+ * `id` and no `name`: `ProductCard` spreads those into `data-cnstrc-item-*`, and React omits an
44
+ * attribute whose value is `undefined`, so a loading pod emits no product data attributes at all.
45
+ */
46
+ const placeholderItems = (count) => Array.from({ length: count }, () => ({}));
47
+ /**
48
+ * The product row, greyed out. It uses the real carousel rather than a set of measurements copied
49
+ * out of the package, so its padding, its gaps, how many cards it fits at the current width and its
50
+ * arrows are all right by construction. Matching the real heights is what keeps the page from
51
+ * jumping when the data lands.
52
+ */
53
+ function RecsCarouselSkeleton({ count, componentOverride, }) {
54
+ const skeleton = (react_1.default.createElement("div", { className: 'cio-pia-recs-skeleton cio-pia-recs-skeleton--carousel', "data-testid": 'cio-pia-recs-skeleton-carousel', "aria-hidden": 'true' },
55
+ react_1.default.createElement(constructorio_ui_components_1.Carousel, { items: placeholderItems(count !== null && count !== void 0 ? count : DEFAULT_COUNT), componentOverrides: CARD_SKELETON })));
56
+ return (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: { skeleton }, override: componentOverride === null || componentOverride === void 0 ? void 0 : componentOverride.reactNode }, skeleton));
57
+ }
58
+ exports.default = RecsCarouselSkeleton;
@@ -0,0 +1,18 @@
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 DEFAULT_COUNT = 3;
6
+ /**
7
+ * The refinement options, greyed out. Matching the real height is what keeps the row from
8
+ * collapsing and shifting the page when the options land.
9
+ *
10
+ * Takes no override, the same way `SuggestedQuestionsSkeleton` takes none: `componentOverrides.
11
+ * loading` stands in for the band above this one, and a caller who replaced it keeps these pills
12
+ * underneath, exactly as a caller who replaces the answer skeleton in question-and-answer keeps the
13
+ * question pills under theirs.
14
+ */
15
+ function RecsOptionsSkeleton({ count }) {
16
+ return (react_1.default.createElement("div", { className: 'cio-pia-recs-skeleton cio-pia-recs-skeleton--options', "data-testid": 'cio-pia-recs-skeleton-options', "aria-hidden": 'true' }, Array.from({ length: count !== null && count !== void 0 ? count : DEFAULT_COUNT }, (_, index) => (react_1.default.createElement("div", { key: index, className: 'skeleton-bar cio-pia-recs-skeleton__block' })))));
17
+ }
18
+ exports.default = RecsOptionsSkeleton;