@constructor-io/constructorio-ui-pia 2.10.4 → 2.11.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 (70) hide show
  1. package/README.md +7 -0
  2. package/dist/constructorio-ui-pia-bundled.js +50 -50
  3. package/lib/cjs/components/CioPia/CioPia.js +8 -96
  4. package/lib/cjs/components/CioPia/CioPiaQna.js +110 -0
  5. package/lib/cjs/components/CioPia/CioPiaStyles.css +19 -0
  6. package/lib/cjs/components/CioPia/types.js +2 -0
  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/mocks/agent.js +17 -0
  18. package/lib/cjs/hooks/useCioPia.js +6 -14
  19. package/lib/cjs/hooks/usePiaClient.js +26 -0
  20. package/lib/cjs/hooks/useRecsPod.js +129 -0
  21. package/lib/cjs/hooks/useViewportTracking.js +3 -2
  22. package/lib/cjs/styles.css +1 -0
  23. package/lib/cjs/utils/classNames.js +13 -0
  24. package/lib/cjs/utils/recs.js +13 -0
  25. package/lib/cjs/utils/translate.js +5 -0
  26. package/lib/cjs/version.js +1 -1
  27. package/lib/mjs/components/CioPia/CioPia.js +7 -95
  28. package/lib/mjs/components/CioPia/CioPiaQna.js +105 -0
  29. package/lib/mjs/components/CioPia/CioPiaStyles.css +19 -0
  30. package/lib/mjs/components/CioPia/types.js +1 -0
  31. package/lib/mjs/components/CioPiaRecs/CioPiaRecs.js +69 -0
  32. package/lib/mjs/components/CioPiaRecs/CioPiaRecsStyles.css +138 -0
  33. package/lib/mjs/components/CioPiaRecs/RecsCarouselSkeleton.js +54 -0
  34. package/lib/mjs/components/CioPiaRecs/RecsOptionsSkeleton.js +14 -0
  35. package/lib/mjs/components/CioPiaRecs/RecsPodRefinement.js +23 -0
  36. package/lib/mjs/components/Input/Input.js +27 -9
  37. package/lib/mjs/components/Input/InputStyles.css +27 -0
  38. package/lib/mjs/components/SuggestedQuestion/SuggestedQuestion.js +2 -3
  39. package/lib/mjs/components/SuggestedQuestionsContainer/SuggestedQuestionsContainerStyles.css +1 -1
  40. package/lib/mjs/constants.js +11 -0
  41. package/lib/mjs/hooks/mocks/agent.js +15 -0
  42. package/lib/mjs/hooks/useCioPia.js +6 -14
  43. package/lib/mjs/hooks/usePiaClient.js +22 -0
  44. package/lib/mjs/hooks/useRecsPod.js +125 -0
  45. package/lib/mjs/hooks/useViewportTracking.js +3 -2
  46. package/lib/mjs/styles.css +1 -0
  47. package/lib/mjs/utils/classNames.js +9 -0
  48. package/lib/mjs/utils/recs.js +9 -0
  49. package/lib/mjs/utils/translate.js +6 -1
  50. package/lib/mjs/version.js +1 -1
  51. package/lib/types/components/CioPia/CioPia.d.ts +1 -36
  52. package/lib/types/components/CioPia/CioPiaQna.d.ts +4 -0
  53. package/lib/types/components/CioPia/index.d.ts +1 -0
  54. package/lib/types/components/CioPia/types.d.ts +40 -0
  55. package/lib/types/components/CioPiaRecs/CioPiaRecs.d.ts +13 -0
  56. package/lib/types/components/CioPiaRecs/RecsCarouselSkeleton.d.ts +16 -0
  57. package/lib/types/components/CioPiaRecs/RecsOptionsSkeleton.d.ts +16 -0
  58. package/lib/types/components/CioPiaRecs/RecsPodRefinement.d.ts +21 -0
  59. package/lib/types/components/Input/Input.d.ts +9 -1
  60. package/lib/types/components/SuggestedQuestion/SuggestedQuestion.d.ts +4 -2
  61. package/lib/types/constants.d.ts +11 -0
  62. package/lib/types/hooks/mocks/agent.d.ts +13 -0
  63. package/lib/types/hooks/usePiaClient.d.ts +17 -0
  64. package/lib/types/hooks/useRecsPod.d.ts +52 -0
  65. package/lib/types/hooks/useViewportTracking.d.ts +2 -1
  66. package/lib/types/types.d.ts +76 -1
  67. package/lib/types/utils/classNames.d.ts +9 -0
  68. package/lib/types/utils/recs.d.ts +6 -0
  69. package/lib/types/version.d.ts +1 -1
  70. package/package.json +1 -1
@@ -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...';
@@ -116,6 +116,23 @@ class MockAgent {
116
116
  }
117
117
  });
118
118
  }
119
+ /**
120
+ * Fetches one set of recommendations, in the shape the pod renders.
121
+ *
122
+ * Recommendations are not available yet: there is no endpoint to ask, so nothing is requested and
123
+ * an empty result is returned. The pod renders nothing in that state, which leaves whatever the
124
+ * retailer already had in that slot showing through. Supply a `cioClient` with your own
125
+ * `agent.getRecs` to drive the pod from your own data in the meantime.
126
+ *
127
+ * `props` is still declared, because it is the contract a caller's own `getRecs` implements and
128
+ * `useRecsPod` passes it - there is simply nothing here to send it to yet.
129
+ */
130
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
131
+ getRecs(props) {
132
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
133
+ return { title: '', items: null, refinement: null, status: 'complete' };
134
+ });
135
+ }
119
136
  getAnswerResultsStream({ itemId, threadId, variationId, question, parameters, onStart, onMessage, onEnd, }) {
120
137
  return tslib_1.__awaiter(this, void 0, void 0, function* () {
121
138
  if (!itemId)
@@ -1,24 +1,16 @@
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"));
9
7
  function useCioPia(props) {
10
8
  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]);
9
+ const { cioClient: client, threadId } = (0, usePiaClient_1.default)({
10
+ apiKey,
11
+ threadId: providedThreadId,
12
+ cioClient: providedClient,
13
+ });
22
14
  const suggestedQuestions = (0, useSuggestedQuestions_1.default)({
23
15
  itemId,
24
16
  variationId,
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const react_1 = require("react");
5
+ const MockConstructorIOClient_1 = tslib_1.__importDefault(require("./mocks/MockConstructorIOClient"));
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
+ function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
13
+ const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
14
+ const threadId = providedThreadId || generatedThreadId;
15
+ const client = (0, react_1.useMemo)(() => {
16
+ if (providedClient)
17
+ return providedClient;
18
+ return new MockConstructorIOClient_1.default({
19
+ apiKey,
20
+ sendTrackingEvents: true,
21
+ version: `cio-ui-pia-${version_1.default}`,
22
+ });
23
+ }, [apiKey, providedClient]);
24
+ return { cioClient: client, threadId };
25
+ }
26
+ exports.default = usePiaClient;
@@ -0,0 +1,129 @@
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
+ // Nothing to ask, so settle the loading state rather than leaving the caller in a skeleton
37
+ // that never resolves.
38
+ if (!cioClient) {
39
+ setIsLoading(false);
40
+ return;
41
+ }
42
+ requestIdRef.current += 1;
43
+ const requestId = requestIdRef.current;
44
+ const isCurrent = () => requestId === requestIdRef.current;
45
+ setIsLoading(true);
46
+ setError(null);
47
+ setHasUnsupportedInput(false);
48
+ cioClient.agent
49
+ .getRecs({
50
+ itemId,
51
+ variationId,
52
+ threadId,
53
+ strategy,
54
+ shopperInput,
55
+ numResults,
56
+ formatImageUrl: formatImageUrlRef.current,
57
+ })
58
+ .then((fetchedResult) => {
59
+ if (!isCurrent())
60
+ return;
61
+ setResult(fetchedResult);
62
+ setLastShopperInput(shopperInput || '');
63
+ })
64
+ .catch((err) => {
65
+ if (!isCurrent())
66
+ return;
67
+ const failure = err instanceof Error ? err : new Error('Error fetching recommendations');
68
+ const status = failure instanceof errors_1.AgentRequestError ? failure.status : undefined;
69
+ if (status === 400) {
70
+ // A 400 means the request we built was malformed, which is our bug, not the
71
+ // shopper's. Make it loud so it is caught in development.
72
+ console.error('Constructor PIA: the recommendations request was rejected.', failure);
73
+ }
74
+ // A rejected input changes nothing but the input, so the response on screen stays.
75
+ // Only text the shopper wrote can be rejected this way: an option is a label the API
76
+ // suggested, so blaming their input for it would point at the wrong thing entirely.
77
+ if (status === 422 && source === 'input') {
78
+ setHasUnsupportedInput(true);
79
+ return;
80
+ }
81
+ setError(failure);
82
+ })
83
+ .finally(() => {
84
+ if (!isCurrent())
85
+ return;
86
+ setIsLoading(false);
87
+ });
88
+ },
89
+ // Every dependency is a primitive. `formatImageUrl` is read through a ref instead, so a
90
+ // caller writing it inline cannot restart the request on every render.
91
+ [cioClient, itemId, variationId, threadId, strategy, numResults]);
92
+ (0, react_1.useEffect)(() => {
93
+ fetchResult();
94
+ }, [fetchResult]);
95
+ const refine = (0, react_1.useCallback)((text, source) => {
96
+ const refinementText = text.trim();
97
+ if (!refinementText)
98
+ return;
99
+ fetchResult(refinementText, source);
100
+ }, [fetchResult]);
101
+ // The title belonging to the last response we kept. `defaultTitle` is the caller's last resort
102
+ // for a response that carried items but no title of its own.
103
+ const settledTitle = (result === null || result === void 0 ? void 0 : result.title) || defaultTitle || '';
104
+ let title = settledTitle;
105
+ if (isLoading) {
106
+ // The loading copy wins whether or not a title is held, so a refinement announces itself
107
+ // rather than leaving the previous personalization above products that no longer match it.
108
+ title = (0, translate_1.translate)(constants_1.RECS_LOADING_TITLE, translations);
109
+ }
110
+ else if (!hasUnsupportedInput && (error || (result === null || result === void 0 ? void 0 : result.status) === 'partial')) {
111
+ // Either the request failed or it came back degraded, so the personalized title cannot be
112
+ // trusted. Any products it did carry are still worth showing under a generic title.
113
+ title = (0, translate_1.translate)(constants_1.RECS_FALLBACK_TITLE, translations);
114
+ }
115
+ return {
116
+ title,
117
+ // Normalized so every caller can treat "nothing to render" as a single case. Our own adapter
118
+ // never returns an empty array, but `cioClient` is a public prop and a consumer-supplied client
119
+ // can. Note `[]` is truthy, so `|| null` would not catch it.
120
+ items: ((_a = result === null || result === void 0 ? void 0 : result.items) === null || _a === void 0 ? void 0 : _a.length) ? result.items : null,
121
+ refinement: (result === null || result === void 0 ? void 0 : result.refinement) || null,
122
+ isLoading,
123
+ error,
124
+ inputError: hasUnsupportedInput ? (0, translate_1.translate)(constants_1.RECS_UNSUPPORTED_REQUEST, translations) : null,
125
+ lastShopperInput,
126
+ refine,
127
+ };
128
+ }
129
+ exports.default = useRecsPod;
@@ -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;
@@ -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.11.0';
@@ -1,98 +1,10 @@
1
- import React, { useCallback } from 'react';
2
- import { RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components';
3
- import Input from '../Input/Input';
4
- import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/SuggestedQuestionsContainer';
5
- import useCioPia from '../../hooks/useCioPia';
6
- import useConversation from '../../hooks/useConversation';
7
- import useTracking from '../../hooks/useTracking';
8
- import useViewportTracking from '../../hooks/useViewportTracking';
9
- import useViewportCallbacks from '../../hooks/useViewportCallbacks';
10
- import ErrorBlock from '../Error/ErrorBlock';
11
- import LoadingSkeleton from '../LoadingSkeleton/LoadingSkeleton';
12
- import { translate } from '../../utils/translate';
13
- import PiaInlineAnswer from '../PiaInlineAnswer/PiaInlineAnswer';
14
- import PiaModal from '../PiaConversation/PiaModal';
15
- import PiaConversation from '../PiaConversation/PiaConversation';
16
- // eslint-disable-next-line complexity
1
+ import React from 'react';
2
+ import CioPiaQna from './CioPiaQna';
3
+ import CioPiaRecs from '../CioPiaRecs/CioPiaRecs';
17
4
  export default function CioPia(props) {
18
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, parameters, } = props;
19
- const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
20
- const isConversation = mode === 'conversation' || type === 'modal';
21
- const { priceCurrency } = productCardProps || {};
22
- const pia = useCioPia({
23
- apiKey,
24
- itemId,
25
- threadId,
26
- variationId,
27
- cioClient,
28
- suggestedQuestionsParameters,
29
- parameters,
30
- formatImageUrl: formatters?.formatImageUrl,
31
- });
32
- const tracking = useTracking({
33
- cioClient: pia.cioClient,
34
- itemId,
35
- itemName,
36
- variationId,
37
- threadId: pia.threadId,
38
- });
39
- const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, handleFeedback, resetState, } = useConversation({ pia, itemId, isConversation, callbacks, tracking });
40
- const { containerRef: viewportContainerRef } = useViewportTracking({
41
- tracking,
42
- questions: displayedQuestions,
43
- });
44
- const { containerRef: callbackContainerRef } = useViewportCallbacks({ callbacks, context });
45
- const containerRef = useCallback((node) => {
46
- viewportContainerRef(node);
47
- callbackContainerRef(node);
48
- }, [viewportContainerRef, callbackContainerRef]);
49
- const qnaResultId = pia.answers.data?.qna_result_id;
50
- const renderProps = {
51
- items: currentItems,
52
- isLoading,
53
- error,
54
- currentAnswer,
55
- currentQuestion,
56
- displayedQuestions,
57
- handleSubmitQuestion,
58
- conversationHistory,
59
- };
60
- const conversationHistoryProps = {
61
- conversationHistory,
62
- isLoading,
63
- error,
64
- currentItems,
65
- showFeedback,
66
- showPreviousItems,
67
- learnMoreUrl,
68
- disclaimerPosition,
69
- translations,
70
- callbacks,
71
- componentOverrides,
72
- displayedQuestions,
73
- handleSubmitQuestion,
74
- handleQuestionClick,
75
- handleFeedback,
76
- onResultClick: tracking.trackResultClick,
77
- qnaResultId,
78
- containerRef,
79
- onInputFocus: handleInputFocus,
80
- priceCurrency,
81
- };
82
- if (type === 'modal') {
83
- return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
84
- React.createElement(PiaConversation, { ...conversationHistoryProps })));
5
+ const mode = props.displayConfigs?.mode ?? 'default';
6
+ if (mode === 'recommendations') {
7
+ return React.createElement(CioPiaRecs, { ...props });
85
8
  }
86
- if (isConversation)
87
- return React.createElement(PiaConversation, { ...conversationHistoryProps });
88
- // Default inline mode
89
- return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
90
- React.createElement(RenderPropsWrapper, { props: renderProps, override: children || componentOverrides?.reactNode },
91
- React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
92
- React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides?.input }),
93
- isLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
94
- !isLoading && error && React.createElement(ErrorBlock, { message: error?.message || 'Unexpected error' }),
95
- !isLoading && !error && (React.createElement(React.Fragment, null,
96
- currentAnswer && (React.createElement(PiaInlineAnswer, { 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 })),
97
- React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions }))))));
9
+ return React.createElement(CioPiaQna, { ...props });
98
10
  }
@@ -0,0 +1,105 @@
1
+ import React, { useCallback } from 'react';
2
+ import { RenderPropsWrapper } from '@constructor-io/constructorio-ui-components';
3
+ import Input from '../Input/Input';
4
+ import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/SuggestedQuestionsContainer';
5
+ import SuggestedQuestionsSkeleton from '../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton';
6
+ import useCioPia from '../../hooks/useCioPia';
7
+ import useConversation from '../../hooks/useConversation';
8
+ import useTracking from '../../hooks/useTracking';
9
+ import useViewportTracking from '../../hooks/useViewportTracking';
10
+ import useViewportCallbacks from '../../hooks/useViewportCallbacks';
11
+ import ErrorBlock from '../Error/ErrorBlock';
12
+ import LoadingSkeleton from '../LoadingSkeleton/LoadingSkeleton';
13
+ import { translate } from '../../utils/translate';
14
+ import PiaInlineAnswer from '../PiaInlineAnswer/PiaInlineAnswer';
15
+ import PiaModal from '../PiaConversation/PiaModal';
16
+ import PiaConversation from '../PiaConversation/PiaConversation';
17
+ /** The question-and-answer experience: `mode: 'default'`, `mode: 'conversation'`, and the modal. */
18
+ // eslint-disable-next-line complexity
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;
21
+ const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
22
+ const isConversation = mode === 'conversation' || type === 'modal';
23
+ const { priceCurrency } = productCardProps || {};
24
+ const pia = useCioPia({
25
+ apiKey,
26
+ itemId,
27
+ threadId,
28
+ variationId,
29
+ cioClient,
30
+ suggestedQuestionsParameters,
31
+ parameters,
32
+ formatImageUrl: formatters?.formatImageUrl,
33
+ });
34
+ const tracking = useTracking({
35
+ cioClient: pia.cioClient,
36
+ itemId,
37
+ itemName,
38
+ variationId,
39
+ threadId: pia.threadId,
40
+ });
41
+ const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, handleFeedback, resetState, } = useConversation({ pia, itemId, isConversation, callbacks, tracking });
42
+ const { containerRef: viewportContainerRef } = useViewportTracking({
43
+ tracking,
44
+ questions: displayedQuestions,
45
+ viewThreshold: trackingConfigs?.viewThreshold,
46
+ });
47
+ const { containerRef: callbackContainerRef } = useViewportCallbacks({ callbacks, context });
48
+ const containerRef = useCallback((node) => {
49
+ viewportContainerRef(node);
50
+ callbackContainerRef(node);
51
+ }, [viewportContainerRef, callbackContainerRef]);
52
+ const qnaResultId = pia.answers.data?.qna_result_id;
53
+ // The text bars stand in for the answer alone, so they track the answer request only. The question
54
+ // row keeps the combined `isLoading`: the follow-up questions that refill it arrive with the
55
+ // answer, so the row is waiting on either request.
56
+ const isAnswerLoading = pia.answers.isLoading;
57
+ const renderProps = {
58
+ items: currentItems,
59
+ isLoading,
60
+ error,
61
+ currentAnswer,
62
+ currentQuestion,
63
+ displayedQuestions,
64
+ handleSubmitQuestion,
65
+ conversationHistory,
66
+ };
67
+ const conversationHistoryProps = {
68
+ conversationHistory,
69
+ isLoading,
70
+ error,
71
+ currentItems,
72
+ showFeedback,
73
+ showPreviousItems,
74
+ learnMoreUrl,
75
+ disclaimerPosition,
76
+ translations,
77
+ callbacks,
78
+ componentOverrides,
79
+ displayedQuestions,
80
+ handleSubmitQuestion,
81
+ handleQuestionClick,
82
+ handleFeedback,
83
+ onResultClick: tracking.trackResultClick,
84
+ qnaResultId,
85
+ containerRef,
86
+ onInputFocus: handleInputFocus,
87
+ priceCurrency,
88
+ };
89
+ if (type === 'modal') {
90
+ return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
91
+ React.createElement(PiaConversation, { ...conversationHistoryProps })));
92
+ }
93
+ if (isConversation)
94
+ return React.createElement(PiaConversation, { ...conversationHistoryProps });
95
+ // Default inline mode
96
+ return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
97
+ React.createElement(RenderPropsWrapper, { props: renderProps, override: children || componentOverrides?.reactNode },
98
+ React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
99
+ React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides?.input }),
100
+ isAnswerLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
101
+ !isAnswerLoading && error && React.createElement(ErrorBlock, { message: error?.message || 'Unexpected error' }),
102
+ !isAnswerLoading && !error && currentAnswer && (React.createElement(PiaInlineAnswer, { 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 })),
103
+ isLoading && React.createElement(SuggestedQuestionsSkeleton, null),
104
+ !isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })))));
105
+ }
@@ -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
+ }
@@ -0,0 +1 @@
1
+ export {};