@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
@@ -0,0 +1,27 @@
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 Input_1 = tslib_1.__importDefault(require("../Input/Input"));
6
+ const SuggestedQuestion_1 = tslib_1.__importDefault(require("../SuggestedQuestion/SuggestedQuestion"));
7
+ const icons_1 = require("../icons");
8
+ const RecsOptionsSkeleton_1 = tslib_1.__importDefault(require("./RecsOptionsSkeleton"));
9
+ const recs_1 = require("../../utils/recs");
10
+ const constants_1 = require("../../constants");
11
+ /**
12
+ * The row under the products: a prompt line, the short options the API suggested, and a box for
13
+ * anything it did not think of. The prompt line is held still while the rest reloads, so the row
14
+ * never changes height under the shopper's cursor.
15
+ */
16
+ function RecsPodRefinement({ refinement, isLoading, inputError, showInput, translations, componentOverrides, onRefine, onSubmit, onInputFocus, }) {
17
+ const options = (refinement === null || refinement === void 0 ? void 0 : refinement.options) || [];
18
+ return (react_1.default.createElement("div", { className: 'cio-pia-recs-pod__refinement' },
19
+ react_1.default.createElement("span", { className: 'cio-pia-recs-pod__refinement-label' }, (0, recs_1.resolveRefinementQuestion)(refinement, translations)),
20
+ isLoading ? (react_1.default.createElement(RecsOptionsSkeleton_1.default, { count: options.length || undefined })) : (options.map((option) => (react_1.default.createElement(SuggestedQuestion_1.default, { key: option, question: option, icon: react_1.default.createElement(icons_1.SparklesIcon, null), onClick: () => onRefine(option) })))),
21
+ showInput && (react_1.default.createElement("div", { className: 'cio-pia-recs-pod__input' },
22
+ react_1.default.createElement(Input_1.default, { onSubmit: onSubmit, onFocus: onInputFocus, disabled: isLoading, error: inputError !== null && inputError !== void 0 ? inputError : undefined, placeholderKey: constants_1.RECS_INPUT_PLACEHOLDER,
23
+ // The pod's box sits in a row beside the options and submits on Enter. Q&A keeps
24
+ // its button; this one has none in the mocks.
25
+ showSendButton: false, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.input })))));
26
+ }
27
+ exports.default = RecsPodRefinement;
@@ -3,10 +3,25 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importStar(require("react"));
5
5
  const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
+ const classNames_1 = require("../../utils/classNames");
6
7
  const translate_1 = require("../../utils/translate");
7
8
  const icons_1 = require("../icons");
8
- function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, }) {
9
+ /**
10
+ * Numbers the `Input`s mounted on the page, so each one can give its error message a DOM `id` no
11
+ * other input will collide with. Nothing to do with messages or errors themselves - the id is
12
+ * needed because that is what the field's `aria-describedby` has to point at.
13
+ *
14
+ * `useId` is the built-in way to do this, but it is React 18+ and this package supports React
15
+ * >= 16.12. Read once per instance through a lazy initializer, so the number an input gets holds
16
+ * across its re-renders.
17
+ */
18
+ let inputIdCounter = 0;
19
+ function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, placeholderKey = 'Ask anything', showSendButton = true, }) {
9
20
  const [value, setValue] = (0, react_1.useState)(providedValue || '');
21
+ const [errorId] = (0, react_1.useState)(() => {
22
+ inputIdCounter += 1;
23
+ return `cio-pia-input-${inputIdCounter}-error`;
24
+ });
10
25
  (0, react_1.useEffect)(() => {
11
26
  if (providedValue) {
12
27
  setValue(providedValue);
@@ -27,20 +42,23 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
27
42
  }
28
43
  };
29
44
  // Priority: translations > placeholder prop > default
30
- const resolvedPlaceholder = (translations === null || translations === void 0 ? void 0 : translations['Ask anything']) !== undefined
31
- ? (0, translate_1.translate)('Ask anything', translations)
32
- : (placeholder !== null && placeholder !== void 0 ? placeholder : (0, translate_1.translate)('Ask anything'));
45
+ const resolvedPlaceholder = (translations === null || translations === void 0 ? void 0 : translations[placeholderKey]) !== undefined
46
+ ? (0, translate_1.translate)(placeholderKey, translations)
47
+ : (placeholder !== null && placeholder !== void 0 ? placeholder : (0, translate_1.translate)(placeholderKey));
33
48
  return (react_1.default.createElement(constructorio_ui_components_1.RenderPropsWrapper, { props: {
34
49
  disabled,
35
50
  placeholder: resolvedPlaceholder,
36
51
  onSubmit: handleSubmit,
37
52
  onFocus,
38
53
  translations,
54
+ error,
39
55
  }, override: componentOverride === null || componentOverride === void 0 ? void 0 : componentOverride.reactNode },
40
- react_1.default.createElement("div", { className: 'cio-pia-input-container' },
41
- react_1.default.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, disabled: disabled, className: 'cio-pia-input' }),
42
- react_1.default.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
43
- (0, translate_1.translate)('Send', translations),
44
- react_1.default.createElement(icons_1.SendIcon, null)))));
56
+ react_1.default.createElement(react_1.default.Fragment, null,
57
+ react_1.default.createElement("div", { className: 'cio-pia-input-container' },
58
+ react_1.default.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, disabled: disabled, className: (0, classNames_1.cx)('cio-pia-input', error && 'cio-pia-input--error', !showSendButton && 'cio-pia-input--no-send'), "aria-invalid": error ? true : undefined, "aria-describedby": error ? errorId : undefined }),
59
+ showSendButton && (react_1.default.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
60
+ (0, translate_1.translate)('Send', translations),
61
+ react_1.default.createElement(icons_1.SendIcon, null)))),
62
+ error && (react_1.default.createElement("span", { className: 'cio-pia-input-error', id: errorId, role: 'alert' }, error)))));
45
63
  }
46
64
  exports.default = Input;
@@ -21,11 +21,38 @@
21
21
  1px 1px 1px 0px rgba(18, 18, 105, 0.05) inset;
22
22
  }
23
23
 
24
+ /*
25
+ * Input Without A Send Button
26
+ *
27
+ * The right padding above is room for the Send button. With no button there is nothing to keep
28
+ * clear of, and leaving it would truncate what the shopper typed well short of the border.
29
+ */
30
+ .cio-pia-input--no-send {
31
+ padding-right: 12px;
32
+ }
33
+
24
34
  /* Input Hover */
25
35
  .cio-pia-input:hover {
26
36
  border: 1px solid #c8cad0;
27
37
  }
28
38
 
39
+ /* Input Rejected Value */
40
+ .cio-pia-input--error,
41
+ .cio-pia-input--error:hover,
42
+ .cio-pia-input--error:focus {
43
+ border: 1px solid #d93025;
44
+ box-shadow: none;
45
+ }
46
+
47
+ /* Input Rejected Value Message */
48
+ .cio-pia-input-error {
49
+ display: block;
50
+ margin-top: 4px;
51
+ color: #d93025;
52
+ font-size: 12px;
53
+ line-height: 16px;
54
+ }
55
+
29
56
  /* Input Focus */
30
57
  .cio-pia-input:focus {
31
58
  outline: none;
@@ -3,10 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importDefault(require("react"));
5
5
  const icons_1 = require("../icons");
6
- function SuggestedQuestion({ question, onClick }) {
6
+ function SuggestedQuestion({ question, onClick, icon }) {
7
7
  return (react_1.default.createElement("button", { type: 'button', className: 'cio-pia-suggested-question', onClick: onClick },
8
- react_1.default.createElement("div", { className: 'cio-pia-suggested-question-icon' },
9
- react_1.default.createElement(icons_1.QuestionIcon, null)),
8
+ react_1.default.createElement("div", { className: 'cio-pia-suggested-question-icon' }, icon !== null && icon !== void 0 ? icon : react_1.default.createElement(icons_1.QuestionIcon, null)),
10
9
  question));
11
10
  }
12
11
  exports.default = SuggestedQuestion;
@@ -21,7 +21,7 @@
21
21
  }
22
22
 
23
23
  .cio-pia-suggested-question-skeleton {
24
- height: 28px;
24
+ height: 34px;
25
25
  border-radius: 4px;
26
26
  background-color: rgba(0, 0, 0, 0.05);
27
27
  animation: loading-animation 2s infinite ease-in-out;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DISCLAIMER_TEXT = exports.MOCK_QUESTIONS = exports.DEMO_QUESTION_ALTERNATIVE_PRODUCTS = exports.DEMO_QUESTION = exports.DEMO_ITEM_NAME = exports.DEMO_ITEM_ID = exports.DEMO_API_KEY = void 0;
3
+ exports.RECS_INPUT_PLACEHOLDER = exports.RECS_REFINEMENT_LABEL = exports.RECS_UNSUPPORTED_REQUEST = exports.RECS_FALLBACK_TITLE = exports.RECS_LOADING_TITLE = exports.DISCLAIMER_TEXT = exports.MOCK_QUESTIONS = exports.DEMO_QUESTION_ALTERNATIVE_PRODUCTS = exports.DEMO_QUESTION = exports.DEMO_ITEM_NAME = exports.DEMO_ITEM_ID = exports.DEMO_API_KEY = void 0;
4
4
  exports.DEMO_API_KEY = 'key_13TO6qN9H3BP3Wrn';
5
5
  exports.DEMO_ITEM_ID = '149100215';
6
6
  exports.DEMO_ITEM_NAME = 'Ensure Original Nutrition Shake';
@@ -15,3 +15,14 @@ exports.MOCK_QUESTIONS = [
15
15
  { value: 'What is the serving size and calorie count?' },
16
16
  ];
17
17
  exports.DISCLAIMER_TEXT = 'AI-generated answers aim to help, but they may occasionally miss details or be inaccurate. Double-check important information before purchasing.';
18
+ /**
19
+ * Recommendations pod strings the API cannot supply, because in these states there is either
20
+ * no response yet or no usable response at all. Everything else the pod shows comes from the
21
+ * API. All five are `Translations` keys, so reference the constants instead of retyping the
22
+ * literals - a mistyped key falls back to itself with no warning.
23
+ */
24
+ exports.RECS_LOADING_TITLE = 'Adapting recommendations to your preference';
25
+ exports.RECS_FALLBACK_TITLE = 'Best selling products';
26
+ exports.RECS_UNSUPPORTED_REQUEST = 'Unsupported request, try a different feature.';
27
+ exports.RECS_REFINEMENT_LABEL = "Not what you're looking for? Try:";
28
+ exports.RECS_INPUT_PLACEHOLDER = 'Describe something else...';
@@ -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) => {
@@ -1,38 +1,68 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- const react_1 = require("react");
5
- const MockConstructorIOClient_1 = tslib_1.__importDefault(require("./mocks/MockConstructorIOClient"));
6
4
  const useAnswerResults_1 = tslib_1.__importDefault(require("./useAnswerResults"));
5
+ const usePiaClient_1 = tslib_1.__importDefault(require("./usePiaClient"));
7
6
  const useSuggestedQuestions_1 = tslib_1.__importDefault(require("./useSuggestedQuestions"));
8
- const version_1 = tslib_1.__importDefault(require("../version"));
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
+ }
9
42
  function useCioPia(props) {
10
- const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, parameters, formatImageUrl, } = props;
11
- const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
12
- const threadId = providedThreadId || generatedThreadId;
13
- const client = (0, react_1.useMemo)(() => {
14
- if (providedClient)
15
- return providedClient;
16
- return new MockConstructorIOClient_1.default({
17
- apiKey,
18
- sendTrackingEvents: true,
19
- version: `cio-ui-pia-${version_1.default}`,
20
- });
21
- }, [apiKey, providedClient]);
43
+ const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
44
+ const { cioClient: client, threadId } = (0, usePiaClient_1.default)({
45
+ apiKey,
46
+ threadId: providedThreadId,
47
+ cioClient: providedClient,
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;
22
53
  const suggestedQuestions = (0, useSuggestedQuestions_1.default)({
23
54
  itemId,
24
55
  variationId,
25
56
  threadId,
26
57
  cioClient: client,
27
- parameters: suggestedQuestionsParameters,
28
- requestParameters: parameters,
58
+ parameters: mergedSuggestedParams,
29
59
  });
30
60
  const answers = (0, useAnswerResults_1.default)({
31
61
  itemId,
32
62
  variationId,
33
63
  threadId,
34
64
  cioClient: client,
35
- parameters,
65
+ parameters: mergedAnswerParams,
36
66
  formatImageUrl,
37
67
  });
38
68
  return {
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const react_1 = require("react");
5
+ const constructorio_client_javascript_1 = tslib_1.__importDefault(require("@constructor-io/constructorio-client-javascript"));
6
+ const version_1 = tslib_1.__importDefault(require("../version"));
7
+ function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
8
+ const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
9
+ const threadId = providedThreadId || generatedThreadId;
10
+ const client = (0, react_1.useMemo)(() => {
11
+ if (providedClient)
12
+ return providedClient;
13
+ return new constructorio_client_javascript_1.default({
14
+ apiKey,
15
+ sendTrackingEvents: true,
16
+ version: `cio-ui-pia-${version_1.default}`,
17
+ });
18
+ }, [apiKey, providedClient]);
19
+ return { cioClient: client, threadId };
20
+ }
21
+ exports.default = usePiaClient;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const react_1 = require("react");
4
+ const errors_1 = require("../errors");
5
+ const translate_1 = require("../utils/translate");
6
+ const constants_1 = require("../constants");
7
+ const DEFAULT_STRATEGY = 'complementary_items';
8
+ /**
9
+ * Owns everything a recommendations pod shows: one request on mount, one on every refinement,
10
+ * and the title that belongs to whichever of those is happening right now.
11
+ *
12
+ * The previous items are deliberately kept while a new request is in flight, so a refinement
13
+ * swaps the products without collapsing the row they sit in. The title is the one thing that does
14
+ * change, so the shopper can see the pod reacting to what they asked for.
15
+ */
16
+ function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, formatImageUrl, translations, }) {
17
+ var _a;
18
+ const [result, setResult] = (0, react_1.useState)(null);
19
+ const [isLoading, setIsLoading] = (0, react_1.useState)(true);
20
+ const [error, setError] = (0, react_1.useState)(null);
21
+ const [hasUnsupportedInput, setHasUnsupportedInput] = (0, react_1.useState)(false);
22
+ const [lastShopperInput, setLastShopperInput] = (0, react_1.useState)('');
23
+ // Counts requests so a slow response cannot overwrite a newer one. There is no
24
+ // AbortController in this library, so the older request still completes; its result is
25
+ // simply dropped.
26
+ const requestIdRef = (0, react_1.useRef)(0);
27
+ // Mirrored into a ref so it never becomes a fetch dependency. It is commonly written inline by
28
+ // the caller, which would give it a new identity on every render and refetch forever.
29
+ const formatImageUrlRef = (0, react_1.useRef)(formatImageUrl);
30
+ (0, react_1.useEffect)(() => {
31
+ formatImageUrlRef.current = formatImageUrl;
32
+ }, [formatImageUrl]);
33
+ const strategy = (parameters === null || parameters === void 0 ? void 0 : parameters.strategy) || DEFAULT_STRATEGY;
34
+ const { numResults, defaultTitle } = parameters || {};
35
+ const fetchResult = (0, react_1.useCallback)((shopperInput, source) => {
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
+ }
44
+ setIsLoading(false);
45
+ return;
46
+ }
47
+ requestIdRef.current += 1;
48
+ const requestId = requestIdRef.current;
49
+ const isCurrent = () => requestId === requestIdRef.current;
50
+ setIsLoading(true);
51
+ setError(null);
52
+ setHasUnsupportedInput(false);
53
+ getRecs
54
+ .call(cioClient.agent, {
55
+ itemId,
56
+ variationId,
57
+ threadId,
58
+ strategy,
59
+ shopperInput,
60
+ numResults,
61
+ formatImageUrl: formatImageUrlRef.current,
62
+ })
63
+ .then((fetchedResult) => {
64
+ if (!isCurrent())
65
+ return;
66
+ setResult(fetchedResult);
67
+ setLastShopperInput(shopperInput || '');
68
+ })
69
+ .catch((err) => {
70
+ if (!isCurrent())
71
+ return;
72
+ const failure = err instanceof Error ? err : new Error('Error fetching recommendations');
73
+ const status = failure instanceof errors_1.AgentRequestError ? failure.status : undefined;
74
+ if (status === 400) {
75
+ // A 400 means the request we built was malformed, which is our bug, not the
76
+ // shopper's. Make it loud so it is caught in development.
77
+ console.error('Constructor PIA: the recommendations request was rejected.', failure);
78
+ }
79
+ // A rejected input changes nothing but the input, so the response on screen stays.
80
+ // Only text the shopper wrote can be rejected this way: an option is a label the API
81
+ // suggested, so blaming their input for it would point at the wrong thing entirely.
82
+ if (status === 422 && source === 'input') {
83
+ setHasUnsupportedInput(true);
84
+ return;
85
+ }
86
+ setError(failure);
87
+ })
88
+ .finally(() => {
89
+ if (!isCurrent())
90
+ return;
91
+ setIsLoading(false);
92
+ });
93
+ },
94
+ // Every dependency is a primitive. `formatImageUrl` is read through a ref instead, so a
95
+ // caller writing it inline cannot restart the request on every render.
96
+ [cioClient, itemId, variationId, threadId, strategy, numResults]);
97
+ (0, react_1.useEffect)(() => {
98
+ fetchResult();
99
+ }, [fetchResult]);
100
+ const refine = (0, react_1.useCallback)((text, source) => {
101
+ const refinementText = text.trim();
102
+ if (!refinementText)
103
+ return;
104
+ fetchResult(refinementText, source);
105
+ }, [fetchResult]);
106
+ // The title belonging to the last response we kept. `defaultTitle` is the caller's last resort
107
+ // for a response that carried items but no title of its own.
108
+ const settledTitle = (result === null || result === void 0 ? void 0 : result.title) || defaultTitle || '';
109
+ let title = settledTitle;
110
+ if (isLoading) {
111
+ // The loading copy wins whether or not a title is held, so a refinement announces itself
112
+ // rather than leaving the previous personalization above products that no longer match it.
113
+ title = (0, translate_1.translate)(constants_1.RECS_LOADING_TITLE, translations);
114
+ }
115
+ else if (!hasUnsupportedInput && (error || (result === null || result === void 0 ? void 0 : result.status) === 'partial')) {
116
+ // Either the request failed or it came back degraded, so the personalized title cannot be
117
+ // trusted. Any products it did carry are still worth showing under a generic title.
118
+ title = (0, translate_1.translate)(constants_1.RECS_FALLBACK_TITLE, translations);
119
+ }
120
+ return {
121
+ title,
122
+ // Normalized so every caller can treat "nothing to render" as a single case. Our own adapter
123
+ // never returns an empty array, but `cioClient` is a public prop and a consumer-supplied client
124
+ // can. Note `[]` is truthy, so `|| null` would not catch it.
125
+ items: ((_a = result === null || result === void 0 ? void 0 : result.items) === null || _a === void 0 ? void 0 : _a.length) ? result.items : null,
126
+ refinement: (result === null || result === void 0 ? void 0 : result.refinement) || null,
127
+ isLoading,
128
+ error,
129
+ inputError: hasUnsupportedInput ? (0, translate_1.translate)(constants_1.RECS_UNSUPPORTED_REQUEST, translations) : null,
130
+ lastShopperInput,
131
+ refine,
132
+ };
133
+ }
134
+ exports.default = useRecsPod;
@@ -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]);
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
4
  const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
5
- function useViewportTracking({ tracking, questions, }) {
5
+ const DEFAULT_VIEW_THRESHOLD = 0.5;
6
+ function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, }) {
6
7
  const nodeRef = (0, react_1.useRef)(null);
7
8
  const observerRef = (0, react_1.useRef)(null);
8
9
  const intervalRef = (0, react_1.useRef)(null);
@@ -50,7 +51,7 @@ function useViewportTracking({ tracking, questions, }) {
50
51
  entryTimeRef.current = null;
51
52
  trackingRef.current.trackOutOfView();
52
53
  }
53
- }, { threshold: 0.5 });
54
+ }, { threshold: viewThreshold });
54
55
  observer.observe(element);
55
56
  observerRef.current = observer;
56
57
  intervalRef.current = setInterval(flush, FLUSH_INTERVAL_MS);
@@ -9,6 +9,7 @@
9
9
  @import './components/CioPia/CioPiaStyles.css';
10
10
  @import './components/ConversationHistory/ConversationHistoryStyles.css';
11
11
  @import './components/PiaConversation/PiaConversationStyles.css';
12
+ @import './components/CioPiaRecs/CioPiaRecsStyles.css';
12
13
 
13
14
  .cio-pia-container {
14
15
  font-family: 'Inter', sans-serif;
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);
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cx = void 0;
4
+ /**
5
+ * Joins class names, dropping the falsy ones, so a conditional class can be written inline.
6
+ *
7
+ * Deliberately not `clsx` or `classnames`: neither is a dependency of this package, and the copy of
8
+ * `clsx` in the tree belongs to `@constructor-io/constructorio-ui-components`, which is a peer
9
+ * dependency - importing it here would work locally and break in a consumer that hoists
10
+ * differently. Worth revisiting if this ever needs to accept objects or nested arrays.
11
+ */
12
+ const cx = (...parts) => parts.filter(Boolean).join(' ');
13
+ exports.cx = cx;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveRefinementQuestion = void 0;
4
+ const translate_1 = require("./translate");
5
+ const constants_1 = require("../constants");
6
+ /**
7
+ * The line that introduces the refinement options. Prefers the prompt the API sent, so our
8
+ * copy and theirs cannot drift.
9
+ */
10
+ function resolveRefinementQuestion(refinement, translations) {
11
+ return (refinement === null || refinement === void 0 ? void 0 : refinement.question) || (0, translate_1.translate)(constants_1.RECS_REFINEMENT_LABEL, translations);
12
+ }
13
+ exports.resolveRefinementQuestion = resolveRefinementQuestion;
@@ -11,6 +11,11 @@ const defaultTranslations = {
11
11
  'Learn More.': 'Learn More.',
12
12
  'Ask about this product': 'Ask about this product',
13
13
  'Add to Cart': 'Add to Cart',
14
+ [constants_1.RECS_LOADING_TITLE]: constants_1.RECS_LOADING_TITLE,
15
+ [constants_1.RECS_FALLBACK_TITLE]: constants_1.RECS_FALLBACK_TITLE,
16
+ [constants_1.RECS_UNSUPPORTED_REQUEST]: constants_1.RECS_UNSUPPORTED_REQUEST,
17
+ [constants_1.RECS_REFINEMENT_LABEL]: constants_1.RECS_REFINEMENT_LABEL,
18
+ [constants_1.RECS_INPUT_PLACEHOLDER]: constants_1.RECS_INPUT_PLACEHOLDER,
14
19
  };
15
20
  /**
16
21
  * Translates a word using the provided translations object.
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '2.10.4';
3
+ exports.default = '2.12.0';