@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.
- package/README.md +7 -0
- package/dist/constructorio-ui-pia-bundled.js +55 -55
- package/lib/cjs/components/CioPia/CioPia.js +8 -96
- package/lib/cjs/components/CioPia/CioPiaQna.js +111 -0
- package/lib/cjs/components/CioPia/CioPiaStyles.css +19 -0
- package/lib/cjs/components/CioPia/PiaCustomCarousel.js +24 -29
- 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/useAnswerResults.js +4 -14
- package/lib/cjs/hooks/useCioPia.js +48 -18
- package/lib/cjs/hooks/usePiaClient.js +21 -0
- package/lib/cjs/hooks/useRecsPod.js +134 -0
- package/lib/cjs/hooks/useSuggestedQuestions.js +10 -26
- package/lib/cjs/hooks/useViewportTracking.js +3 -2
- package/lib/cjs/styles.css +1 -0
- package/lib/cjs/types.js +0 -2
- 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 +106 -0
- package/lib/mjs/components/CioPia/CioPiaStyles.css +19 -0
- package/lib/mjs/components/CioPia/PiaCustomCarousel.js +43 -30
- 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/useAnswerResults.js +4 -13
- package/lib/mjs/hooks/useCioPia.js +49 -18
- package/lib/mjs/hooks/usePiaClient.js +17 -0
- package/lib/mjs/hooks/useRecsPod.js +129 -0
- package/lib/mjs/hooks/useSuggestedQuestions.js +12 -24
- package/lib/mjs/hooks/useViewportTracking.js +3 -2
- package/lib/mjs/styles.css +1 -0
- package/lib/mjs/types.js +0 -1
- 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 +55 -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/useAnswerResults.d.ts +4 -4
- package/lib/types/hooks/useCioPia.d.ts +20 -5
- package/lib/types/hooks/usePiaClient.d.ts +12 -0
- package/lib/types/hooks/useRecsPod.d.ts +52 -0
- package/lib/types/hooks/useSuggestedQuestions.d.ts +3 -4
- package/lib/types/hooks/useViewportTracking.d.ts +2 -1
- package/lib/types/types.d.ts +119 -7
- 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 +2 -2
- package/lib/cjs/hooks/mocks/MockConstructorIOClient.js +0 -24
- package/lib/cjs/hooks/mocks/agent.js +0 -166
- package/lib/mjs/hooks/mocks/MockConstructorIOClient.js +0 -38
- package/lib/mjs/hooks/mocks/agent.js +0 -158
- package/lib/types/hooks/mocks/MockConstructorIOClient.d.ts +0 -14
- package/lib/types/hooks/mocks/agent.d.ts +0 -10
- package/lib/types/hooks/mocks/types.d.ts +0 -77
- /package/lib/cjs/{hooks/mocks → components/CioPia}/types.js +0 -0
- /package/lib/mjs/{hooks/mocks → components/CioPia}/types.js +0 -0
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components';
|
|
3
|
+
import { cx } from '../../utils/classNames';
|
|
3
4
|
import { translate } from '../../utils/translate';
|
|
4
5
|
import { SendIcon } from '../icons';
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Numbers the `Input`s mounted on the page, so each one can give its error message a DOM `id` no
|
|
8
|
+
* other input will collide with. Nothing to do with messages or errors themselves - the id is
|
|
9
|
+
* needed because that is what the field's `aria-describedby` has to point at.
|
|
10
|
+
*
|
|
11
|
+
* `useId` is the built-in way to do this, but it is React 18+ and this package supports React
|
|
12
|
+
* >= 16.12. Read once per instance through a lazy initializer, so the number an input gets holds
|
|
13
|
+
* across its re-renders.
|
|
14
|
+
*/
|
|
15
|
+
let inputIdCounter = 0;
|
|
16
|
+
function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, componentOverride, error, placeholderKey = 'Ask anything', showSendButton = true, }) {
|
|
6
17
|
const [value, setValue] = useState(providedValue || '');
|
|
18
|
+
const [errorId] = useState(() => {
|
|
19
|
+
inputIdCounter += 1;
|
|
20
|
+
return `cio-pia-input-${inputIdCounter}-error`;
|
|
21
|
+
});
|
|
7
22
|
useEffect(() => {
|
|
8
23
|
if (providedValue) {
|
|
9
24
|
setValue(providedValue);
|
|
@@ -24,20 +39,23 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
|
|
|
24
39
|
}
|
|
25
40
|
};
|
|
26
41
|
// Priority: translations > placeholder prop > default
|
|
27
|
-
const resolvedPlaceholder = translations?.[
|
|
28
|
-
? translate(
|
|
29
|
-
: (placeholder ?? translate(
|
|
42
|
+
const resolvedPlaceholder = translations?.[placeholderKey] !== undefined
|
|
43
|
+
? translate(placeholderKey, translations)
|
|
44
|
+
: (placeholder ?? translate(placeholderKey));
|
|
30
45
|
return (React.createElement(RenderPropsWrapper, { props: {
|
|
31
46
|
disabled,
|
|
32
47
|
placeholder: resolvedPlaceholder,
|
|
33
48
|
onSubmit: handleSubmit,
|
|
34
49
|
onFocus,
|
|
35
50
|
translations,
|
|
51
|
+
error,
|
|
36
52
|
}, override: componentOverride?.reactNode },
|
|
37
|
-
React.createElement(
|
|
38
|
-
React.createElement("
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
React.createElement(React.Fragment, null,
|
|
54
|
+
React.createElement("div", { className: 'cio-pia-input-container' },
|
|
55
|
+
React.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, disabled: disabled, className: 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 }),
|
|
56
|
+
showSendButton && (React.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
|
|
57
|
+
translate('Send', translations),
|
|
58
|
+
React.createElement(SendIcon, null)))),
|
|
59
|
+
error && (React.createElement("span", { className: 'cio-pia-input-error', id: errorId, role: 'alert' }, error)))));
|
|
42
60
|
}
|
|
43
61
|
export 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;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { QuestionIcon } from '../icons';
|
|
3
|
-
function SuggestedQuestion({ question, onClick }) {
|
|
3
|
+
function SuggestedQuestion({ question, onClick, icon }) {
|
|
4
4
|
return (React.createElement("button", { type: 'button', className: 'cio-pia-suggested-question', onClick: onClick },
|
|
5
|
-
React.createElement("div", { className: 'cio-pia-suggested-question-icon' },
|
|
6
|
-
React.createElement(QuestionIcon, null)),
|
|
5
|
+
React.createElement("div", { className: 'cio-pia-suggested-question-icon' }, icon ?? React.createElement(QuestionIcon, null)),
|
|
7
6
|
question));
|
|
8
7
|
}
|
|
9
8
|
export default SuggestedQuestion;
|
package/lib/mjs/constants.js
CHANGED
|
@@ -12,3 +12,14 @@ export const MOCK_QUESTIONS = [
|
|
|
12
12
|
{ value: 'What is the serving size and calorie count?' },
|
|
13
13
|
];
|
|
14
14
|
export const DISCLAIMER_TEXT = 'AI-generated answers aim to help, but they may occasionally miss details or be inaccurate. Double-check important information before purchasing.';
|
|
15
|
+
/**
|
|
16
|
+
* Recommendations pod strings the API cannot supply, because in these states there is either
|
|
17
|
+
* no response yet or no usable response at all. Everything else the pod shows comes from the
|
|
18
|
+
* API. All five are `Translations` keys, so reference the constants instead of retyping the
|
|
19
|
+
* literals - a mistyped key falls back to itself with no warning.
|
|
20
|
+
*/
|
|
21
|
+
export const RECS_LOADING_TITLE = 'Adapting recommendations to your preference';
|
|
22
|
+
export const RECS_FALLBACK_TITLE = 'Best selling products';
|
|
23
|
+
export const RECS_UNSUPPORTED_REQUEST = 'Unsupported request, try a different feature.';
|
|
24
|
+
export const RECS_REFINEMENT_LABEL = "Not what you're looking for? Try:";
|
|
25
|
+
export const RECS_INPUT_PLACEHOLDER = 'Describe something else...';
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'react';
|
|
2
2
|
import { extractAndTransformItems } from '../utils/transformers';
|
|
3
|
-
const fetchAnswerResults = async ({ client, itemId, question, variationId, threadId, parameters, }) => {
|
|
4
|
-
const response = await client.agent.getAnswerResults({
|
|
5
|
-
itemId,
|
|
6
|
-
variationId,
|
|
7
|
-
threadId,
|
|
8
|
-
question,
|
|
9
|
-
parameters,
|
|
10
|
-
});
|
|
11
|
-
return response;
|
|
12
|
-
};
|
|
13
3
|
export default function useAnswerResults({ itemId, variationId, threadId, cioClient, parameters, formatImageUrl, }) {
|
|
14
4
|
const [answerResults, setAnswerResults] = useState(null);
|
|
15
5
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -21,9 +11,10 @@ export default function useAnswerResults({ itemId, variationId, threadId, cioCli
|
|
|
21
11
|
setIsLoading(true);
|
|
22
12
|
setError(null);
|
|
23
13
|
setAnswerResults(null);
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
14
|
+
cioClient.agent.pia
|
|
15
|
+
.getAnswerResults(itemId, question, { threadId, variationId, ...parameters })
|
|
16
|
+
.then((response) => {
|
|
17
|
+
setAnswerResults(response);
|
|
27
18
|
setError(null);
|
|
28
19
|
})
|
|
29
20
|
.catch((err) => {
|
|
@@ -1,35 +1,66 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import MockConstructorIOClient from './mocks/MockConstructorIOClient';
|
|
3
1
|
import useAnswerResults from './useAnswerResults';
|
|
2
|
+
import usePiaClient from './usePiaClient';
|
|
4
3
|
import useSuggestedQuestions from './useSuggestedQuestions';
|
|
5
|
-
|
|
4
|
+
// TODO: remove SUPPORTED_DEPRECATED_PARAMS and mapDeprecatedParameters once the
|
|
5
|
+
// deprecated `parameters` prop is removed from CioPiaProps.
|
|
6
|
+
const SUPPORTED_DEPRECATED_PARAMS = {
|
|
7
|
+
guard: { camelKey: 'guard', isObject: false },
|
|
8
|
+
num_results: { camelKey: 'numResults', isObject: false },
|
|
9
|
+
numResults: { camelKey: 'numResults', isObject: false },
|
|
10
|
+
pre_filter_expression: { camelKey: 'preFilterExpression', isObject: true },
|
|
11
|
+
preFilterExpression: { camelKey: 'preFilterExpression', isObject: true },
|
|
12
|
+
fmt_options: { camelKey: 'fmtOptions', isObject: true },
|
|
13
|
+
fmtOptions: { camelKey: 'fmtOptions', isObject: true },
|
|
14
|
+
};
|
|
15
|
+
function mapDeprecatedParameters(params) {
|
|
16
|
+
if (!params)
|
|
17
|
+
return undefined;
|
|
18
|
+
const mapped = {};
|
|
19
|
+
let hasEntries = false;
|
|
20
|
+
for (const [key, value] of Object.entries(params)) {
|
|
21
|
+
const config = SUPPORTED_DEPRECATED_PARAMS[key];
|
|
22
|
+
if (!config)
|
|
23
|
+
continue;
|
|
24
|
+
hasEntries = true;
|
|
25
|
+
if (config.isObject && typeof value === 'string') {
|
|
26
|
+
try {
|
|
27
|
+
mapped[config.camelKey] = JSON.parse(value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
mapped[config.camelKey] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
mapped[config.camelKey] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return hasEntries ? mapped : undefined;
|
|
38
|
+
}
|
|
6
39
|
export default function useCioPia(props) {
|
|
7
|
-
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, parameters, formatImageUrl, } = props;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}, [apiKey, providedClient]);
|
|
40
|
+
const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, answerParameters, parameters, formatImageUrl, } = props;
|
|
41
|
+
const { cioClient: client, threadId } = usePiaClient({
|
|
42
|
+
apiKey,
|
|
43
|
+
threadId: providedThreadId,
|
|
44
|
+
cioClient: providedClient,
|
|
45
|
+
});
|
|
46
|
+
const mappedDeprecated = mapDeprecatedParameters(parameters);
|
|
47
|
+
const mergedSuggestedParams = mappedDeprecated || suggestedQuestionsParameters
|
|
48
|
+
? { ...mappedDeprecated, ...suggestedQuestionsParameters }
|
|
49
|
+
: undefined;
|
|
50
|
+
const mergedAnswerParams = mappedDeprecated || answerParameters ? { ...mappedDeprecated, ...answerParameters } : undefined;
|
|
19
51
|
const suggestedQuestions = useSuggestedQuestions({
|
|
20
52
|
itemId,
|
|
21
53
|
variationId,
|
|
22
54
|
threadId,
|
|
23
55
|
cioClient: client,
|
|
24
|
-
parameters:
|
|
25
|
-
requestParameters: parameters,
|
|
56
|
+
parameters: mergedSuggestedParams,
|
|
26
57
|
});
|
|
27
58
|
const answers = useAnswerResults({
|
|
28
59
|
itemId,
|
|
29
60
|
variationId,
|
|
30
61
|
threadId,
|
|
31
62
|
cioClient: client,
|
|
32
|
-
parameters,
|
|
63
|
+
parameters: mergedAnswerParams,
|
|
33
64
|
formatImageUrl,
|
|
34
65
|
});
|
|
35
66
|
return {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
|
|
3
|
+
import version from '../version';
|
|
4
|
+
export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
|
|
5
|
+
const [generatedThreadId] = useState(() => crypto.randomUUID());
|
|
6
|
+
const threadId = providedThreadId || generatedThreadId;
|
|
7
|
+
const client = useMemo(() => {
|
|
8
|
+
if (providedClient)
|
|
9
|
+
return providedClient;
|
|
10
|
+
return new ConstructorIOClient({
|
|
11
|
+
apiKey,
|
|
12
|
+
sendTrackingEvents: true,
|
|
13
|
+
version: `cio-ui-pia-${version}`,
|
|
14
|
+
});
|
|
15
|
+
}, [apiKey, providedClient]);
|
|
16
|
+
return { cioClient: client, threadId };
|
|
17
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { AgentRequestError } from '../errors';
|
|
3
|
+
import { translate } from '../utils/translate';
|
|
4
|
+
import { RECS_FALLBACK_TITLE, RECS_LOADING_TITLE, RECS_UNSUPPORTED_REQUEST } from '../constants';
|
|
5
|
+
const DEFAULT_STRATEGY = 'complementary_items';
|
|
6
|
+
/**
|
|
7
|
+
* Owns everything a recommendations pod shows: one request on mount, one on every refinement,
|
|
8
|
+
* and the title that belongs to whichever of those is happening right now.
|
|
9
|
+
*
|
|
10
|
+
* The previous items are deliberately kept while a new request is in flight, so a refinement
|
|
11
|
+
* swaps the products without collapsing the row they sit in. The title is the one thing that does
|
|
12
|
+
* change, so the shopper can see the pod reacting to what they asked for.
|
|
13
|
+
*/
|
|
14
|
+
export default function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, formatImageUrl, translations, }) {
|
|
15
|
+
const [result, setResult] = useState(null);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const [hasUnsupportedInput, setHasUnsupportedInput] = useState(false);
|
|
19
|
+
const [lastShopperInput, setLastShopperInput] = useState('');
|
|
20
|
+
// Counts requests so a slow response cannot overwrite a newer one. There is no
|
|
21
|
+
// AbortController in this library, so the older request still completes; its result is
|
|
22
|
+
// simply dropped.
|
|
23
|
+
const requestIdRef = useRef(0);
|
|
24
|
+
// Mirrored into a ref so it never becomes a fetch dependency. It is commonly written inline by
|
|
25
|
+
// the caller, which would give it a new identity on every render and refetch forever.
|
|
26
|
+
const formatImageUrlRef = useRef(formatImageUrl);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
formatImageUrlRef.current = formatImageUrl;
|
|
29
|
+
}, [formatImageUrl]);
|
|
30
|
+
const strategy = parameters?.strategy || DEFAULT_STRATEGY;
|
|
31
|
+
const { numResults, defaultTitle } = parameters || {};
|
|
32
|
+
const fetchResult = useCallback((shopperInput, source) => {
|
|
33
|
+
// TODO: once getRecs is added to the JS client SDK, replace this guard+cast
|
|
34
|
+
// with a direct call: cioClient.agent.getRecs({...})
|
|
35
|
+
const getRecs = cioClient?.agent?.getRecs;
|
|
36
|
+
if (!cioClient || !getRecs) {
|
|
37
|
+
if (cioClient && !getRecs) {
|
|
38
|
+
console.info('[CioPia] getRecs is not available on the client SDK yet.');
|
|
39
|
+
}
|
|
40
|
+
setIsLoading(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
requestIdRef.current += 1;
|
|
44
|
+
const requestId = requestIdRef.current;
|
|
45
|
+
const isCurrent = () => requestId === requestIdRef.current;
|
|
46
|
+
setIsLoading(true);
|
|
47
|
+
setError(null);
|
|
48
|
+
setHasUnsupportedInput(false);
|
|
49
|
+
getRecs
|
|
50
|
+
.call(cioClient.agent, {
|
|
51
|
+
itemId,
|
|
52
|
+
variationId,
|
|
53
|
+
threadId,
|
|
54
|
+
strategy,
|
|
55
|
+
shopperInput,
|
|
56
|
+
numResults,
|
|
57
|
+
formatImageUrl: formatImageUrlRef.current,
|
|
58
|
+
})
|
|
59
|
+
.then((fetchedResult) => {
|
|
60
|
+
if (!isCurrent())
|
|
61
|
+
return;
|
|
62
|
+
setResult(fetchedResult);
|
|
63
|
+
setLastShopperInput(shopperInput || '');
|
|
64
|
+
})
|
|
65
|
+
.catch((err) => {
|
|
66
|
+
if (!isCurrent())
|
|
67
|
+
return;
|
|
68
|
+
const failure = err instanceof Error ? err : new Error('Error fetching recommendations');
|
|
69
|
+
const status = failure instanceof AgentRequestError ? failure.status : undefined;
|
|
70
|
+
if (status === 400) {
|
|
71
|
+
// A 400 means the request we built was malformed, which is our bug, not the
|
|
72
|
+
// shopper's. Make it loud so it is caught in development.
|
|
73
|
+
console.error('Constructor PIA: the recommendations request was rejected.', failure);
|
|
74
|
+
}
|
|
75
|
+
// A rejected input changes nothing but the input, so the response on screen stays.
|
|
76
|
+
// Only text the shopper wrote can be rejected this way: an option is a label the API
|
|
77
|
+
// suggested, so blaming their input for it would point at the wrong thing entirely.
|
|
78
|
+
if (status === 422 && source === 'input') {
|
|
79
|
+
setHasUnsupportedInput(true);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setError(failure);
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
if (!isCurrent())
|
|
86
|
+
return;
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
// Every dependency is a primitive. `formatImageUrl` is read through a ref instead, so a
|
|
91
|
+
// caller writing it inline cannot restart the request on every render.
|
|
92
|
+
[cioClient, itemId, variationId, threadId, strategy, numResults]);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
fetchResult();
|
|
95
|
+
}, [fetchResult]);
|
|
96
|
+
const refine = useCallback((text, source) => {
|
|
97
|
+
const refinementText = text.trim();
|
|
98
|
+
if (!refinementText)
|
|
99
|
+
return;
|
|
100
|
+
fetchResult(refinementText, source);
|
|
101
|
+
}, [fetchResult]);
|
|
102
|
+
// The title belonging to the last response we kept. `defaultTitle` is the caller's last resort
|
|
103
|
+
// for a response that carried items but no title of its own.
|
|
104
|
+
const settledTitle = result?.title || defaultTitle || '';
|
|
105
|
+
let title = settledTitle;
|
|
106
|
+
if (isLoading) {
|
|
107
|
+
// The loading copy wins whether or not a title is held, so a refinement announces itself
|
|
108
|
+
// rather than leaving the previous personalization above products that no longer match it.
|
|
109
|
+
title = translate(RECS_LOADING_TITLE, translations);
|
|
110
|
+
}
|
|
111
|
+
else if (!hasUnsupportedInput && (error || result?.status === 'partial')) {
|
|
112
|
+
// Either the request failed or it came back degraded, so the personalized title cannot be
|
|
113
|
+
// trusted. Any products it did carry are still worth showing under a generic title.
|
|
114
|
+
title = translate(RECS_FALLBACK_TITLE, translations);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
title,
|
|
118
|
+
// Normalized so every caller can treat "nothing to render" as a single case. Our own adapter
|
|
119
|
+
// never returns an empty array, but `cioClient` is a public prop and a consumer-supplied client
|
|
120
|
+
// can. Note `[]` is truthy, so `|| null` would not catch it.
|
|
121
|
+
items: result?.items?.length ? result.items : null,
|
|
122
|
+
refinement: result?.refinement || null,
|
|
123
|
+
isLoading,
|
|
124
|
+
error,
|
|
125
|
+
inputError: hasUnsupportedInput ? translate(RECS_UNSUPPORTED_REQUEST, translations) : null,
|
|
126
|
+
lastShopperInput,
|
|
127
|
+
refine,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -1,35 +1,22 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
|
|
3
|
-
const response = await client.agent.getSuggestedQuestions({
|
|
4
|
-
itemId,
|
|
5
|
-
variationId,
|
|
6
|
-
threadId,
|
|
7
|
-
parameters,
|
|
8
|
-
requestParameters,
|
|
9
|
-
});
|
|
10
|
-
return response.questions;
|
|
11
|
-
};
|
|
12
|
-
export default function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, requestParameters, }) {
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
export default function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, }) {
|
|
13
3
|
const [questions, setQuestions] = useState([]);
|
|
14
4
|
const [isLoading, setIsLoading] = useState(false);
|
|
15
5
|
const [error, setError] = useState(null);
|
|
16
|
-
|
|
17
|
-
const requestParametersKey = JSON.stringify(requestParameters);
|
|
6
|
+
const serializedParameters = useMemo(() => JSON.stringify(parameters), [parameters]);
|
|
18
7
|
const fetchResult = useCallback(() => {
|
|
19
8
|
if (!cioClient)
|
|
20
9
|
return;
|
|
21
10
|
setIsLoading(true);
|
|
22
11
|
setError(null);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
itemId,
|
|
26
|
-
variationId,
|
|
12
|
+
cioClient.agent.pia
|
|
13
|
+
.getSuggestedQuestions(itemId, {
|
|
27
14
|
threadId,
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
variationId,
|
|
16
|
+
...parameters,
|
|
30
17
|
})
|
|
31
|
-
.then((
|
|
32
|
-
setQuestions(
|
|
18
|
+
.then((response) => {
|
|
19
|
+
setQuestions(response.questions);
|
|
33
20
|
setError(null);
|
|
34
21
|
})
|
|
35
22
|
.catch((err) => {
|
|
@@ -38,8 +25,9 @@ export default function useSuggestedQuestions({ itemId, variationId, threadId, c
|
|
|
38
25
|
.finally(() => {
|
|
39
26
|
setIsLoading(false);
|
|
40
27
|
});
|
|
41
|
-
//
|
|
42
|
-
|
|
28
|
+
// parameters is serialized via serializedParameters to prevent refetch on identity changes
|
|
29
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
30
|
+
}, [cioClient, itemId, variationId, threadId, serializedParameters]);
|
|
43
31
|
useEffect(() => {
|
|
44
32
|
fetchResult();
|
|
45
33
|
}, [fetchResult]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
|
|
3
|
-
|
|
3
|
+
const DEFAULT_VIEW_THRESHOLD = 0.5;
|
|
4
|
+
export default function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, }) {
|
|
4
5
|
const nodeRef = useRef(null);
|
|
5
6
|
const observerRef = useRef(null);
|
|
6
7
|
const intervalRef = useRef(null);
|
|
@@ -48,7 +49,7 @@ export default function useViewportTracking({ tracking, questions, }) {
|
|
|
48
49
|
entryTimeRef.current = null;
|
|
49
50
|
trackingRef.current.trackOutOfView();
|
|
50
51
|
}
|
|
51
|
-
}, { threshold:
|
|
52
|
+
}, { threshold: viewThreshold });
|
|
52
53
|
observer.observe(element);
|
|
53
54
|
observerRef.current = observer;
|
|
54
55
|
intervalRef.current = setInterval(flush, FLUSH_INTERVAL_MS);
|
package/lib/mjs/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;
|
package/lib/mjs/types.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joins class names, dropping the falsy ones, so a conditional class can be written inline.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not `clsx` or `classnames`: neither is a dependency of this package, and the copy of
|
|
5
|
+
* `clsx` in the tree belongs to `@constructor-io/constructorio-ui-components`, which is a peer
|
|
6
|
+
* dependency - importing it here would work locally and break in a consumer that hoists
|
|
7
|
+
* differently. Worth revisiting if this ever needs to accept objects or nested arrays.
|
|
8
|
+
*/
|
|
9
|
+
export const cx = (...parts) => parts.filter(Boolean).join(' ');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { translate } from './translate';
|
|
2
|
+
import { RECS_REFINEMENT_LABEL } from '../constants';
|
|
3
|
+
/**
|
|
4
|
+
* The line that introduces the refinement options. Prefers the prompt the API sent, so our
|
|
5
|
+
* copy and theirs cannot drift.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveRefinementQuestion(refinement, translations) {
|
|
8
|
+
return refinement?.question || translate(RECS_REFINEMENT_LABEL, translations);
|
|
9
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DISCLAIMER_TEXT } from '../constants';
|
|
1
|
+
import { DISCLAIMER_TEXT, RECS_FALLBACK_TITLE, RECS_INPUT_PLACEHOLDER, RECS_LOADING_TITLE, RECS_REFINEMENT_LABEL, RECS_UNSUPPORTED_REQUEST, } from '../constants';
|
|
2
2
|
const defaultTranslations = {
|
|
3
3
|
'Any questions about this product?': 'Any questions about this product?',
|
|
4
4
|
'Ask anything': 'Ask anything',
|
|
@@ -8,6 +8,11 @@ const defaultTranslations = {
|
|
|
8
8
|
'Learn More.': 'Learn More.',
|
|
9
9
|
'Ask about this product': 'Ask about this product',
|
|
10
10
|
'Add to Cart': 'Add to Cart',
|
|
11
|
+
[RECS_LOADING_TITLE]: RECS_LOADING_TITLE,
|
|
12
|
+
[RECS_FALLBACK_TITLE]: RECS_FALLBACK_TITLE,
|
|
13
|
+
[RECS_UNSUPPORTED_REQUEST]: RECS_UNSUPPORTED_REQUEST,
|
|
14
|
+
[RECS_REFINEMENT_LABEL]: RECS_REFINEMENT_LABEL,
|
|
15
|
+
[RECS_INPUT_PLACEHOLDER]: RECS_INPUT_PLACEHOLDER,
|
|
11
16
|
};
|
|
12
17
|
/**
|
|
13
18
|
* Translates a word using the provided translations object.
|
package/lib/mjs/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '2.
|
|
1
|
+
export default '2.12.0';
|
|
@@ -1,38 +1,3 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import MockConstructorIOClient from '../../hooks/mocks/MockConstructorIOClient';
|
|
4
|
-
import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, Translations, SuggestedQuestionsParameters, Formatters, ProductCardDisplayProps } from '../../types';
|
|
5
|
-
export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
|
|
6
|
-
/** Your Constructor.io API key. */
|
|
7
|
-
apiKey: string;
|
|
8
|
-
/** The product item ID to fetch insights for. */
|
|
9
|
-
itemId: string;
|
|
10
|
-
/** The product display name, sent with tracking events. */
|
|
11
|
-
itemName: string;
|
|
12
|
-
/** Thread ID for conversation context. Must be a valid UUID (e.g., "550e8400-e29b-41d4-a716-446655440000"). */
|
|
13
|
-
threadId?: string;
|
|
14
|
-
/** Optional variation ID for the product. */
|
|
15
|
-
variationId?: string;
|
|
16
|
-
/** Optional Constructor.io client instance. If not provided, one will be created internally. */
|
|
17
|
-
cioClient?: MockConstructorIOClient;
|
|
18
|
-
/** Display configuration options (mode, type, showFeedback, etc.). */
|
|
19
|
-
displayConfigs?: CioPiaDisplayConfigs;
|
|
20
|
-
/** Callback handlers for user interactions (onQuestionSubmit, onProductCardClick, onFeedback). */
|
|
21
|
-
callbacks?: Callbacks;
|
|
22
|
-
/** Custom component overrides via reactNode or render props functions. */
|
|
23
|
-
componentOverrides?: CioPiaComponentOverrides;
|
|
24
|
-
/** Formatter functions for transforming data before display. */
|
|
25
|
-
formatters?: Formatters;
|
|
26
|
-
/** Props forwarded to the ProductCard rendered inside the carousel. */
|
|
27
|
-
productCardProps?: ProductCardDisplayProps;
|
|
28
|
-
/** UI string translations for internationalization. */
|
|
29
|
-
translations?: Translations;
|
|
30
|
-
/** Parameters for the suggested questions request. */
|
|
31
|
-
suggestedQuestionsParameters?: SuggestedQuestionsParameters;
|
|
32
|
-
/**
|
|
33
|
-
* Extra query parameters appended to PIA API requests (e.g. `ef-*` test cell params).
|
|
34
|
-
* Define outside the component or wrap with useMemo to avoid unnecessary re-renders.
|
|
35
|
-
*/
|
|
36
|
-
parameters?: Record<string, string | number | boolean>;
|
|
37
|
-
}
|
|
2
|
+
import type { CioPiaProps } from './types';
|
|
38
3
|
export default function CioPia(props: CioPiaProps): React.JSX.Element;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { IncludeComponentOverrides, IncludeRenderProps } from '@constructor-io/constructorio-ui-components';
|
|
2
|
+
import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, CioPiaTrackingConfigs, Translations, SuggestedQuestionsParameters, AnswerRequestParameters, RecsPodParameters, Formatters, ProductCardDisplayProps } from '../../types';
|
|
3
|
+
import type { CioClient } from '../../hooks/usePiaClient';
|
|
4
|
+
export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
|
|
5
|
+
/** Your Constructor.io API key. */
|
|
6
|
+
apiKey: string;
|
|
7
|
+
/** The product item ID to fetch insights for. */
|
|
8
|
+
itemId: string;
|
|
9
|
+
/** The product display name, sent with tracking events. */
|
|
10
|
+
itemName: string;
|
|
11
|
+
/** Thread ID for conversation context. Must be a valid UUID (e.g., "550e8400-e29b-41d4-a716-446655440000"). */
|
|
12
|
+
threadId?: string;
|
|
13
|
+
/** Optional variation ID for the product. */
|
|
14
|
+
variationId?: string;
|
|
15
|
+
/** Optional Constructor.io client instance. If not provided, one will be created internally. */
|
|
16
|
+
cioClient?: CioClient;
|
|
17
|
+
/** Display configuration options (mode, type, showFeedback, etc.). */
|
|
18
|
+
displayConfigs?: CioPiaDisplayConfigs;
|
|
19
|
+
/** Tracking configuration options (viewThreshold, etc.). */
|
|
20
|
+
trackingConfigs?: CioPiaTrackingConfigs;
|
|
21
|
+
/** Callback handlers for user interactions (onQuestionSubmit, onProductCardClick, onFeedback). */
|
|
22
|
+
callbacks?: Callbacks;
|
|
23
|
+
/** Custom component overrides via reactNode or render props functions. */
|
|
24
|
+
componentOverrides?: CioPiaComponentOverrides;
|
|
25
|
+
/** Formatter functions for transforming data before display. */
|
|
26
|
+
formatters?: Formatters;
|
|
27
|
+
/** Props forwarded to the ProductCard rendered inside the carousel. */
|
|
28
|
+
productCardProps?: ProductCardDisplayProps;
|
|
29
|
+
/** UI string translations for internationalization. */
|
|
30
|
+
translations?: Translations;
|
|
31
|
+
/** Parameters for the suggested questions request. */
|
|
32
|
+
suggestedQuestionsParameters?: SuggestedQuestionsParameters;
|
|
33
|
+
/** Parameters for the answer request. */
|
|
34
|
+
answerParameters?: AnswerRequestParameters;
|
|
35
|
+
/** Parameters for the recommendations request, used by `mode: 'recommendations'`. */
|
|
36
|
+
recsPodParameters?: RecsPodParameters;
|
|
37
|
+
/**
|
|
38
|
+
* @deprecated Use `answerParameters` and `suggestedQuestionsParameters` instead.
|
|
39
|
+
*
|
|
40
|
+
* Only the following keys are forwarded (snake_case or camelCase accepted):
|
|
41
|
+
*
|
|
42
|
+
* Suggested-questions endpoint:
|
|
43
|
+
* - `num_results` / `numResults` → numResults
|
|
44
|
+
* - `pre_filter_expression` / `preFilterExpression` → preFilterExpression (JSON-parsed if string)
|
|
45
|
+
*
|
|
46
|
+
* Answer endpoint:
|
|
47
|
+
* - `guard` → guard
|
|
48
|
+
* - `pre_filter_expression` / `preFilterExpression` → preFilterExpression (JSON-parsed if string)
|
|
49
|
+
* - `fmt_options` / `fmtOptions` → fmtOptions (JSON-parsed if string)
|
|
50
|
+
*
|
|
51
|
+
* All other keys (e.g. `ef-*`) are silently dropped.
|
|
52
|
+
* Typed parameters take precedence over values specified here.
|
|
53
|
+
*/
|
|
54
|
+
parameters?: Record<string, string | number | boolean>;
|
|
55
|
+
}
|