@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.
- package/README.md +7 -0
- package/dist/constructorio-ui-pia-bundled.js +50 -50
- package/lib/cjs/components/CioPia/CioPia.js +8 -96
- package/lib/cjs/components/CioPia/CioPiaQna.js +110 -0
- package/lib/cjs/components/CioPia/CioPiaStyles.css +19 -0
- package/lib/cjs/components/CioPia/types.js +2 -0
- package/lib/cjs/components/CioPiaRecs/CioPiaRecs.js +73 -0
- package/lib/cjs/components/CioPiaRecs/CioPiaRecsStyles.css +138 -0
- package/lib/cjs/components/CioPiaRecs/RecsCarouselSkeleton.js +58 -0
- package/lib/cjs/components/CioPiaRecs/RecsOptionsSkeleton.js +18 -0
- package/lib/cjs/components/CioPiaRecs/RecsPodRefinement.js +27 -0
- package/lib/cjs/components/Input/Input.js +27 -9
- package/lib/cjs/components/Input/InputStyles.css +27 -0
- package/lib/cjs/components/SuggestedQuestion/SuggestedQuestion.js +2 -3
- package/lib/cjs/components/SuggestedQuestionsContainer/SuggestedQuestionsContainerStyles.css +1 -1
- package/lib/cjs/constants.js +12 -1
- package/lib/cjs/hooks/mocks/agent.js +17 -0
- package/lib/cjs/hooks/useCioPia.js +6 -14
- package/lib/cjs/hooks/usePiaClient.js +26 -0
- package/lib/cjs/hooks/useRecsPod.js +129 -0
- package/lib/cjs/hooks/useViewportTracking.js +3 -2
- package/lib/cjs/styles.css +1 -0
- package/lib/cjs/utils/classNames.js +13 -0
- package/lib/cjs/utils/recs.js +13 -0
- package/lib/cjs/utils/translate.js +5 -0
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/CioPia/CioPia.js +7 -95
- package/lib/mjs/components/CioPia/CioPiaQna.js +105 -0
- package/lib/mjs/components/CioPia/CioPiaStyles.css +19 -0
- package/lib/mjs/components/CioPia/types.js +1 -0
- package/lib/mjs/components/CioPiaRecs/CioPiaRecs.js +69 -0
- package/lib/mjs/components/CioPiaRecs/CioPiaRecsStyles.css +138 -0
- package/lib/mjs/components/CioPiaRecs/RecsCarouselSkeleton.js +54 -0
- package/lib/mjs/components/CioPiaRecs/RecsOptionsSkeleton.js +14 -0
- package/lib/mjs/components/CioPiaRecs/RecsPodRefinement.js +23 -0
- package/lib/mjs/components/Input/Input.js +27 -9
- package/lib/mjs/components/Input/InputStyles.css +27 -0
- package/lib/mjs/components/SuggestedQuestion/SuggestedQuestion.js +2 -3
- package/lib/mjs/components/SuggestedQuestionsContainer/SuggestedQuestionsContainerStyles.css +1 -1
- package/lib/mjs/constants.js +11 -0
- package/lib/mjs/hooks/mocks/agent.js +15 -0
- package/lib/mjs/hooks/useCioPia.js +6 -14
- package/lib/mjs/hooks/usePiaClient.js +22 -0
- package/lib/mjs/hooks/useRecsPod.js +125 -0
- package/lib/mjs/hooks/useViewportTracking.js +3 -2
- package/lib/mjs/styles.css +1 -0
- package/lib/mjs/utils/classNames.js +9 -0
- package/lib/mjs/utils/recs.js +9 -0
- package/lib/mjs/utils/translate.js +6 -1
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/CioPia/CioPia.d.ts +1 -36
- package/lib/types/components/CioPia/CioPiaQna.d.ts +4 -0
- package/lib/types/components/CioPia/index.d.ts +1 -0
- package/lib/types/components/CioPia/types.d.ts +40 -0
- package/lib/types/components/CioPiaRecs/CioPiaRecs.d.ts +13 -0
- package/lib/types/components/CioPiaRecs/RecsCarouselSkeleton.d.ts +16 -0
- package/lib/types/components/CioPiaRecs/RecsOptionsSkeleton.d.ts +16 -0
- package/lib/types/components/CioPiaRecs/RecsPodRefinement.d.ts +21 -0
- package/lib/types/components/Input/Input.d.ts +9 -1
- package/lib/types/components/SuggestedQuestion/SuggestedQuestion.d.ts +4 -2
- package/lib/types/constants.d.ts +11 -0
- package/lib/types/hooks/mocks/agent.d.ts +13 -0
- package/lib/types/hooks/usePiaClient.d.ts +17 -0
- package/lib/types/hooks/useRecsPod.d.ts +52 -0
- package/lib/types/hooks/useViewportTracking.d.ts +2 -1
- package/lib/types/types.d.ts +76 -1
- package/lib/types/utils/classNames.d.ts +9 -0
- package/lib/types/utils/recs.d.ts +6 -0
- package/lib/types/version.d.ts +1 -1
- 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;
|
package/lib/cjs/constants.js
CHANGED
|
@@ -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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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:
|
|
54
|
+
}, { threshold: viewThreshold });
|
|
54
55
|
observer.observe(element);
|
|
55
56
|
observerRef.current = observer;
|
|
56
57
|
intervalRef.current = setInterval(flush, FLUSH_INTERVAL_MS);
|
package/lib/cjs/styles.css
CHANGED
|
@@ -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.
|
package/lib/cjs/version.js
CHANGED
|
@@ -1,98 +1,10 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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 {};
|