@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,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,106 @@
|
|
|
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, answerParameters, 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
|
+
answerParameters,
|
|
32
|
+
parameters,
|
|
33
|
+
formatImageUrl: formatters?.formatImageUrl,
|
|
34
|
+
});
|
|
35
|
+
const tracking = useTracking({
|
|
36
|
+
cioClient: pia.cioClient,
|
|
37
|
+
itemId,
|
|
38
|
+
itemName,
|
|
39
|
+
variationId,
|
|
40
|
+
threadId: pia.threadId,
|
|
41
|
+
});
|
|
42
|
+
const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, handleFeedback, resetState, } = useConversation({ pia, itemId, isConversation, callbacks, tracking });
|
|
43
|
+
const { containerRef: viewportContainerRef } = useViewportTracking({
|
|
44
|
+
tracking,
|
|
45
|
+
questions: displayedQuestions,
|
|
46
|
+
viewThreshold: trackingConfigs?.viewThreshold,
|
|
47
|
+
});
|
|
48
|
+
const { containerRef: callbackContainerRef } = useViewportCallbacks({ callbacks, context });
|
|
49
|
+
const containerRef = useCallback((node) => {
|
|
50
|
+
viewportContainerRef(node);
|
|
51
|
+
callbackContainerRef(node);
|
|
52
|
+
}, [viewportContainerRef, callbackContainerRef]);
|
|
53
|
+
const qnaResultId = pia.answers.data?.qna_result_id;
|
|
54
|
+
// The text bars stand in for the answer alone, so they track the answer request only. The question
|
|
55
|
+
// row keeps the combined `isLoading`: the follow-up questions that refill it arrive with the
|
|
56
|
+
// answer, so the row is waiting on either request.
|
|
57
|
+
const isAnswerLoading = pia.answers.isLoading;
|
|
58
|
+
const renderProps = {
|
|
59
|
+
items: currentItems,
|
|
60
|
+
isLoading,
|
|
61
|
+
error,
|
|
62
|
+
currentAnswer,
|
|
63
|
+
currentQuestion,
|
|
64
|
+
displayedQuestions,
|
|
65
|
+
handleSubmitQuestion,
|
|
66
|
+
conversationHistory,
|
|
67
|
+
};
|
|
68
|
+
const conversationHistoryProps = {
|
|
69
|
+
conversationHistory,
|
|
70
|
+
isLoading,
|
|
71
|
+
error,
|
|
72
|
+
currentItems,
|
|
73
|
+
showFeedback,
|
|
74
|
+
showPreviousItems,
|
|
75
|
+
learnMoreUrl,
|
|
76
|
+
disclaimerPosition,
|
|
77
|
+
translations,
|
|
78
|
+
callbacks,
|
|
79
|
+
componentOverrides,
|
|
80
|
+
displayedQuestions,
|
|
81
|
+
handleSubmitQuestion,
|
|
82
|
+
handleQuestionClick,
|
|
83
|
+
handleFeedback,
|
|
84
|
+
onResultClick: tracking.trackResultClick,
|
|
85
|
+
qnaResultId,
|
|
86
|
+
containerRef,
|
|
87
|
+
onInputFocus: handleInputFocus,
|
|
88
|
+
priceCurrency,
|
|
89
|
+
};
|
|
90
|
+
if (type === 'modal') {
|
|
91
|
+
return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
|
|
92
|
+
React.createElement(PiaConversation, { ...conversationHistoryProps })));
|
|
93
|
+
}
|
|
94
|
+
if (isConversation)
|
|
95
|
+
return React.createElement(PiaConversation, { ...conversationHistoryProps });
|
|
96
|
+
// Default inline mode
|
|
97
|
+
return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
|
|
98
|
+
React.createElement(RenderPropsWrapper, { props: renderProps, override: children || componentOverrides?.reactNode },
|
|
99
|
+
React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
|
|
100
|
+
React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations, componentOverride: componentOverrides?.input }),
|
|
101
|
+
isAnswerLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
|
|
102
|
+
!isAnswerLoading && error && React.createElement(ErrorBlock, { message: error?.message || 'Unexpected error' }),
|
|
103
|
+
!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 })),
|
|
104
|
+
isLoading && React.createElement(SuggestedQuestionsSkeleton, null),
|
|
105
|
+
!isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })))));
|
|
106
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 }) {
|
|
@@ -55,12 +55,10 @@ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
|
|
58
|
-
const wrapperRef = useRef(null);
|
|
59
58
|
const { onAddToCart } = callbacks || {};
|
|
60
59
|
const addToCartHandler = useCallback((event, product) => {
|
|
61
60
|
onAddToCart?.(product, event);
|
|
62
61
|
}, [onAddToCart]);
|
|
63
|
-
// Determine to use user-defined click handler or default behavior
|
|
64
62
|
const productClickHandler = useCallback((item) => {
|
|
65
63
|
const position = items.findIndex((i) => i.id === item.id);
|
|
66
64
|
if (onResultClick && question) {
|
|
@@ -74,47 +72,62 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
|
|
|
74
72
|
window.open(item.url, '_blank', 'noopener,noreferrer');
|
|
75
73
|
}
|
|
76
74
|
}, [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
75
|
const priceSectionOverride = useMemo(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
|
|
76
|
+
const clickHandlerProps = useMemo(() => ({
|
|
77
|
+
onProductClick: productClickHandler,
|
|
78
|
+
onAddToCart: onAddToCart ? addToCartHandler : undefined,
|
|
79
|
+
}), [productClickHandler, onAddToCart, addToCartHandler]);
|
|
94
80
|
const mergedOverrides = useMemo(() => {
|
|
95
81
|
const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
|
|
82
|
+
// Override priority: carousel.reactNode → item.reactNode → item.productCard.reactNode → default ProductCard.
|
|
83
|
+
// The casts are required because the override signatures don't declare these extra props,
|
|
84
|
+
// but consumers receive them as part of the render-props contract documented in CioPiaProps.
|
|
85
|
+
const carouselOverride = baseOverrides.reactNode;
|
|
86
|
+
if (typeof carouselOverride === 'function') {
|
|
87
|
+
return {
|
|
88
|
+
...baseOverrides,
|
|
89
|
+
reactNode: (props) => carouselOverride({ ...props, ...clickHandlerProps }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
96
92
|
const cardOverrides = baseOverrides.item?.productCard;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
const itemOverride = baseOverrides.item?.reactNode;
|
|
94
|
+
if (typeof itemOverride === 'function') {
|
|
95
|
+
return {
|
|
96
|
+
...baseOverrides,
|
|
97
|
+
item: {
|
|
98
|
+
...baseOverrides.item,
|
|
99
|
+
reactNode: (props) => itemOverride({ ...props, ...clickHandlerProps }),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (typeof cardOverrides?.reactNode === 'function') {
|
|
104
|
+
const originalReactNode = cardOverrides.reactNode;
|
|
105
|
+
return {
|
|
106
|
+
...baseOverrides,
|
|
107
|
+
item: {
|
|
108
|
+
...baseOverrides.item,
|
|
109
|
+
productCard: {
|
|
110
|
+
...cardOverrides,
|
|
111
|
+
reactNode: (props) => originalReactNode({ ...props, ...clickHandlerProps }),
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
101
115
|
}
|
|
102
|
-
|
|
116
|
+
// Default: render ProductCard with handlers directly
|
|
117
|
+
const addToCartText = onAddToCart ? translate('Add to Cart', translations) : undefined;
|
|
103
118
|
return {
|
|
104
119
|
...baseOverrides,
|
|
105
120
|
item: {
|
|
106
121
|
...baseOverrides.item,
|
|
107
122
|
productCard: {
|
|
108
123
|
...cardOverrides,
|
|
109
|
-
reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart:
|
|
124
|
+
reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: clickHandlerProps.onAddToCart, onProductClick: clickHandlerProps.onProductClick, componentOverrides: cardOverrides })),
|
|
110
125
|
},
|
|
111
126
|
},
|
|
112
127
|
};
|
|
113
|
-
}, [componentOverrides, priceSectionOverride, onAddToCart, translations
|
|
114
|
-
// If there are no items, do not render the carousel
|
|
128
|
+
}, [componentOverrides, priceSectionOverride, clickHandlerProps, onAddToCart, translations]);
|
|
115
129
|
if (items.length === 0) {
|
|
116
130
|
return null;
|
|
117
131
|
}
|
|
118
|
-
return
|
|
119
|
-
React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides })));
|
|
132
|
+
return React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides });
|
|
120
133
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { RenderPropsWrapper } from '@constructor-io/constructorio-ui-components';
|
|
3
|
+
import PiaCustomCarousel from '../CioPia/PiaCustomCarousel';
|
|
4
|
+
import RecsPodRefinement from './RecsPodRefinement';
|
|
5
|
+
import RecsCarouselSkeleton from './RecsCarouselSkeleton';
|
|
6
|
+
import usePiaClient from '../../hooks/usePiaClient';
|
|
7
|
+
import useRecsPod from '../../hooks/useRecsPod';
|
|
8
|
+
import useViewportCallbacks from '../../hooks/useViewportCallbacks';
|
|
9
|
+
import { cx } from '../../utils/classNames';
|
|
10
|
+
/**
|
|
11
|
+
* A pod of AI-curated product suggestions: a personalized title, a row of products, and short
|
|
12
|
+
* options for narrowing them down. It asks no questions and makes no Q&A requests, so a retailer
|
|
13
|
+
* can use it on its own.
|
|
14
|
+
*
|
|
15
|
+
* Renders nothing at all once a request has settled with no products, so whatever the retailer
|
|
16
|
+
* already had in that slot shows through instead of an empty shell. That is the default: a caller
|
|
17
|
+
* that supplied its own root — `children` or `componentOverrides.reactNode` — still gets it in that
|
|
18
|
+
* state, with `error` available, and decides what belongs there.
|
|
19
|
+
*/
|
|
20
|
+
export default function CioPiaRecs(props) {
|
|
21
|
+
const { apiKey, itemId, threadId, variationId, cioClient, componentOverrides, callbacks, formatters, productCardProps, children, translations, recsPodParameters, } = props;
|
|
22
|
+
const { priceCurrency } = productCardProps || {};
|
|
23
|
+
const { showInput = true, numResults } = recsPodParameters || {};
|
|
24
|
+
const { answer: answerOverride, carousel: carouselOverride, loading: loadingOverride, reactNode: rootOverride, } = componentOverrides || {};
|
|
25
|
+
const { cioClient: client, threadId: resolvedThreadId } = usePiaClient({
|
|
26
|
+
apiKey,
|
|
27
|
+
threadId,
|
|
28
|
+
cioClient,
|
|
29
|
+
});
|
|
30
|
+
const { title, items, refinement, isLoading, error, inputError, lastShopperInput, refine } = useRecsPod({
|
|
31
|
+
itemId,
|
|
32
|
+
variationId,
|
|
33
|
+
threadId: resolvedThreadId,
|
|
34
|
+
cioClient: client,
|
|
35
|
+
parameters: recsPodParameters,
|
|
36
|
+
formatImageUrl: formatters?.formatImageUrl,
|
|
37
|
+
translations,
|
|
38
|
+
});
|
|
39
|
+
const context = useMemo(() => ({ itemId, threadId: resolvedThreadId }), [itemId, resolvedThreadId]);
|
|
40
|
+
const { containerRef } = useViewportCallbacks({ callbacks, context });
|
|
41
|
+
const handleInputFocus = useCallback(() => callbacks?.onFocus?.(context), [callbacks, context]);
|
|
42
|
+
const refineFromOption = useCallback((text) => refine(text, 'option'), [refine]);
|
|
43
|
+
const refineFromInput = useCallback((text) => refine(text, 'input'), [refine]);
|
|
44
|
+
const renderProps = {
|
|
45
|
+
items,
|
|
46
|
+
isLoading,
|
|
47
|
+
error,
|
|
48
|
+
currentAnswer: title,
|
|
49
|
+
currentQuestion: lastShopperInput,
|
|
50
|
+
displayedQuestions: (refinement?.options || []).map((value) => ({ value })),
|
|
51
|
+
// A caller's own root gets the free-text behavior: whatever it submits is the shopper's, so a
|
|
52
|
+
// rejection belongs under their input.
|
|
53
|
+
handleSubmitQuestion: refineFromInput,
|
|
54
|
+
conversationHistory: [],
|
|
55
|
+
};
|
|
56
|
+
// Nothing of ours to show. Rendering no markup at all is the default, so whatever the retailer
|
|
57
|
+
// already had in that slot shows through instead of an empty shell. TODO: an interim, not a
|
|
58
|
+
// settled decision — the empty-state design and the shape of a hard failure are both still open.
|
|
59
|
+
// A caller who supplied its own root still gets it, with `error` in hand, so a consumer can put
|
|
60
|
+
// its own message here.
|
|
61
|
+
if (!isLoading && !items && !children && !rootOverride)
|
|
62
|
+
return null;
|
|
63
|
+
return (React.createElement("div", { ref: containerRef, className: cx('cio-pia-container', 'cio-pia-recs-pod', isLoading && 'cio-pia-recs-pod--loading'), "data-testid": 'cio-pia-recs-pod' },
|
|
64
|
+
React.createElement(RenderPropsWrapper, { props: renderProps, override: children || rootOverride },
|
|
65
|
+
title && (React.createElement(RenderPropsWrapper, { props: { text: title }, override: answerOverride?.reactNode },
|
|
66
|
+
React.createElement("h3", { key: title, className: 'cio-pia-recs-pod__title' }, title))),
|
|
67
|
+
React.createElement("div", { className: 'cio-pia-recs-pod__products', "data-testid": 'cio-pia-recs-pod-products' }, isLoading || !items ? (React.createElement(RecsCarouselSkeleton, { count: numResults ?? items?.length, componentOverride: loadingOverride })) : (React.createElement(PiaCustomCarousel, { items: items, componentOverrides: carouselOverride, callbacks: callbacks, translations: translations, priceCurrency: priceCurrency }))),
|
|
68
|
+
React.createElement(RecsPodRefinement, { refinement: refinement, isLoading: isLoading, inputError: inputError, showInput: showInput, translations: translations, componentOverrides: componentOverrides, onRefine: refineFromOption, onSubmit: refineFromInput, onInputFocus: handleInputFocus }))));
|
|
69
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
.cio-pia-recs-pod {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The title is the one part of the pod that changes on a refinement, so it fades in rather than
|
|
10
|
+
* snapping. `CioPiaRecs` keys the node on the title, which remounts it and replays this.
|
|
11
|
+
*
|
|
12
|
+
* This is a fade-in on the arriving title, not a cross-fade: React removes the outgoing text
|
|
13
|
+
* synchronously, so there is nothing left on screen to fade out.
|
|
14
|
+
*/
|
|
15
|
+
.cio-pia-recs-pod__title {
|
|
16
|
+
min-height: 20px;
|
|
17
|
+
margin: 0;
|
|
18
|
+
font-size: 16px;
|
|
19
|
+
font-weight: 500;
|
|
20
|
+
color: #14151a;
|
|
21
|
+
text-align: start;
|
|
22
|
+
animation: recs-pod-title-fade-in 200ms ease-in;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@keyframes recs-pod-title-fade-in {
|
|
26
|
+
from {
|
|
27
|
+
opacity: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
to {
|
|
31
|
+
opacity: 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Holds the products, and the placeholders that stand in for them while a request is in flight */
|
|
36
|
+
.cio-pia-recs-pod__products {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Options and the free-text box share one row, wrapping on narrow screens */
|
|
42
|
+
.cio-pia-recs-pod__refinement {
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.cio-pia-recs-pod__refinement-label {
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
font-weight: 500;
|
|
52
|
+
color: #0f1324;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.cio-pia-recs-pod__input {
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
flex: 1 1 200px;
|
|
59
|
+
min-width: 200px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Nothing in the row is clickable while its contents are being replaced */
|
|
63
|
+
.cio-pia-recs-pod--loading .cio-pia-recs-pod__refinement {
|
|
64
|
+
pointer-events: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Placeholders, shaped like the real content so the page does not jump when data lands */
|
|
68
|
+
.cio-pia-recs-skeleton {
|
|
69
|
+
display: flex;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
width: 100%;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* This one holds the real carousel, arrows and all, so nothing in it is clickable while its
|
|
76
|
+
* contents are being replaced.
|
|
77
|
+
*/
|
|
78
|
+
.cio-pia-recs-skeleton--carousel {
|
|
79
|
+
flex: 1 1 auto;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* The only two measurements taken from the package, both from the card's own image
|
|
85
|
+
* (`min-h-[224px] rounded-2xl`). The image is what gives a card its height, so these are what stop
|
|
86
|
+
* the refinement row moving when the products land. Everything else about the card - its
|
|
87
|
+
* background, radius, shadow, padding, and how many fit across - comes from the carousel itself.
|
|
88
|
+
*/
|
|
89
|
+
.cio-pia-recs-skeleton__image {
|
|
90
|
+
min-height: 224px;
|
|
91
|
+
border-radius: 16px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Bar heights are the line boxes of the text they stand in for, so the card totals the same */
|
|
95
|
+
.cio-pia-recs-skeleton__price {
|
|
96
|
+
width: 40%;
|
|
97
|
+
height: 28px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.cio-pia-recs-skeleton__title {
|
|
101
|
+
width: 80%;
|
|
102
|
+
height: 24px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.cio-pia-recs-skeleton__desc {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
gap: 4px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.cio-pia-recs-skeleton__desc .skeleton-bar {
|
|
112
|
+
height: 18px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Short last line, the way a wrapped sentence ends */
|
|
116
|
+
.cio-pia-recs-skeleton__desc .skeleton-bar:last-child {
|
|
117
|
+
width: 60%;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.cio-pia-recs-skeleton--options {
|
|
121
|
+
width: auto;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cio-pia-recs-skeleton--options .cio-pia-recs-skeleton__block {
|
|
125
|
+
height: 26px;
|
|
126
|
+
width: 96px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@media (prefers-reduced-motion: reduce) {
|
|
130
|
+
.cio-pia-recs-skeleton__block,
|
|
131
|
+
.cio-pia-recs-skeleton__image,
|
|
132
|
+
.cio-pia-recs-skeleton__price,
|
|
133
|
+
.cio-pia-recs-skeleton__title,
|
|
134
|
+
.cio-pia-recs-skeleton__desc .skeleton-bar,
|
|
135
|
+
.cio-pia-recs-pod__title {
|
|
136
|
+
animation: none;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Carousel, RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components';
|
|
3
|
+
/** The widest band the carousel lays out. Narrower ones need no count: its viewport clips them. */
|
|
4
|
+
const DEFAULT_COUNT = 6;
|
|
5
|
+
/**
|
|
6
|
+
* Grey bars standing in for a card's own sections. The card around them - white background,
|
|
7
|
+
* rounded corners, shadow, padding - and the row it sits in are the real ones, so nothing here has
|
|
8
|
+
* to know their measurements.
|
|
9
|
+
*
|
|
10
|
+
* `RenderPropsWrapper` draws an override in place of a section rather than alongside it, so these
|
|
11
|
+
* appear even though the placeholder carries no image, price or name of its own.
|
|
12
|
+
*
|
|
13
|
+
* A caller's own `productCard` overrides are deliberately not merged in, unlike the real row in
|
|
14
|
+
* `PiaCustomCarousel`: the placeholders below are empty objects, so a caller's section renderer
|
|
15
|
+
* would be handed a product with every field `undefined`.
|
|
16
|
+
*/
|
|
17
|
+
const CARD_SKELETON = {
|
|
18
|
+
item: {
|
|
19
|
+
productCard: {
|
|
20
|
+
image: {
|
|
21
|
+
reactNode: () => (React.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__image', "data-testid": 'cio-pia-recs-skeleton-card' })),
|
|
22
|
+
},
|
|
23
|
+
content: {
|
|
24
|
+
price: { reactNode: () => React.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__price' }) },
|
|
25
|
+
title: { reactNode: () => React.createElement("div", { className: 'skeleton-bar cio-pia-recs-skeleton__title' }) },
|
|
26
|
+
description: {
|
|
27
|
+
// Two lines, not one. The card clamps its description at three lines and stretches every
|
|
28
|
+
// card in a row to the tallest of them, so a single line lands short of where real
|
|
29
|
+
// products settle.
|
|
30
|
+
reactNode: () => (React.createElement("div", { className: 'cio-pia-recs-skeleton__desc' },
|
|
31
|
+
React.createElement("div", { className: 'skeleton-bar' }),
|
|
32
|
+
React.createElement("div", { className: 'skeleton-bar' }))),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Placeholders exist only to give the carousel something to lay out. They deliberately carry no
|
|
40
|
+
* `id` and no `name`: `ProductCard` spreads those into `data-cnstrc-item-*`, and React omits an
|
|
41
|
+
* attribute whose value is `undefined`, so a loading pod emits no product data attributes at all.
|
|
42
|
+
*/
|
|
43
|
+
const placeholderItems = (count) => Array.from({ length: count }, () => ({}));
|
|
44
|
+
/**
|
|
45
|
+
* The product row, greyed out. It uses the real carousel rather than a set of measurements copied
|
|
46
|
+
* out of the package, so its padding, its gaps, how many cards it fits at the current width and its
|
|
47
|
+
* arrows are all right by construction. Matching the real heights is what keeps the page from
|
|
48
|
+
* jumping when the data lands.
|
|
49
|
+
*/
|
|
50
|
+
export default function RecsCarouselSkeleton({ count, componentOverride, }) {
|
|
51
|
+
const skeleton = (React.createElement("div", { className: 'cio-pia-recs-skeleton cio-pia-recs-skeleton--carousel', "data-testid": 'cio-pia-recs-skeleton-carousel', "aria-hidden": 'true' },
|
|
52
|
+
React.createElement(Carousel, { items: placeholderItems(count ?? DEFAULT_COUNT), componentOverrides: CARD_SKELETON })));
|
|
53
|
+
return (React.createElement(RenderPropsWrapper, { props: { skeleton }, override: componentOverride?.reactNode }, skeleton));
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
const DEFAULT_COUNT = 3;
|
|
3
|
+
/**
|
|
4
|
+
* The refinement options, greyed out. Matching the real height is what keeps the row from
|
|
5
|
+
* collapsing and shifting the page when the options land.
|
|
6
|
+
*
|
|
7
|
+
* Takes no override, the same way `SuggestedQuestionsSkeleton` takes none: `componentOverrides.
|
|
8
|
+
* loading` stands in for the band above this one, and a caller who replaced it keeps these pills
|
|
9
|
+
* underneath, exactly as a caller who replaces the answer skeleton in question-and-answer keeps the
|
|
10
|
+
* question pills under theirs.
|
|
11
|
+
*/
|
|
12
|
+
export default function RecsOptionsSkeleton({ count }) {
|
|
13
|
+
return (React.createElement("div", { className: 'cio-pia-recs-skeleton cio-pia-recs-skeleton--options', "data-testid": 'cio-pia-recs-skeleton-options', "aria-hidden": 'true' }, Array.from({ length: count ?? DEFAULT_COUNT }, (_, index) => (React.createElement("div", { key: index, className: 'skeleton-bar cio-pia-recs-skeleton__block' })))));
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Input from '../Input/Input';
|
|
3
|
+
import SuggestedQuestion from '../SuggestedQuestion/SuggestedQuestion';
|
|
4
|
+
import { SparklesIcon } from '../icons';
|
|
5
|
+
import RecsOptionsSkeleton from './RecsOptionsSkeleton';
|
|
6
|
+
import { resolveRefinementQuestion } from '../../utils/recs';
|
|
7
|
+
import { RECS_INPUT_PLACEHOLDER } from '../../constants';
|
|
8
|
+
/**
|
|
9
|
+
* The row under the products: a prompt line, the short options the API suggested, and a box for
|
|
10
|
+
* anything it did not think of. The prompt line is held still while the rest reloads, so the row
|
|
11
|
+
* never changes height under the shopper's cursor.
|
|
12
|
+
*/
|
|
13
|
+
export default function RecsPodRefinement({ refinement, isLoading, inputError, showInput, translations, componentOverrides, onRefine, onSubmit, onInputFocus, }) {
|
|
14
|
+
const options = refinement?.options || [];
|
|
15
|
+
return (React.createElement("div", { className: 'cio-pia-recs-pod__refinement' },
|
|
16
|
+
React.createElement("span", { className: 'cio-pia-recs-pod__refinement-label' }, resolveRefinementQuestion(refinement, translations)),
|
|
17
|
+
isLoading ? (React.createElement(RecsOptionsSkeleton, { count: options.length || undefined })) : (options.map((option) => (React.createElement(SuggestedQuestion, { key: option, question: option, icon: React.createElement(SparklesIcon, null), onClick: () => onRefine(option) })))),
|
|
18
|
+
showInput && (React.createElement("div", { className: 'cio-pia-recs-pod__input' },
|
|
19
|
+
React.createElement(Input, { onSubmit: onSubmit, onFocus: onInputFocus, disabled: isLoading, error: inputError ?? undefined, placeholderKey: RECS_INPUT_PLACEHOLDER,
|
|
20
|
+
// The pod's box sits in a row beside the options and submits on Enter. Q&A keeps
|
|
21
|
+
// its button; this one has none in the mocks.
|
|
22
|
+
showSendButton: false, translations: translations, componentOverride: componentOverrides?.input })))));
|
|
23
|
+
}
|