@constructor-io/constructorio-ui-pia 2.11.0 → 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 (37) hide show
  1. package/dist/constructorio-ui-pia-bundled.js +60 -60
  2. package/lib/cjs/components/CioPia/CioPiaQna.js +2 -1
  3. package/lib/cjs/components/CioPia/PiaCustomCarousel.js +24 -29
  4. package/lib/cjs/hooks/useAnswerResults.js +4 -14
  5. package/lib/cjs/hooks/useCioPia.js +42 -4
  6. package/lib/cjs/hooks/usePiaClient.js +2 -7
  7. package/lib/cjs/hooks/useRecsPod.js +10 -5
  8. package/lib/cjs/hooks/useSuggestedQuestions.js +10 -26
  9. package/lib/cjs/types.js +0 -2
  10. package/lib/cjs/version.js +1 -1
  11. package/lib/mjs/components/CioPia/CioPiaQna.js +2 -1
  12. package/lib/mjs/components/CioPia/PiaCustomCarousel.js +43 -30
  13. package/lib/mjs/hooks/useAnswerResults.js +4 -13
  14. package/lib/mjs/hooks/useCioPia.js +43 -4
  15. package/lib/mjs/hooks/usePiaClient.js +2 -7
  16. package/lib/mjs/hooks/useRecsPod.js +9 -5
  17. package/lib/mjs/hooks/useSuggestedQuestions.js +12 -24
  18. package/lib/mjs/types.js +0 -1
  19. package/lib/mjs/version.js +1 -1
  20. package/lib/types/components/CioPia/types.d.ts +20 -5
  21. package/lib/types/hooks/useAnswerResults.d.ts +4 -4
  22. package/lib/types/hooks/useCioPia.d.ts +20 -5
  23. package/lib/types/hooks/usePiaClient.d.ts +4 -9
  24. package/lib/types/hooks/useRecsPod.d.ts +2 -2
  25. package/lib/types/hooks/useSuggestedQuestions.d.ts +3 -4
  26. package/lib/types/types.d.ts +43 -6
  27. package/lib/types/version.d.ts +1 -1
  28. package/package.json +2 -2
  29. package/lib/cjs/hooks/mocks/MockConstructorIOClient.js +0 -24
  30. package/lib/cjs/hooks/mocks/agent.js +0 -183
  31. package/lib/cjs/hooks/mocks/types.js +0 -2
  32. package/lib/mjs/hooks/mocks/MockConstructorIOClient.js +0 -38
  33. package/lib/mjs/hooks/mocks/agent.js +0 -173
  34. package/lib/mjs/hooks/mocks/types.js +0 -1
  35. package/lib/types/hooks/mocks/MockConstructorIOClient.d.ts +0 -14
  36. package/lib/types/hooks/mocks/agent.d.ts +0 -23
  37. package/lib/types/hooks/mocks/types.d.ts +0 -77
@@ -21,7 +21,7 @@ const PiaConversation_1 = tslib_1.__importDefault(require("../PiaConversation/Pi
21
21
  // eslint-disable-next-line complexity
22
22
  function CioPiaQna(props) {
23
23
  var _a;
24
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, parameters, trackingConfigs, } = props;
24
+ const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
25
25
  const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
26
26
  const isConversation = mode === 'conversation' || type === 'modal';
27
27
  const { priceCurrency } = productCardProps || {};
@@ -32,6 +32,7 @@ function CioPiaQna(props) {
32
32
  variationId,
33
33
  cioClient,
34
34
  suggestedQuestionsParameters,
35
+ answerParameters,
35
36
  parameters,
36
37
  formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
37
38
  });
@@ -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;
@@ -1,18 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
3
  const react_1 = require("react");
5
4
  const transformers_1 = require("../utils/transformers");
6
- const fetchAnswerResults = ({ client, itemId, question, variationId, threadId, parameters, }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
7
- const response = yield client.agent.getAnswerResults({
8
- itemId,
9
- variationId,
10
- threadId,
11
- question,
12
- parameters,
13
- });
14
- return response;
15
- });
16
5
  function useAnswerResults({ itemId, variationId, threadId, cioClient, parameters, formatImageUrl, }) {
17
6
  const [answerResults, setAnswerResults] = (0, react_1.useState)(null);
18
7
  const [isLoading, setIsLoading] = (0, react_1.useState)(false);
@@ -24,9 +13,10 @@ function useAnswerResults({ itemId, variationId, threadId, cioClient, parameters
24
13
  setIsLoading(true);
25
14
  setError(null);
26
15
  setAnswerResults(null);
27
- fetchAnswerResults({ client: cioClient, itemId, question, variationId, threadId, parameters })
28
- .then((fetchedAnswerResults) => {
29
- setAnswerResults(fetchedAnswerResults);
16
+ cioClient.agent.pia
17
+ .getAnswerResults(itemId, question, Object.assign({ threadId, variationId }, parameters))
18
+ .then((response) => {
19
+ setAnswerResults(response);
30
20
  setError(null);
31
21
  })
32
22
  .catch((err) => {
@@ -4,27 +4,65 @@ const tslib_1 = require("tslib");
4
4
  const useAnswerResults_1 = tslib_1.__importDefault(require("./useAnswerResults"));
5
5
  const usePiaClient_1 = tslib_1.__importDefault(require("./usePiaClient"));
6
6
  const useSuggestedQuestions_1 = tslib_1.__importDefault(require("./useSuggestedQuestions"));
7
+ // TODO: remove SUPPORTED_DEPRECATED_PARAMS and mapDeprecatedParameters once the
8
+ // deprecated `parameters` prop is removed from CioPiaProps.
9
+ const SUPPORTED_DEPRECATED_PARAMS = {
10
+ guard: { camelKey: 'guard', isObject: false },
11
+ num_results: { camelKey: 'numResults', isObject: false },
12
+ numResults: { camelKey: 'numResults', isObject: false },
13
+ pre_filter_expression: { camelKey: 'preFilterExpression', isObject: true },
14
+ preFilterExpression: { camelKey: 'preFilterExpression', isObject: true },
15
+ fmt_options: { camelKey: 'fmtOptions', isObject: true },
16
+ fmtOptions: { camelKey: 'fmtOptions', isObject: true },
17
+ };
18
+ function mapDeprecatedParameters(params) {
19
+ if (!params)
20
+ return undefined;
21
+ const mapped = {};
22
+ let hasEntries = false;
23
+ for (const [key, value] of Object.entries(params)) {
24
+ const config = SUPPORTED_DEPRECATED_PARAMS[key];
25
+ if (!config)
26
+ continue;
27
+ hasEntries = true;
28
+ if (config.isObject && typeof value === 'string') {
29
+ try {
30
+ mapped[config.camelKey] = JSON.parse(value);
31
+ }
32
+ catch (_a) {
33
+ mapped[config.camelKey] = value;
34
+ }
35
+ }
36
+ else {
37
+ mapped[config.camelKey] = value;
38
+ }
39
+ }
40
+ return hasEntries ? mapped : undefined;
41
+ }
7
42
  function useCioPia(props) {
8
- const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, parameters, formatImageUrl, } = props;
43
+ const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
9
44
  const { cioClient: client, threadId } = (0, usePiaClient_1.default)({
10
45
  apiKey,
11
46
  threadId: providedThreadId,
12
47
  cioClient: providedClient,
13
48
  });
49
+ const mappedDeprecated = mapDeprecatedParameters(parameters);
50
+ const mergedSuggestedParams = mappedDeprecated || suggestedQuestionsParameters
51
+ ? Object.assign(Object.assign({}, mappedDeprecated), suggestedQuestionsParameters) : undefined;
52
+ const mergedAnswerParams = mappedDeprecated || answerParameters ? Object.assign(Object.assign({}, mappedDeprecated), answerParameters) : undefined;
14
53
  const suggestedQuestions = (0, useSuggestedQuestions_1.default)({
15
54
  itemId,
16
55
  variationId,
17
56
  threadId,
18
57
  cioClient: client,
19
- parameters: suggestedQuestionsParameters,
20
- requestParameters: parameters,
58
+ parameters: mergedSuggestedParams,
21
59
  });
22
60
  const answers = (0, useAnswerResults_1.default)({
23
61
  itemId,
24
62
  variationId,
25
63
  threadId,
26
64
  cioClient: client,
27
- parameters,
65
+ parameters: mergedAnswerParams,
28
66
  formatImageUrl,
29
67
  });
30
68
  return {
@@ -2,20 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = require("react");
5
- const MockConstructorIOClient_1 = tslib_1.__importDefault(require("./mocks/MockConstructorIOClient"));
5
+ const constructorio_client_javascript_1 = tslib_1.__importDefault(require("@constructor-io/constructorio-client-javascript"));
6
6
  const version_1 = tslib_1.__importDefault(require("../version"));
7
- /**
8
- * Resolves the client and thread ID every mode needs: the caller's client when they supplied
9
- * one, otherwise a client built from the API key, plus a generated thread ID that stays stable
10
- * for the life of the component.
11
- */
12
7
  function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
13
8
  const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
14
9
  const threadId = providedThreadId || generatedThreadId;
15
10
  const client = (0, react_1.useMemo)(() => {
16
11
  if (providedClient)
17
12
  return providedClient;
18
- return new MockConstructorIOClient_1.default({
13
+ return new constructorio_client_javascript_1.default({
19
14
  apiKey,
20
15
  sendTrackingEvents: true,
21
16
  version: `cio-ui-pia-${version_1.default}`,
@@ -33,9 +33,14 @@ function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, form
33
33
  const strategy = (parameters === null || parameters === void 0 ? void 0 : parameters.strategy) || DEFAULT_STRATEGY;
34
34
  const { numResults, defaultTitle } = parameters || {};
35
35
  const fetchResult = (0, react_1.useCallback)((shopperInput, source) => {
36
- // Nothing to ask, so settle the loading state rather than leaving the caller in a skeleton
37
- // that never resolves.
38
- if (!cioClient) {
36
+ var _a;
37
+ // TODO: once getRecs is added to the JS client SDK, replace this guard+cast
38
+ // with a direct call: cioClient.agent.getRecs({...})
39
+ const getRecs = (_a = cioClient === null || cioClient === void 0 ? void 0 : cioClient.agent) === null || _a === void 0 ? void 0 : _a.getRecs;
40
+ if (!cioClient || !getRecs) {
41
+ if (cioClient && !getRecs) {
42
+ console.info('[CioPia] getRecs is not available on the client SDK yet.');
43
+ }
39
44
  setIsLoading(false);
40
45
  return;
41
46
  }
@@ -45,8 +50,8 @@ function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, form
45
50
  setIsLoading(true);
46
51
  setError(null);
47
52
  setHasUnsupportedInput(false);
48
- cioClient.agent
49
- .getRecs({
53
+ getRecs
54
+ .call(cioClient.agent, {
50
55
  itemId,
51
56
  variationId,
52
57
  threadId,
@@ -1,38 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
3
  const react_1 = require("react");
5
- const fetchSuggestedQuestions = ({ client, itemId, variationId, threadId, parameters, requestParameters, }) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
6
- const response = yield client.agent.getSuggestedQuestions({
7
- itemId,
8
- variationId,
9
- threadId,
10
- parameters,
11
- requestParameters,
12
- });
13
- return response.questions;
14
- });
15
- function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, requestParameters, }) {
4
+ function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, }) {
16
5
  const [questions, setQuestions] = (0, react_1.useState)([]);
17
6
  const [isLoading, setIsLoading] = (0, react_1.useState)(false);
18
7
  const [error, setError] = (0, react_1.useState)(null);
19
- // Serialize to a primitive so an object-literal prop doesn't retrigger the auto-fetch effect each render.
20
- const requestParametersKey = JSON.stringify(requestParameters);
8
+ const serializedParameters = (0, react_1.useMemo)(() => JSON.stringify(parameters), [parameters]);
21
9
  const fetchResult = (0, react_1.useCallback)(() => {
22
10
  if (!cioClient)
23
11
  return;
24
12
  setIsLoading(true);
25
13
  setError(null);
26
- fetchSuggestedQuestions({
27
- client: cioClient,
28
- itemId,
29
- variationId,
30
- threadId,
31
- parameters,
32
- requestParameters,
33
- })
34
- .then((fetchedQuestions) => {
35
- setQuestions(fetchedQuestions);
14
+ cioClient.agent.pia
15
+ .getSuggestedQuestions(itemId, Object.assign({ threadId,
16
+ variationId }, parameters))
17
+ .then((response) => {
18
+ setQuestions(response.questions);
36
19
  setError(null);
37
20
  })
38
21
  .catch((err) => {
@@ -41,8 +24,9 @@ function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, param
41
24
  .finally(() => {
42
25
  setIsLoading(false);
43
26
  });
44
- // eslint-disable-next-line react-hooks/exhaustive-deps -- primitive deps prevent refetch loop
45
- }, [cioClient, itemId, variationId, threadId, parameters === null || parameters === void 0 ? void 0 : parameters.numResults, requestParametersKey]);
27
+ // parameters is serialized via serializedParameters to prevent refetch on identity changes
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [cioClient, itemId, variationId, threadId, serializedParameters]);
46
30
  (0, react_1.useEffect)(() => {
47
31
  fetchResult();
48
32
  }, [fetchResult]);
package/lib/cjs/types.js CHANGED
@@ -1,10 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FeedbackType = void 0;
4
- const tslib_1 = require("tslib");
5
4
  var FeedbackType;
6
5
  (function (FeedbackType) {
7
6
  FeedbackType["UP"] = "up";
8
7
  FeedbackType["DOWN"] = "down";
9
8
  })(FeedbackType = exports.FeedbackType || (exports.FeedbackType = {}));
10
- tslib_1.__exportStar(require("./hooks/mocks/types"), exports);
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '2.11.0';
3
+ exports.default = '2.12.0';
@@ -17,7 +17,7 @@ import PiaConversation from '../PiaConversation/PiaConversation';
17
17
  /** The question-and-answer experience: `mode: 'default'`, `mode: 'conversation'`, and the modal. */
18
18
  // eslint-disable-next-line complexity
19
19
  export default function CioPiaQna(props) {
20
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, parameters, trackingConfigs, } = props;
20
+ const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
21
21
  const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
22
22
  const isConversation = mode === 'conversation' || type === 'modal';
23
23
  const { priceCurrency } = productCardProps || {};
@@ -28,6 +28,7 @@ export default function CioPiaQna(props) {
28
28
  variationId,
29
29
  cioClient,
30
30
  suggestedQuestionsParameters,
31
+ answerParameters,
31
32
  parameters,
32
33
  formatImageUrl: formatters?.formatImageUrl,
33
34
  });
@@ -1,5 +1,5 @@
1
- import React, { useCallback, useRef, useEffect, useMemo } from 'react';
2
- import { Carousel, ProductCard, CIO_EVENTS, } from '@constructor-io/constructorio-ui-components';
1
+ import React, { useCallback, useMemo } from 'react';
2
+ import { Carousel, ProductCard, } from '@constructor-io/constructorio-ui-components';
3
3
  import { sanitizeHtml } from '../../utils/contentTransformers';
4
4
  import { translate } from '../../utils/translate';
5
5
  function HtmlDescription({ product }) {
@@ -55,12 +55,10 @@ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
55
55
  };
56
56
  }
57
57
  export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
58
- const wrapperRef = useRef(null);
59
58
  const { onAddToCart } = callbacks || {};
60
59
  const addToCartHandler = useCallback((event, product) => {
61
60
  onAddToCart?.(product, event);
62
61
  }, [onAddToCart]);
63
- // Determine to use user-defined click handler or default behavior
64
62
  const productClickHandler = useCallback((item) => {
65
63
  const position = items.findIndex((i) => i.id === item.id);
66
64
  if (onResultClick && question) {
@@ -74,47 +72,62 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
74
72
  window.open(item.url, '_blank', 'noopener,noreferrer');
75
73
  }
76
74
  }, [callbacks, items, onResultClick, question, qnaResultId]);
77
- // Set up event listener for product card clicks
78
- useEffect(() => {
79
- const el = wrapperRef.current;
80
- if (!el)
81
- return undefined;
82
- const handleClick = (e) => {
83
- const { product } = e.detail;
84
- if (product) {
85
- productClickHandler(product);
86
- }
87
- };
88
- el.addEventListener(CIO_EVENTS.productCard.click, handleClick);
89
- return () => {
90
- el.removeEventListener(CIO_EVENTS.productCard.click, handleClick);
91
- };
92
- }, [productClickHandler]);
93
75
  const priceSectionOverride = useMemo(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
76
+ const clickHandlerProps = useMemo(() => ({
77
+ onProductClick: productClickHandler,
78
+ onAddToCart: onAddToCart ? addToCartHandler : undefined,
79
+ }), [productClickHandler, onAddToCart, addToCartHandler]);
94
80
  const mergedOverrides = useMemo(() => {
95
81
  const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
82
+ // Override priority: carousel.reactNode → item.reactNode → item.productCard.reactNode → default ProductCard.
83
+ // The casts are required because the override signatures don't declare these extra props,
84
+ // but consumers receive them as part of the render-props contract documented in CioPiaProps.
85
+ const carouselOverride = baseOverrides.reactNode;
86
+ if (typeof carouselOverride === 'function') {
87
+ return {
88
+ ...baseOverrides,
89
+ reactNode: (props) => carouselOverride({ ...props, ...clickHandlerProps }),
90
+ };
91
+ }
96
92
  const cardOverrides = baseOverrides.item?.productCard;
97
- // The carousel never passes onAddToCart down and the card hides the button without it, so
98
- // re-render the card with the handler. A full card override owns its own layout - skip it.
99
- if (!onAddToCart || cardOverrides?.reactNode) {
100
- return baseOverrides;
93
+ const itemOverride = baseOverrides.item?.reactNode;
94
+ if (typeof itemOverride === 'function') {
95
+ return {
96
+ ...baseOverrides,
97
+ item: {
98
+ ...baseOverrides.item,
99
+ reactNode: (props) => itemOverride({ ...props, ...clickHandlerProps }),
100
+ },
101
+ };
102
+ }
103
+ if (typeof cardOverrides?.reactNode === 'function') {
104
+ const originalReactNode = cardOverrides.reactNode;
105
+ return {
106
+ ...baseOverrides,
107
+ item: {
108
+ ...baseOverrides.item,
109
+ productCard: {
110
+ ...cardOverrides,
111
+ reactNode: (props) => originalReactNode({ ...props, ...clickHandlerProps }),
112
+ },
113
+ },
114
+ };
101
115
  }
102
- const addToCartText = translate('Add to Cart', translations);
116
+ // Default: render ProductCard with handlers directly
117
+ const addToCartText = onAddToCart ? translate('Add to Cart', translations) : undefined;
103
118
  return {
104
119
  ...baseOverrides,
105
120
  item: {
106
121
  ...baseOverrides.item,
107
122
  productCard: {
108
123
  ...cardOverrides,
109
- reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: addToCartHandler, componentOverrides: cardOverrides })),
124
+ reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: clickHandlerProps.onAddToCart, onProductClick: clickHandlerProps.onProductClick, componentOverrides: cardOverrides })),
110
125
  },
111
126
  },
112
127
  };
113
- }, [componentOverrides, priceSectionOverride, onAddToCart, translations, addToCartHandler]);
114
- // If there are no items, do not render the carousel
128
+ }, [componentOverrides, priceSectionOverride, clickHandlerProps, onAddToCart, translations]);
115
129
  if (items.length === 0) {
116
130
  return null;
117
131
  }
118
- return (React.createElement("div", { ref: wrapperRef },
119
- React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides })));
132
+ return React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides });
120
133
  }
@@ -1,15 +1,5 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
2
  import { extractAndTransformItems } from '../utils/transformers';
3
- const fetchAnswerResults = async ({ client, itemId, question, variationId, threadId, parameters, }) => {
4
- const response = await client.agent.getAnswerResults({
5
- itemId,
6
- variationId,
7
- threadId,
8
- question,
9
- parameters,
10
- });
11
- return response;
12
- };
13
3
  export default function useAnswerResults({ itemId, variationId, threadId, cioClient, parameters, formatImageUrl, }) {
14
4
  const [answerResults, setAnswerResults] = useState(null);
15
5
  const [isLoading, setIsLoading] = useState(false);
@@ -21,9 +11,10 @@ export default function useAnswerResults({ itemId, variationId, threadId, cioCli
21
11
  setIsLoading(true);
22
12
  setError(null);
23
13
  setAnswerResults(null);
24
- fetchAnswerResults({ client: cioClient, itemId, question, variationId, threadId, parameters })
25
- .then((fetchedAnswerResults) => {
26
- setAnswerResults(fetchedAnswerResults);
14
+ cioClient.agent.pia
15
+ .getAnswerResults(itemId, question, { threadId, variationId, ...parameters })
16
+ .then((response) => {
17
+ setAnswerResults(response);
27
18
  setError(null);
28
19
  })
29
20
  .catch((err) => {
@@ -1,27 +1,66 @@
1
1
  import useAnswerResults from './useAnswerResults';
2
2
  import usePiaClient from './usePiaClient';
3
3
  import useSuggestedQuestions from './useSuggestedQuestions';
4
+ // TODO: remove SUPPORTED_DEPRECATED_PARAMS and mapDeprecatedParameters once the
5
+ // deprecated `parameters` prop is removed from CioPiaProps.
6
+ const SUPPORTED_DEPRECATED_PARAMS = {
7
+ guard: { camelKey: 'guard', isObject: false },
8
+ num_results: { camelKey: 'numResults', isObject: false },
9
+ numResults: { camelKey: 'numResults', isObject: false },
10
+ pre_filter_expression: { camelKey: 'preFilterExpression', isObject: true },
11
+ preFilterExpression: { camelKey: 'preFilterExpression', isObject: true },
12
+ fmt_options: { camelKey: 'fmtOptions', isObject: true },
13
+ fmtOptions: { camelKey: 'fmtOptions', isObject: true },
14
+ };
15
+ function mapDeprecatedParameters(params) {
16
+ if (!params)
17
+ return undefined;
18
+ const mapped = {};
19
+ let hasEntries = false;
20
+ for (const [key, value] of Object.entries(params)) {
21
+ const config = SUPPORTED_DEPRECATED_PARAMS[key];
22
+ if (!config)
23
+ continue;
24
+ hasEntries = true;
25
+ if (config.isObject && typeof value === 'string') {
26
+ try {
27
+ mapped[config.camelKey] = JSON.parse(value);
28
+ }
29
+ catch {
30
+ mapped[config.camelKey] = value;
31
+ }
32
+ }
33
+ else {
34
+ mapped[config.camelKey] = value;
35
+ }
36
+ }
37
+ return hasEntries ? mapped : undefined;
38
+ }
4
39
  export default function useCioPia(props) {
5
- const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, parameters, formatImageUrl, } = props;
40
+ const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
6
41
  const { cioClient: client, threadId } = usePiaClient({
7
42
  apiKey,
8
43
  threadId: providedThreadId,
9
44
  cioClient: providedClient,
10
45
  });
46
+ const mappedDeprecated = mapDeprecatedParameters(parameters);
47
+ const mergedSuggestedParams = mappedDeprecated || suggestedQuestionsParameters
48
+ ? { ...mappedDeprecated, ...suggestedQuestionsParameters }
49
+ : undefined;
50
+ const mergedAnswerParams = mappedDeprecated || answerParameters ? { ...mappedDeprecated, ...answerParameters } : undefined;
11
51
  const suggestedQuestions = useSuggestedQuestions({
12
52
  itemId,
13
53
  variationId,
14
54
  threadId,
15
55
  cioClient: client,
16
- parameters: suggestedQuestionsParameters,
17
- requestParameters: parameters,
56
+ parameters: mergedSuggestedParams,
18
57
  });
19
58
  const answers = useAnswerResults({
20
59
  itemId,
21
60
  variationId,
22
61
  threadId,
23
62
  cioClient: client,
24
- parameters,
63
+ parameters: mergedAnswerParams,
25
64
  formatImageUrl,
26
65
  });
27
66
  return {
@@ -1,18 +1,13 @@
1
1
  import { useMemo, useState } from 'react';
2
- import MockConstructorIOClient from './mocks/MockConstructorIOClient';
2
+ import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
3
3
  import version from '../version';
4
- /**
5
- * Resolves the client and thread ID every mode needs: the caller's client when they supplied
6
- * one, otherwise a client built from the API key, plus a generated thread ID that stays stable
7
- * for the life of the component.
8
- */
9
4
  export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
10
5
  const [generatedThreadId] = useState(() => crypto.randomUUID());
11
6
  const threadId = providedThreadId || generatedThreadId;
12
7
  const client = useMemo(() => {
13
8
  if (providedClient)
14
9
  return providedClient;
15
- return new MockConstructorIOClient({
10
+ return new ConstructorIOClient({
16
11
  apiKey,
17
12
  sendTrackingEvents: true,
18
13
  version: `cio-ui-pia-${version}`,
@@ -30,9 +30,13 @@ export default function useRecsPod({ itemId, variationId, threadId, cioClient, p
30
30
  const strategy = parameters?.strategy || DEFAULT_STRATEGY;
31
31
  const { numResults, defaultTitle } = parameters || {};
32
32
  const fetchResult = useCallback((shopperInput, source) => {
33
- // Nothing to ask, so settle the loading state rather than leaving the caller in a skeleton
34
- // that never resolves.
35
- if (!cioClient) {
33
+ // TODO: once getRecs is added to the JS client SDK, replace this guard+cast
34
+ // with a direct call: cioClient.agent.getRecs({...})
35
+ const getRecs = cioClient?.agent?.getRecs;
36
+ if (!cioClient || !getRecs) {
37
+ if (cioClient && !getRecs) {
38
+ console.info('[CioPia] getRecs is not available on the client SDK yet.');
39
+ }
36
40
  setIsLoading(false);
37
41
  return;
38
42
  }
@@ -42,8 +46,8 @@ export default function useRecsPod({ itemId, variationId, threadId, cioClient, p
42
46
  setIsLoading(true);
43
47
  setError(null);
44
48
  setHasUnsupportedInput(false);
45
- cioClient.agent
46
- .getRecs({
49
+ getRecs
50
+ .call(cioClient.agent, {
47
51
  itemId,
48
52
  variationId,
49
53
  threadId,