@constructor-io/constructorio-ui-pia 2.11.0 → 2.13.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/dist/constructorio-ui-pia-bundled.js +62 -62
- package/lib/cjs/components/CioPia/CioPiaQna.js +10 -4
- package/lib/cjs/components/CioPia/CioPiaStyles.css +5 -0
- package/lib/cjs/components/CioPia/PiaCustomCarousel.js +34 -29
- package/lib/cjs/components/ConversationHistory/ConversationHistory.js +10 -5
- package/lib/cjs/components/Error/ErrorBlock.js +7 -5
- package/lib/cjs/components/Feedback/Feedback.js +4 -4
- package/lib/cjs/components/Input/Input.js +2 -1
- package/lib/cjs/components/LoadingSkeleton/LoadingSkeleton.js +2 -1
- package/lib/cjs/components/PiaConversation/PiaModal.js +22 -4
- package/lib/cjs/components/StatusRegion/StatusRegion.js +34 -0
- package/lib/cjs/components/SuggestedQuestionsContainer/SuggestedQuestionsSkeleton.js +1 -1
- package/lib/cjs/components/icons/QuestionIcon.js +1 -1
- package/lib/cjs/components/icons/SendIcon.js +1 -1
- package/lib/cjs/components/icons/SparklesIcon.js +1 -1
- package/lib/cjs/hooks/useAnswerResults.js +4 -14
- package/lib/cjs/hooks/useCioPia.js +42 -4
- package/lib/cjs/hooks/useConversation.js +10 -8
- package/lib/cjs/hooks/usePiaClient.js +2 -7
- package/lib/cjs/hooks/useRecsPod.js +10 -5
- package/lib/cjs/hooks/useSuggestedQuestions.js +10 -26
- package/lib/cjs/types.js +0 -2
- package/lib/cjs/utils/translate.js +12 -0
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/CioPia/CioPiaQna.js +10 -4
- package/lib/mjs/components/CioPia/CioPiaStyles.css +5 -0
- package/lib/mjs/components/CioPia/PiaCustomCarousel.js +53 -30
- package/lib/mjs/components/ConversationHistory/ConversationHistory.js +10 -5
- package/lib/mjs/components/Error/ErrorBlock.js +7 -5
- package/lib/mjs/components/Feedback/Feedback.js +4 -4
- package/lib/mjs/components/Input/Input.js +2 -1
- package/lib/mjs/components/LoadingSkeleton/LoadingSkeleton.js +2 -1
- package/lib/mjs/components/PiaConversation/PiaModal.js +22 -4
- package/lib/mjs/components/StatusRegion/StatusRegion.js +28 -0
- package/lib/mjs/components/SuggestedQuestionsContainer/SuggestedQuestionsSkeleton.js +1 -1
- package/lib/mjs/components/icons/QuestionIcon.js +1 -1
- package/lib/mjs/components/icons/SendIcon.js +1 -1
- package/lib/mjs/components/icons/SparklesIcon.js +1 -1
- package/lib/mjs/hooks/useAnswerResults.js +4 -13
- package/lib/mjs/hooks/useCioPia.js +43 -4
- package/lib/mjs/hooks/useConversation.js +7 -5
- package/lib/mjs/hooks/usePiaClient.js +2 -7
- package/lib/mjs/hooks/useRecsPod.js +9 -5
- package/lib/mjs/hooks/useSuggestedQuestions.js +12 -24
- package/lib/mjs/types.js +0 -1
- package/lib/mjs/utils/translate.js +12 -0
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/CioPia/types.d.ts +20 -5
- package/lib/types/components/ConversationHistory/ConversationHistory.d.ts +7 -1
- package/lib/types/components/Error/ErrorBlock.d.ts +9 -1
- package/lib/types/components/StatusRegion/StatusRegion.d.ts +13 -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 +4 -9
- package/lib/types/hooks/useRecsPod.d.ts +2 -2
- package/lib/types/hooks/useSuggestedQuestions.d.ts +3 -4
- package/lib/types/types.d.ts +53 -6
- package/lib/types/utils/translate.d.ts +3 -0
- package/lib/types/version.d.ts +1 -1
- package/package.json +3 -3
- package/lib/cjs/hooks/mocks/MockConstructorIOClient.js +0 -24
- package/lib/cjs/hooks/mocks/agent.js +0 -183
- package/lib/cjs/hooks/mocks/types.js +0 -2
- package/lib/mjs/hooks/mocks/MockConstructorIOClient.js +0 -38
- package/lib/mjs/hooks/mocks/agent.js +0 -173
- package/lib/mjs/hooks/mocks/types.js +0 -1
- package/lib/types/hooks/mocks/MockConstructorIOClient.d.ts +0 -14
- package/lib/types/hooks/mocks/agent.d.ts +0 -23
- package/lib/types/hooks/mocks/types.d.ts +0 -77
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const react_1 = require("react");
|
|
4
4
|
function useConversation({ pia, itemId, isConversation, callbacks, tracking, }) {
|
|
5
|
-
var _a, _b, _c
|
|
5
|
+
var _a, _b, _c;
|
|
6
6
|
const { suggestedQuestions, answers, threadId } = pia;
|
|
7
7
|
const { getAnswer } = answers;
|
|
8
8
|
const context = (0, react_1.useMemo)(() => ({ itemId, threadId }), [itemId, threadId]);
|
|
@@ -10,7 +10,7 @@ function useConversation({ pia, itemId, isConversation, callbacks, tracking, })
|
|
|
10
10
|
const [displayedQuestions, setDisplayedQuestions] = (0, react_1.useState)([]);
|
|
11
11
|
const [conversationHistory, setConversationHistory] = (0, react_1.useState)([]);
|
|
12
12
|
const entryIdRef = (0, react_1.useRef)(0);
|
|
13
|
-
const
|
|
13
|
+
const prevAnswerDataRef = (0, react_1.useRef)(answers.data);
|
|
14
14
|
const hasTrackedCurrentAnswerRef = (0, react_1.useRef)(false);
|
|
15
15
|
const answersRef = (0, react_1.useRef)(answers);
|
|
16
16
|
const callbacksRef = (0, react_1.useRef)(callbacks);
|
|
@@ -68,13 +68,13 @@ function useConversation({ pia, itemId, isConversation, callbacks, tracking, })
|
|
|
68
68
|
setCurrentQuestion('');
|
|
69
69
|
setDisplayedQuestions(suggestedQuestions.data);
|
|
70
70
|
setConversationHistory([]);
|
|
71
|
-
|
|
71
|
+
prevAnswerDataRef.current = null;
|
|
72
72
|
}, [suggestedQuestions.data]);
|
|
73
73
|
(0, react_1.useEffect)(() => {
|
|
74
74
|
setCurrentQuestion('');
|
|
75
75
|
setDisplayedQuestions([]);
|
|
76
76
|
setConversationHistory([]);
|
|
77
|
-
|
|
77
|
+
prevAnswerDataRef.current = null;
|
|
78
78
|
}, [itemId]);
|
|
79
79
|
(0, react_1.useEffect)(() => {
|
|
80
80
|
setDisplayedQuestions(suggestedQuestions.data);
|
|
@@ -93,9 +93,11 @@ function useConversation({ pia, itemId, isConversation, callbacks, tracking, })
|
|
|
93
93
|
const answerValue = (_b = (_a = answers.data) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '';
|
|
94
94
|
if (!answerValue)
|
|
95
95
|
return;
|
|
96
|
-
|
|
96
|
+
// Compare by response object, not by text: two consecutive answers with
|
|
97
|
+
// identical text are still two answers and both must reach the history.
|
|
98
|
+
if (answers.data === prevAnswerDataRef.current)
|
|
97
99
|
return;
|
|
98
|
-
|
|
100
|
+
prevAnswerDataRef.current = answers.data;
|
|
99
101
|
const answerThreadId = (_c = answers.data) === null || _c === void 0 ? void 0 : _c.thread_id;
|
|
100
102
|
const qnaResultId = (_d = answers.data) === null || _d === void 0 ? void 0 : _d.qna_result_id;
|
|
101
103
|
if (isConversation) {
|
|
@@ -122,8 +124,8 @@ function useConversation({ pia, itemId, isConversation, callbacks, tracking, })
|
|
|
122
124
|
(_f = (_e = callbacksRef.current) === null || _e === void 0 ? void 0 : _e.onAnswer) === null || _f === void 0 ? void 0 : _f.call(_e, [entry], contextRef.current);
|
|
123
125
|
}
|
|
124
126
|
}, [isConversation, answers.data, answers.items]);
|
|
125
|
-
const currentAnswer = (
|
|
126
|
-
const currentItems = (
|
|
127
|
+
const currentAnswer = (_b = (_a = answers.data) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : '';
|
|
128
|
+
const currentItems = (_c = answers.items) !== null && _c !== void 0 ? _c : null;
|
|
127
129
|
const error = answers.error || suggestedQuestions.error;
|
|
128
130
|
const isLoading = answers.isLoading || suggestedQuestions.isLoading;
|
|
129
131
|
return {
|
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
4
|
const react_1 = require("react");
|
|
5
|
-
const
|
|
5
|
+
const constructorio_client_javascript_1 = tslib_1.__importDefault(require("@constructor-io/constructorio-client-javascript"));
|
|
6
6
|
const version_1 = tslib_1.__importDefault(require("../version"));
|
|
7
|
-
/**
|
|
8
|
-
* Resolves the client and thread ID every mode needs: the caller's client when they supplied
|
|
9
|
-
* one, otherwise a client built from the API key, plus a generated thread ID that stays stable
|
|
10
|
-
* for the life of the component.
|
|
11
|
-
*/
|
|
12
7
|
function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }) {
|
|
13
8
|
const [generatedThreadId] = (0, react_1.useState)(() => crypto.randomUUID());
|
|
14
9
|
const threadId = providedThreadId || generatedThreadId;
|
|
15
10
|
const client = (0, react_1.useMemo)(() => {
|
|
16
11
|
if (providedClient)
|
|
17
12
|
return providedClient;
|
|
18
|
-
return new
|
|
13
|
+
return new constructorio_client_javascript_1.default({
|
|
19
14
|
apiKey,
|
|
20
15
|
sendTrackingEvents: true,
|
|
21
16
|
version: `cio-ui-pia-${version_1.default}`,
|
|
@@ -33,9 +33,14 @@ function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, form
|
|
|
33
33
|
const strategy = (parameters === null || parameters === void 0 ? void 0 : parameters.strategy) || DEFAULT_STRATEGY;
|
|
34
34
|
const { numResults, defaultTitle } = parameters || {};
|
|
35
35
|
const fetchResult = (0, react_1.useCallback)((shopperInput, source) => {
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
36
|
+
var _a;
|
|
37
|
+
// TODO: once getRecs is added to the JS client SDK, replace this guard+cast
|
|
38
|
+
// with a direct call: cioClient.agent.getRecs({...})
|
|
39
|
+
const getRecs = (_a = cioClient === null || cioClient === void 0 ? void 0 : cioClient.agent) === null || _a === void 0 ? void 0 : _a.getRecs;
|
|
40
|
+
if (!cioClient || !getRecs) {
|
|
41
|
+
if (cioClient && !getRecs) {
|
|
42
|
+
console.info('[CioPia] getRecs is not available on the client SDK yet.');
|
|
43
|
+
}
|
|
39
44
|
setIsLoading(false);
|
|
40
45
|
return;
|
|
41
46
|
}
|
|
@@ -45,8 +50,8 @@ function useRecsPod({ itemId, variationId, threadId, cioClient, parameters, form
|
|
|
45
50
|
setIsLoading(true);
|
|
46
51
|
setError(null);
|
|
47
52
|
setHasUnsupportedInput(false);
|
|
48
|
-
|
|
49
|
-
.
|
|
53
|
+
getRecs
|
|
54
|
+
.call(cioClient.agent, {
|
|
50
55
|
itemId,
|
|
51
56
|
variationId,
|
|
52
57
|
threadId,
|
|
@@ -1,38 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const tslib_1 = require("tslib");
|
|
4
3
|
const react_1 = require("react");
|
|
5
|
-
|
|
6
|
-
const response = yield client.agent.getSuggestedQuestions({
|
|
7
|
-
itemId,
|
|
8
|
-
variationId,
|
|
9
|
-
threadId,
|
|
10
|
-
parameters,
|
|
11
|
-
requestParameters,
|
|
12
|
-
});
|
|
13
|
-
return response.questions;
|
|
14
|
-
});
|
|
15
|
-
function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, requestParameters, }) {
|
|
4
|
+
function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, }) {
|
|
16
5
|
const [questions, setQuestions] = (0, react_1.useState)([]);
|
|
17
6
|
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
18
7
|
const [error, setError] = (0, react_1.useState)(null);
|
|
19
|
-
|
|
20
|
-
const requestParametersKey = JSON.stringify(requestParameters);
|
|
8
|
+
const serializedParameters = (0, react_1.useMemo)(() => JSON.stringify(parameters), [parameters]);
|
|
21
9
|
const fetchResult = (0, react_1.useCallback)(() => {
|
|
22
10
|
if (!cioClient)
|
|
23
11
|
return;
|
|
24
12
|
setIsLoading(true);
|
|
25
13
|
setError(null);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
parameters,
|
|
32
|
-
requestParameters,
|
|
33
|
-
})
|
|
34
|
-
.then((fetchedQuestions) => {
|
|
35
|
-
setQuestions(fetchedQuestions);
|
|
14
|
+
cioClient.agent.pia
|
|
15
|
+
.getSuggestedQuestions(itemId, Object.assign({ threadId,
|
|
16
|
+
variationId }, parameters))
|
|
17
|
+
.then((response) => {
|
|
18
|
+
setQuestions(response.questions);
|
|
36
19
|
setError(null);
|
|
37
20
|
})
|
|
38
21
|
.catch((err) => {
|
|
@@ -41,8 +24,9 @@ function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, param
|
|
|
41
24
|
.finally(() => {
|
|
42
25
|
setIsLoading(false);
|
|
43
26
|
});
|
|
44
|
-
//
|
|
45
|
-
|
|
27
|
+
// parameters is serialized via serializedParameters to prevent refetch on identity changes
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, [cioClient, itemId, variationId, threadId, serializedParameters]);
|
|
46
30
|
(0, react_1.useEffect)(() => {
|
|
47
31
|
fetchResult();
|
|
48
32
|
}, [fetchResult]);
|
package/lib/cjs/types.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FeedbackType = void 0;
|
|
4
|
-
const tslib_1 = require("tslib");
|
|
5
4
|
var FeedbackType;
|
|
6
5
|
(function (FeedbackType) {
|
|
7
6
|
FeedbackType["UP"] = "up";
|
|
8
7
|
FeedbackType["DOWN"] = "down";
|
|
9
8
|
})(FeedbackType = exports.FeedbackType || (exports.FeedbackType = {}));
|
|
10
|
-
tslib_1.__exportStar(require("./hooks/mocks/types"), exports);
|
|
@@ -5,12 +5,21 @@ const constants_1 = require("../constants");
|
|
|
5
5
|
const defaultTranslations = {
|
|
6
6
|
'Any questions about this product?': 'Any questions about this product?',
|
|
7
7
|
'Ask anything': 'Ask anything',
|
|
8
|
+
'Your question': 'Your question',
|
|
8
9
|
Send: 'Send',
|
|
9
10
|
[constants_1.DISCLAIMER_TEXT]: constants_1.DISCLAIMER_TEXT,
|
|
10
11
|
'Is this answer useful?': 'Is this answer useful?',
|
|
11
12
|
'Learn More.': 'Learn More.',
|
|
12
13
|
'Ask about this product': 'Ask about this product',
|
|
13
14
|
'Add to Cart': 'Add to Cart',
|
|
15
|
+
'Unexpected error': 'Unexpected error',
|
|
16
|
+
'thumbs up': 'thumbs up',
|
|
17
|
+
'thumbs down': 'thumbs down',
|
|
18
|
+
Close: 'Close',
|
|
19
|
+
Retry: 'Retry',
|
|
20
|
+
'Conversation history': 'Conversation history',
|
|
21
|
+
'Loading answer': 'Loading answer',
|
|
22
|
+
'Answer ready': 'Answer ready',
|
|
14
23
|
[constants_1.RECS_LOADING_TITLE]: constants_1.RECS_LOADING_TITLE,
|
|
15
24
|
[constants_1.RECS_FALLBACK_TITLE]: constants_1.RECS_FALLBACK_TITLE,
|
|
16
25
|
[constants_1.RECS_UNSUPPORTED_REQUEST]: constants_1.RECS_UNSUPPORTED_REQUEST,
|
|
@@ -21,6 +30,9 @@ const defaultTranslations = {
|
|
|
21
30
|
* Translates a word using the provided translations object.
|
|
22
31
|
* Falls back to English defaults if translation is not provided.
|
|
23
32
|
*
|
|
33
|
+
* An explicitly empty string is returned as-is: blanking out a string is how
|
|
34
|
+
* consumers hide it.
|
|
35
|
+
*
|
|
24
36
|
* @param word - The key to translate
|
|
25
37
|
* @param translations - Optional user-provided translations object
|
|
26
38
|
* @returns The translated string or the original word if no translation exists
|
package/lib/cjs/version.js
CHANGED
|
@@ -14,10 +14,11 @@ import { translate } from '../../utils/translate';
|
|
|
14
14
|
import PiaInlineAnswer from '../PiaInlineAnswer/PiaInlineAnswer';
|
|
15
15
|
import PiaModal from '../PiaConversation/PiaModal';
|
|
16
16
|
import PiaConversation from '../PiaConversation/PiaConversation';
|
|
17
|
+
import StatusRegion, { answerStatusMessage } from '../StatusRegion/StatusRegion';
|
|
17
18
|
/** The question-and-answer experience: `mode: 'default'`, `mode: 'conversation'`, and the modal. */
|
|
18
19
|
// eslint-disable-next-line complexity
|
|
19
20
|
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 { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, answerParameters, parameters, trackingConfigs, } = props;
|
|
21
22
|
const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
|
|
22
23
|
const isConversation = mode === 'conversation' || type === 'modal';
|
|
23
24
|
const { priceCurrency } = productCardProps || {};
|
|
@@ -28,6 +29,7 @@ export default function CioPiaQna(props) {
|
|
|
28
29
|
variationId,
|
|
29
30
|
cioClient,
|
|
30
31
|
suggestedQuestionsParameters,
|
|
32
|
+
answerParameters,
|
|
31
33
|
parameters,
|
|
32
34
|
formatImageUrl: formatters?.formatImageUrl,
|
|
33
35
|
});
|
|
@@ -50,10 +52,12 @@ export default function CioPiaQna(props) {
|
|
|
50
52
|
callbackContainerRef(node);
|
|
51
53
|
}, [viewportContainerRef, callbackContainerRef]);
|
|
52
54
|
const qnaResultId = pia.answers.data?.qna_result_id;
|
|
53
|
-
// The text bars
|
|
55
|
+
// The text bars and the "Loading answer" status track the answer request only. The question
|
|
54
56
|
// row keeps the combined `isLoading`: the follow-up questions that refill it arrive with the
|
|
55
57
|
// answer, so the row is waiting on either request.
|
|
56
58
|
const isAnswerLoading = pia.answers.isLoading;
|
|
59
|
+
// Inline mode renders the answer outside any live region.
|
|
60
|
+
const answerStatus = answerStatusMessage(isAnswerLoading, !!currentAnswer, translations);
|
|
57
61
|
const renderProps = {
|
|
58
62
|
items: currentItems,
|
|
59
63
|
isLoading,
|
|
@@ -67,6 +71,7 @@ export default function CioPiaQna(props) {
|
|
|
67
71
|
const conversationHistoryProps = {
|
|
68
72
|
conversationHistory,
|
|
69
73
|
isLoading,
|
|
74
|
+
isAnswerLoading,
|
|
70
75
|
error,
|
|
71
76
|
currentItems,
|
|
72
77
|
showFeedback,
|
|
@@ -98,8 +103,9 @@ export default function CioPiaQna(props) {
|
|
|
98
103
|
React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
|
|
99
104
|
React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides?.input }),
|
|
100
105
|
isAnswerLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
|
|
101
|
-
!isAnswerLoading && error && React.createElement(ErrorBlock, {
|
|
106
|
+
!isAnswerLoading && error && (React.createElement(ErrorBlock, { key: error.message || translate('Unexpected error', translations), message: error.message, translations: translations })),
|
|
102
107
|
!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
108
|
isLoading && React.createElement(SuggestedQuestionsSkeleton, null),
|
|
104
|
-
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })))
|
|
109
|
+
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions }))),
|
|
110
|
+
React.createElement(StatusRegion, { message: answerStatus, "data-testid": 'answer-status' })));
|
|
105
111
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useCallback,
|
|
2
|
-
import { Carousel, ProductCard,
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { Carousel, ProductCard, } from '@constructor-io/constructorio-ui-components';
|
|
3
3
|
import { sanitizeHtml } from '../../utils/contentTransformers';
|
|
4
4
|
import { translate } from '../../utils/translate';
|
|
5
5
|
function HtmlDescription({ product }) {
|
|
@@ -8,6 +8,13 @@ function HtmlDescription({ product }) {
|
|
|
8
8
|
return null;
|
|
9
9
|
return (React.createElement("p", { className: 'cio-product-card-description', dangerouslySetInnerHTML: { __html: sanitizeHtml(description) } }));
|
|
10
10
|
}
|
|
11
|
+
// The card root is focusable and acts as a link, so Enter must do what a click does.
|
|
12
|
+
// Only for the card itself: Enter on the Add to Cart button inside it is that button's.
|
|
13
|
+
function handleCardKeyDown(event) {
|
|
14
|
+
if (event.key === 'Enter' && event.target === event.currentTarget) {
|
|
15
|
+
event.currentTarget.click();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
11
18
|
function createPriceSectionOverride(priceCurrency) {
|
|
12
19
|
return function PriceSectionOverride({ product }) {
|
|
13
20
|
const { price, salePrice } = product;
|
|
@@ -55,12 +62,10 @@ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
|
|
|
55
62
|
};
|
|
56
63
|
}
|
|
57
64
|
export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
|
|
58
|
-
const wrapperRef = useRef(null);
|
|
59
65
|
const { onAddToCart } = callbacks || {};
|
|
60
66
|
const addToCartHandler = useCallback((event, product) => {
|
|
61
67
|
onAddToCart?.(product, event);
|
|
62
68
|
}, [onAddToCart]);
|
|
63
|
-
// Determine to use user-defined click handler or default behavior
|
|
64
69
|
const productClickHandler = useCallback((item) => {
|
|
65
70
|
const position = items.findIndex((i) => i.id === item.id);
|
|
66
71
|
if (onResultClick && question) {
|
|
@@ -74,47 +79,65 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
|
|
|
74
79
|
window.open(item.url, '_blank', 'noopener,noreferrer');
|
|
75
80
|
}
|
|
76
81
|
}, [callbacks, items, onResultClick, question, qnaResultId]);
|
|
77
|
-
// Set up event listener for product card clicks
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
const el = wrapperRef.current;
|
|
80
|
-
if (!el)
|
|
81
|
-
return undefined;
|
|
82
|
-
const handleClick = (e) => {
|
|
83
|
-
const { product } = e.detail;
|
|
84
|
-
if (product) {
|
|
85
|
-
productClickHandler(product);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
el.addEventListener(CIO_EVENTS.productCard.click, handleClick);
|
|
89
|
-
return () => {
|
|
90
|
-
el.removeEventListener(CIO_EVENTS.productCard.click, handleClick);
|
|
91
|
-
};
|
|
92
|
-
}, [productClickHandler]);
|
|
93
82
|
const priceSectionOverride = useMemo(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
|
|
83
|
+
const clickHandlerProps = useMemo(() => ({
|
|
84
|
+
onProductClick: productClickHandler,
|
|
85
|
+
onAddToCart: onAddToCart ? addToCartHandler : undefined,
|
|
86
|
+
}), [productClickHandler, onAddToCart, addToCartHandler]);
|
|
94
87
|
const mergedOverrides = useMemo(() => {
|
|
95
88
|
const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
|
|
89
|
+
// Override priority: carousel.reactNode → item.reactNode → item.productCard.reactNode → default ProductCard.
|
|
90
|
+
// The casts are required because the override signatures don't declare these extra props,
|
|
91
|
+
// but consumers receive them as part of the render-props contract documented in CioPiaProps.
|
|
92
|
+
const carouselOverride = baseOverrides.reactNode;
|
|
93
|
+
if (typeof carouselOverride === 'function') {
|
|
94
|
+
return {
|
|
95
|
+
...baseOverrides,
|
|
96
|
+
reactNode: (props) => carouselOverride({ ...props, ...clickHandlerProps }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
96
99
|
const cardOverrides = baseOverrides.item?.productCard;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
const itemOverride = baseOverrides.item?.reactNode;
|
|
101
|
+
if (typeof itemOverride === 'function') {
|
|
102
|
+
return {
|
|
103
|
+
...baseOverrides,
|
|
104
|
+
item: {
|
|
105
|
+
...baseOverrides.item,
|
|
106
|
+
reactNode: (props) => itemOverride({ ...props, ...clickHandlerProps }),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (typeof cardOverrides?.reactNode === 'function') {
|
|
111
|
+
const originalReactNode = cardOverrides.reactNode;
|
|
112
|
+
return {
|
|
113
|
+
...baseOverrides,
|
|
114
|
+
item: {
|
|
115
|
+
...baseOverrides.item,
|
|
116
|
+
productCard: {
|
|
117
|
+
...cardOverrides,
|
|
118
|
+
reactNode: (props) => originalReactNode({ ...props, ...clickHandlerProps }),
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
101
122
|
}
|
|
102
|
-
|
|
123
|
+
// Default: render ProductCard with handlers directly
|
|
124
|
+
const addToCartText = onAddToCart ? translate('Add to Cart', translations) : undefined;
|
|
103
125
|
return {
|
|
104
126
|
...baseOverrides,
|
|
105
127
|
item: {
|
|
106
128
|
...baseOverrides.item,
|
|
107
129
|
productCard: {
|
|
108
130
|
...cardOverrides,
|
|
109
|
-
reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full',
|
|
131
|
+
reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full',
|
|
132
|
+
// Reachable by Tab. The name is the product name: a name computed from the
|
|
133
|
+
// card's content would also swallow the Add to Cart button's text.
|
|
134
|
+
role: 'link', "aria-label": product.name || undefined, tabIndex: 0, onKeyDown: handleCardKeyDown, addToCartText: addToCartText, onAddToCart: clickHandlerProps.onAddToCart, onProductClick: clickHandlerProps.onProductClick, componentOverrides: cardOverrides })),
|
|
110
135
|
},
|
|
111
136
|
},
|
|
112
137
|
};
|
|
113
|
-
}, [componentOverrides, priceSectionOverride, onAddToCart, translations
|
|
114
|
-
// If there are no items, do not render the carousel
|
|
138
|
+
}, [componentOverrides, priceSectionOverride, clickHandlerProps, onAddToCart, translations]);
|
|
115
139
|
if (items.length === 0) {
|
|
116
140
|
return null;
|
|
117
141
|
}
|
|
118
|
-
return
|
|
119
|
-
React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides })));
|
|
142
|
+
return React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides });
|
|
120
143
|
}
|
|
@@ -5,7 +5,9 @@ import Disclaimer from '../CioPia/Disclaimer';
|
|
|
5
5
|
import ErrorBlock from '../Error/ErrorBlock';
|
|
6
6
|
import LoadingSkeleton from '../LoadingSkeleton/LoadingSkeleton';
|
|
7
7
|
import PiaCustomCarousel from '../CioPia/PiaCustomCarousel';
|
|
8
|
-
|
|
8
|
+
import StatusRegion, { answerStatusMessage } from '../StatusRegion/StatusRegion';
|
|
9
|
+
import { translate } from '../../utils/translate';
|
|
10
|
+
export default function ConversationHistory({ conversationHistory, isLoading, isAnswerLoading = isLoading, error, currentItems, showFeedback, showPreviousItems = true, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, priceCurrency, }) {
|
|
9
11
|
const scrollContainerRef = useRef(null);
|
|
10
12
|
useEffect(() => {
|
|
11
13
|
const container = scrollContainerRef.current;
|
|
@@ -16,23 +18,26 @@ export default function ConversationHistory({ conversationHistory, isLoading, er
|
|
|
16
18
|
});
|
|
17
19
|
return () => cancelAnimationFrame(frameId);
|
|
18
20
|
}, [conversationHistory, isLoading]);
|
|
21
|
+
const lastEntry = conversationHistory[conversationHistory.length - 1];
|
|
22
|
+
const answerStatus = answerStatusMessage(isAnswerLoading, !!lastEntry?.answer, translations);
|
|
19
23
|
const disclaimer = (React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer }));
|
|
20
24
|
return (React.createElement("div", { className: 'cio-pia-conversation-history' },
|
|
21
25
|
disclaimerPosition === 'top' && disclaimer,
|
|
22
|
-
React.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-entries', role: 'log', "aria-label": 'Conversation history' }, conversationHistory.map((entry, index) => {
|
|
26
|
+
React.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-entries', role: 'log', tabIndex: 0, "aria-label": translate('Conversation history', translations) }, conversationHistory.map((entry, index) => {
|
|
23
27
|
const isLast = index === conversationHistory.length - 1;
|
|
24
28
|
const previousEntryItems = showPreviousItems ? entry.items : null;
|
|
25
29
|
const latestEntryItems = currentItems !== undefined ? currentItems : entry.items;
|
|
26
30
|
const carouselItems = isLast ? latestEntryItems : previousEntryItems;
|
|
27
31
|
return (React.createElement("div", { key: entry.id, className: 'cio-pia-conversation-entry' },
|
|
28
32
|
React.createElement("div", { className: 'cio-pia-chat-question' }, entry.question),
|
|
29
|
-
isLast && isLoading && (React.createElement("div", { className: 'cio-pia-conversation-loading'
|
|
33
|
+
isLast && isLoading && (React.createElement("div", { className: 'cio-pia-conversation-loading' },
|
|
30
34
|
React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }))),
|
|
31
|
-
isLast && !isLoading && error && (React.createElement(ErrorBlock, {
|
|
35
|
+
isLast && !isLoading && error && (React.createElement(ErrorBlock, { key: error.message || translate('Unexpected error', translations), message: error.message, translations: translations, announce: false })),
|
|
32
36
|
entry.answer && (React.createElement("div", { className: 'cio-pia-answer-container' },
|
|
33
37
|
React.createElement(Answer, { text: entry.answer, componentOverride: componentOverrides?.answer }),
|
|
34
38
|
carouselItems && (React.createElement(PiaCustomCarousel, { items: carouselItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId, translations: translations, priceCurrency: priceCurrency })),
|
|
35
39
|
isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: handleFeedback, componentOverride: componentOverrides?.feedback }))))));
|
|
36
40
|
})),
|
|
37
|
-
disclaimerPosition === 'bottom' && disclaimer
|
|
41
|
+
disclaimerPosition === 'bottom' && disclaimer,
|
|
42
|
+
React.createElement(StatusRegion, { message: answerStatus, "data-testid": 'answer-status' })));
|
|
38
43
|
}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { translate } from '../../utils/translate';
|
|
2
3
|
function WarningIcon() {
|
|
3
|
-
return (React.createElement("svg", { width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
|
|
4
|
+
return (React.createElement("svg", { width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' },
|
|
4
5
|
React.createElement("path", { d: 'M12.0004 2C6.4804 2 2.0004 6.48 2.0004 12C2.0004 17.52 6.4804 22 12.0004 22C17.5204 22 22.0004 17.52 22.0004 12C22.0004 6.48 17.5204 2 12.0004 2ZM13.0004 17H11.0004V15H13.0004V17ZM13.0004 13H11.0004V7H13.0004V13Z', fill: '#F1923A' })));
|
|
5
6
|
}
|
|
6
7
|
function RetryIcon() {
|
|
7
|
-
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
|
|
8
|
+
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' },
|
|
8
9
|
React.createElement("path", { d: 'M11.7633 4.23329C10.7966 3.26663 9.46992 2.66663 7.99659 2.66663C5.04992 2.66663 2.66992 5.05329 2.66992 7.99996C2.66992 10.9466 5.04992 13.3333 7.99659 13.3333C10.4833 13.3333 12.5566 11.6333 13.1499 9.33329H11.7633C11.2166 10.8866 9.73659 12 7.99659 12C5.78992 12 3.99659 10.2066 3.99659 7.99996C3.99659 5.79329 5.78992 3.99996 7.99659 3.99996C9.10326 3.99996 10.0899 4.45996 10.8099 5.18663L8.66326 7.33329H13.3299V2.66663L11.7633 4.23329Z', fill: 'white' })));
|
|
9
10
|
}
|
|
10
|
-
function ErrorBlock({ message, onRetry = undefined }) {
|
|
11
|
+
function ErrorBlock({ message, onRetry = undefined, translations, announce = true, }) {
|
|
12
|
+
const resolvedMessage = message || translate('Unexpected error', translations);
|
|
11
13
|
return (React.createElement("div", { className: 'cio-pia-error-block-container', "data-testid": 'error-block' },
|
|
12
14
|
React.createElement("div", { className: 'cio-pia-error-block-warning-icon-container', "data-testid": 'error-block-warning-icon' },
|
|
13
15
|
React.createElement(WarningIcon, null)),
|
|
14
16
|
React.createElement("div", { className: 'cio-pia-error-block-text-container' },
|
|
15
|
-
React.createElement("p", { className: 'cio-pia-error-block-text' },
|
|
17
|
+
React.createElement("p", { className: 'cio-pia-error-block-text', role: announce ? 'alert' : undefined }, resolvedMessage),
|
|
16
18
|
onRetry && (React.createElement("button", { type: 'button', onClick: onRetry, className: 'cio-pia-error-block-retry-button' },
|
|
17
19
|
React.createElement(RetryIcon, null),
|
|
18
|
-
|
|
20
|
+
translate('Retry', translations))))));
|
|
19
21
|
}
|
|
20
22
|
export default ErrorBlock;
|
|
@@ -3,10 +3,10 @@ import { RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components
|
|
|
3
3
|
import { FeedbackType } from '../../types';
|
|
4
4
|
import { translate } from '../../utils/translate';
|
|
5
5
|
function ThumbsUpIcon({ isSelected }) {
|
|
6
|
-
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' }, isSelected ? (React.createElement("path", { d: 'M3.33317 6.66667H0.666504V14.6667H3.33317V6.66667ZM5.99984 14.6667C5.2665 14.6667 4.6665 14.0667 4.6665 13.3333V6.66667C4.6665 6.3 4.81317 5.96667 5.05317 5.72667L9.4465 1.33333L10.1532 2.03333C10.3332 2.21333 10.4465 2.46667 10.4465 2.74L10.4265 2.95333L9.79317 6.00001H13.9998C14.7332 6.00001 15.3332 6.60001 15.3332 7.33334V8.66668C15.3332 8.84001 15.2998 9.00001 15.2398 9.15334L13.2265 13.8533C13.0265 14.3333 12.5532 14.6667 11.9998 14.6667H5.99984Z', fill: '#4CAF50', fillOpacity: '1' })) : (React.createElement("path", { d: 'M5.99984 14.6667H11.9998C12.5532 14.6667 13.0265 14.3333 13.2265 13.8533L15.2398 9.15334C15.2998 9.00001 15.3332 8.84001 15.3332 8.66668V7.33334C15.3332 6.60001 14.7332 6.00001 13.9998 6.00001H9.79317L10.4265 2.95334L10.4465 2.74001C10.4465 2.46668 10.3332 2.21334 10.1532 2.03334L9.4465 1.33334L5.05317 5.72668C4.81317 5.96668 4.6665 6.30001 4.6665 6.66668V13.3333C4.6665 14.0667 5.2665 14.6667 5.99984 14.6667ZM5.99984 6.66668L8.89317 3.77334L7.99984 7.33334H13.9998V8.66668L11.9998 13.3333H5.99984V6.66668ZM0.666504 6.66668H3.33317V14.6667H0.666504V6.66668Z', fill: '#0F1324', fillOpacity: '0.6' }))));
|
|
6
|
+
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' }, isSelected ? (React.createElement("path", { d: 'M3.33317 6.66667H0.666504V14.6667H3.33317V6.66667ZM5.99984 14.6667C5.2665 14.6667 4.6665 14.0667 4.6665 13.3333V6.66667C4.6665 6.3 4.81317 5.96667 5.05317 5.72667L9.4465 1.33333L10.1532 2.03333C10.3332 2.21333 10.4465 2.46667 10.4465 2.74L10.4265 2.95333L9.79317 6.00001H13.9998C14.7332 6.00001 15.3332 6.60001 15.3332 7.33334V8.66668C15.3332 8.84001 15.2998 9.00001 15.2398 9.15334L13.2265 13.8533C13.0265 14.3333 12.5532 14.6667 11.9998 14.6667H5.99984Z', fill: '#4CAF50', fillOpacity: '1' })) : (React.createElement("path", { d: 'M5.99984 14.6667H11.9998C12.5532 14.6667 13.0265 14.3333 13.2265 13.8533L15.2398 9.15334C15.2998 9.00001 15.3332 8.84001 15.3332 8.66668V7.33334C15.3332 6.60001 14.7332 6.00001 13.9998 6.00001H9.79317L10.4265 2.95334L10.4465 2.74001C10.4465 2.46668 10.3332 2.21334 10.1532 2.03334L9.4465 1.33334L5.05317 5.72668C4.81317 5.96668 4.6665 6.30001 4.6665 6.66668V13.3333C4.6665 14.0667 5.2665 14.6667 5.99984 14.6667ZM5.99984 6.66668L8.89317 3.77334L7.99984 7.33334H13.9998V8.66668L11.9998 13.3333H5.99984V6.66668ZM0.666504 6.66668H3.33317V14.6667H0.666504V6.66668Z', fill: '#0F1324', fillOpacity: '0.6' }))));
|
|
7
7
|
}
|
|
8
8
|
function ThumbsDownIcon({ isSelected }) {
|
|
9
|
-
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' }, isSelected ? (React.createElement("path", { d: 'M12.6665 9.33334H15.3332V1.33334H12.6665V9.33334ZM9.99984 1.33334C10.7332 1.33334 11.3332 1.93334 11.3332 2.66668V9.33334C11.3332 9.70001 11.1865 10.0333 10.9465 10.2733L6.55317 14.6667L5.8465 13.9667C5.6665 13.7867 5.55317 13.5333 5.55317 13.26L5.57317 13.0467L6.2065 10H1.99984C1.2665 10 0.666504 9.40001 0.666504 8.66668V7.33334C0.666504 7.16001 0.699837 7.00001 0.759837 6.84668L2.77317 2.14668C2.97317 1.66668 3.4465 1.33334 3.99984 1.33334H9.99984Z', fill: '#F44336', fillOpacity: '1' })) : (React.createElement("path", { d: 'M9.99984 1.33334H3.99984C3.4465 1.33334 2.97317 1.66668 2.77317 2.14668L0.759837 6.84668C0.699837 7.00001 0.666504 7.16001 0.666504 7.33334V8.66668C0.666504 9.40001 1.2665 10 1.99984 10H6.2065L5.57317 13.0467L5.55317 13.26C5.55317 13.5333 5.6665 13.7867 5.8465 13.9667L6.55317 14.6667L10.9465 10.2733C11.1865 10.0333 11.3332 9.70001 11.3332 9.33334V2.66668C11.3332 1.93334 10.7332 1.33334 9.99984 1.33334ZM9.99984 9.33334L7.1065 12.2267L7.99984 8.66668H1.99984V7.33334L3.99984 2.66668H9.99984V9.33334ZM12.6665 1.33334H15.3332V9.33334H12.6665V1.33334Z', fill: '#0F1324', fillOpacity: '0.6' }))));
|
|
9
|
+
return (React.createElement("svg", { width: '16', height: '16', viewBox: '0 0 16 16', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' }, isSelected ? (React.createElement("path", { d: 'M12.6665 9.33334H15.3332V1.33334H12.6665V9.33334ZM9.99984 1.33334C10.7332 1.33334 11.3332 1.93334 11.3332 2.66668V9.33334C11.3332 9.70001 11.1865 10.0333 10.9465 10.2733L6.55317 14.6667L5.8465 13.9667C5.6665 13.7867 5.55317 13.5333 5.55317 13.26L5.57317 13.0467L6.2065 10H1.99984C1.2665 10 0.666504 9.40001 0.666504 8.66668V7.33334C0.666504 7.16001 0.699837 7.00001 0.759837 6.84668L2.77317 2.14668C2.97317 1.66668 3.4465 1.33334 3.99984 1.33334H9.99984Z', fill: '#F44336', fillOpacity: '1' })) : (React.createElement("path", { d: 'M9.99984 1.33334H3.99984C3.4465 1.33334 2.97317 1.66668 2.77317 2.14668L0.759837 6.84668C0.699837 7.00001 0.666504 7.16001 0.666504 7.33334V8.66668C0.666504 9.40001 1.2665 10 1.99984 10H6.2065L5.57317 13.0467L5.55317 13.26C5.55317 13.5333 5.6665 13.7867 5.8465 13.9667L6.55317 14.6667L10.9465 10.2733C11.1865 10.0333 11.3332 9.70001 11.3332 9.33334V2.66668C11.3332 1.93334 10.7332 1.33334 9.99984 1.33334ZM9.99984 9.33334L7.1065 12.2267L7.99984 8.66668H1.99984V7.33334L3.99984 2.66668H9.99984V9.33334ZM12.6665 1.33334H15.3332V9.33334H12.6665V1.33334Z', fill: '#0F1324', fillOpacity: '0.6' }))));
|
|
10
10
|
}
|
|
11
11
|
export default function Feedback({ translations, onFeedback, componentOverride }) {
|
|
12
12
|
const [feedback, setFeedback] = useState(null);
|
|
@@ -17,8 +17,8 @@ export default function Feedback({ translations, onFeedback, componentOverride }
|
|
|
17
17
|
return (React.createElement(RenderPropsWrapper, { props: { translations, onFeedback }, override: componentOverride?.reactNode },
|
|
18
18
|
React.createElement("div", { className: 'cio-pia-feedback-container' },
|
|
19
19
|
React.createElement("p", { className: 'cio-pia-feedback-text' }, translate('Is this answer useful?', translations)),
|
|
20
|
-
React.createElement("button", { type: 'button', className: 'cio-pia-feedback-button', "aria-label": 'thumbs up', onClick: () => handleFeedback(FeedbackType.UP) },
|
|
20
|
+
React.createElement("button", { type: 'button', className: 'cio-pia-feedback-button', "aria-label": translate('thumbs up', translations), "aria-pressed": feedback === FeedbackType.UP, onClick: () => handleFeedback(FeedbackType.UP) },
|
|
21
21
|
React.createElement(ThumbsUpIcon, { isSelected: feedback === FeedbackType.UP })),
|
|
22
|
-
React.createElement("button", { type: 'button', className: 'cio-pia-feedback-button', "aria-label": 'thumbs down', onClick: () => handleFeedback(FeedbackType.DOWN) },
|
|
22
|
+
React.createElement("button", { type: 'button', className: 'cio-pia-feedback-button', "aria-label": translate('thumbs down', translations), "aria-pressed": feedback === FeedbackType.DOWN, onClick: () => handleFeedback(FeedbackType.DOWN) },
|
|
23
23
|
React.createElement(ThumbsDownIcon, { isSelected: feedback === FeedbackType.DOWN })))));
|
|
24
24
|
}
|
|
@@ -42,6 +42,7 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
|
|
|
42
42
|
const resolvedPlaceholder = translations?.[placeholderKey] !== undefined
|
|
43
43
|
? translate(placeholderKey, translations)
|
|
44
44
|
: (placeholder ?? translate(placeholderKey));
|
|
45
|
+
const accessibleName = resolvedPlaceholder || translate('Your question', translations);
|
|
45
46
|
return (React.createElement(RenderPropsWrapper, { props: {
|
|
46
47
|
disabled,
|
|
47
48
|
placeholder: resolvedPlaceholder,
|
|
@@ -52,7 +53,7 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
|
|
|
52
53
|
}, override: componentOverride?.reactNode },
|
|
53
54
|
React.createElement(React.Fragment, null,
|
|
54
55
|
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
|
+
React.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, "aria-label": accessibleName, 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
57
|
showSendButton && (React.createElement("button", { type: 'button', onClick: () => handleSubmit(value), className: 'cio-pia-send-button', disabled: disabled },
|
|
57
58
|
translate('Send', translations),
|
|
58
59
|
React.createElement(SendIcon, null)))),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components';
|
|
3
|
-
|
|
3
|
+
// Decorative: loading is announced by the always-mounted status region.
|
|
4
|
+
const skeleton = (React.createElement("div", { className: 'cio-pia-loading-skeleton', "data-testid": 'loading-skeleton', "aria-hidden": 'true' },
|
|
4
5
|
React.createElement("div", { className: 'skeleton-bar' }),
|
|
5
6
|
React.createElement("div", { className: 'skeleton-bar' }),
|
|
6
7
|
React.createElement("div", { className: 'skeleton-bar skeleton-short' })));
|
|
@@ -4,13 +4,17 @@ import { translate } from '../../utils/translate';
|
|
|
4
4
|
import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/SuggestedQuestionsContainer';
|
|
5
5
|
const OVERFLOW_HIDDEN_CLASS = 'cio-pia-modal-open';
|
|
6
6
|
function CloseIcon() {
|
|
7
|
-
return (React.createElement("svg", { width: '20', height: '20', viewBox: '0 0 20 20', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
|
|
7
|
+
return (React.createElement("svg", { width: '20', height: '20', viewBox: '0 0 20 20', fill: 'none', xmlns: 'http://www.w3.org/2000/svg', "aria-hidden": 'true', focusable: 'false' },
|
|
8
8
|
React.createElement("path", { d: 'M15.8334 5.34166L14.6584 4.16666L10.0001 8.82499L5.34175 4.16666L4.16675 5.34166L8.82508 9.99999L4.16675 14.6583L5.34175 15.8333L10.0001 11.175L14.6584 15.8333L15.8334 14.6583L11.1751 9.99999L15.8334 5.34166Z', fill: 'currentColor' })));
|
|
9
9
|
}
|
|
10
10
|
export default function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, isLoading, componentOverrides, translations, onInputFocus, onClose, children, }) {
|
|
11
11
|
const [isOpen, setIsOpen] = useState(false);
|
|
12
12
|
const dialogRef = useRef(null);
|
|
13
|
+
const triggerRef = useRef(null);
|
|
13
14
|
const openModal = useCallback(() => {
|
|
15
|
+
const active = document.activeElement;
|
|
16
|
+
const isRealTrigger = !!active && active !== document.body && active !== document.documentElement;
|
|
17
|
+
triggerRef.current = isRealTrigger ? active : null;
|
|
14
18
|
setIsOpen(true);
|
|
15
19
|
}, []);
|
|
16
20
|
const closeModal = useCallback(() => {
|
|
@@ -25,14 +29,28 @@ export default function PiaModal({ initialQuestions, handleSubmitQuestion, handl
|
|
|
25
29
|
dialog.showModal();
|
|
26
30
|
document.body.classList.add(OVERFLOW_HIDDEN_CLASS);
|
|
27
31
|
}
|
|
28
|
-
else
|
|
29
|
-
dialog.
|
|
32
|
+
else {
|
|
33
|
+
if (dialog.open)
|
|
34
|
+
dialog.close();
|
|
30
35
|
document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
|
|
31
36
|
}
|
|
32
37
|
return () => {
|
|
33
38
|
document.body.classList.remove(OVERFLOW_HIDDEN_CLASS);
|
|
34
39
|
};
|
|
35
40
|
}, [isOpen]);
|
|
41
|
+
// Restore focus to the trigger. Re-runs on `isLoading`: the input trigger is
|
|
42
|
+
// disabled while a request is in flight and `focus()` would no-op.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (isOpen)
|
|
45
|
+
return;
|
|
46
|
+
const trigger = triggerRef.current;
|
|
47
|
+
if (!trigger || !trigger.isConnected)
|
|
48
|
+
return;
|
|
49
|
+
if (trigger.disabled)
|
|
50
|
+
return;
|
|
51
|
+
trigger.focus();
|
|
52
|
+
triggerRef.current = null;
|
|
53
|
+
}, [isOpen, isLoading]);
|
|
36
54
|
useEffect(() => {
|
|
37
55
|
const dialog = dialogRef.current;
|
|
38
56
|
if (!dialog)
|
|
@@ -69,7 +87,7 @@ export default function PiaModal({ initialQuestions, handleSubmitQuestion, handl
|
|
|
69
87
|
React.createElement("div", { className: 'cio-pia-modal-content' },
|
|
70
88
|
React.createElement("div", { className: 'cio-pia-modal-header' },
|
|
71
89
|
React.createElement("p", { className: 'cio-pia-title', id: 'cio-pia-modal-title' }, translate('Ask about this product', translations)),
|
|
72
|
-
React.createElement("button", { type: 'button', className: 'cio-pia-modal-close-button', "aria-label": 'Close', onClick: closeModal },
|
|
90
|
+
React.createElement("button", { type: 'button', className: 'cio-pia-modal-close-button', "aria-label": translate('Close', translations), onClick: closeModal },
|
|
73
91
|
React.createElement(CloseIcon, null))),
|
|
74
92
|
React.createElement("div", { className: 'cio-pia-modal-body' }, children)))));
|
|
75
93
|
}
|