@constructor-io/constructorio-ui-pia 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/dist/constructorio-ui-pia-bundled.js +17 -17
  2. package/lib/cjs/components/CioPia/CioPia.js +4 -2
  3. package/lib/cjs/components/ConversationHistory/ConversationHistory.js +18 -14
  4. package/lib/cjs/components/ConversationHistory/ConversationHistoryStyles.css +7 -0
  5. package/lib/cjs/components/PiaConversation/PiaConversation.js +2 -2
  6. package/lib/cjs/hooks/useAnswerResults.js +5 -5
  7. package/lib/cjs/hooks/useCioPia.js +2 -1
  8. package/lib/cjs/hooks/useConversation.js +2 -2
  9. package/lib/cjs/utils/transformers.js +12 -4
  10. package/lib/cjs/version.js +1 -1
  11. package/lib/mjs/components/CioPia/CioPia.js +4 -2
  12. package/lib/mjs/components/ConversationHistory/ConversationHistory.js +18 -14
  13. package/lib/mjs/components/ConversationHistory/ConversationHistoryStyles.css +7 -0
  14. package/lib/mjs/components/PiaConversation/PiaConversation.js +2 -2
  15. package/lib/mjs/hooks/useAnswerResults.js +5 -5
  16. package/lib/mjs/hooks/useCioPia.js +2 -1
  17. package/lib/mjs/hooks/useConversation.js +2 -1
  18. package/lib/mjs/utils/transformers.js +12 -2
  19. package/lib/mjs/version.js +1 -1
  20. package/lib/types/components/CioPia/CioPia.d.ts +3 -1
  21. package/lib/types/components/ConversationHistory/ConversationHistory.d.ts +12 -1
  22. package/lib/types/components/PiaConversation/PiaConversation.d.ts +1 -1
  23. package/lib/types/hooks/useAnswerResults.d.ts +3 -2
  24. package/lib/types/hooks/useCioPia.d.ts +3 -1
  25. package/lib/types/types.d.ts +11 -0
  26. package/lib/types/utils/transformers.d.ts +2 -2
  27. package/lib/types/version.d.ts +1 -1
  28. package/package.json +1 -1
@@ -17,8 +17,8 @@ const PiaCustomCarousel_1 = tslib_1.__importDefault(require("./PiaCustomCarousel
17
17
  const PiaModal_1 = tslib_1.__importDefault(require("../PiaConversation/PiaModal"));
18
18
  const PiaConversation_1 = tslib_1.__importDefault(require("../PiaConversation/PiaConversation"));
19
19
  function CioPia(props) {
20
- const { apiKey, itemId, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, children, translations, suggestedQuestionsParameters, } = props;
21
- const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline' } = displayConfigs || {};
20
+ const { apiKey, itemId, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, children, translations, suggestedQuestionsParameters, } = props;
21
+ const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, } = displayConfigs || {};
22
22
  const isConversation = mode === 'conversation' || type === 'modal';
23
23
  const pia = (0, useCioPia_1.default)({
24
24
  apiKey,
@@ -27,6 +27,7 @@ function CioPia(props) {
27
27
  variationId,
28
28
  cioClient,
29
29
  suggestedQuestionsParameters,
30
+ formatImageUrl: formatters === null || formatters === void 0 ? void 0 : formatters.formatImageUrl,
30
31
  });
31
32
  const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, handleSubmitQuestion, resetState, } = (0, useConversation_1.default)({ pia, itemId, isConversation });
32
33
  const renderProps = {
@@ -45,6 +46,7 @@ function CioPia(props) {
45
46
  error,
46
47
  currentItems,
47
48
  showFeedback,
49
+ showPreviousItems,
48
50
  learnMoreUrl,
49
51
  translations,
50
52
  callbacks,
@@ -8,7 +8,7 @@ const Disclaimer_1 = tslib_1.__importDefault(require("../CioPia/Disclaimer"));
8
8
  const ErrorBlock_1 = tslib_1.__importDefault(require("../Error/ErrorBlock"));
9
9
  const LoadingSkeleton_1 = tslib_1.__importDefault(require("../LoadingSkeleton/LoadingSkeleton"));
10
10
  const PiaCustomCarousel_1 = tslib_1.__importDefault(require("../CioPia/PiaCustomCarousel"));
11
- function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, }) {
11
+ function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems = true, learnMoreUrl, translations, callbacks, componentOverrides, }) {
12
12
  const scrollContainerRef = (0, react_1.useRef)(null);
13
13
  (0, react_1.useEffect)(() => {
14
14
  const container = scrollContainerRef.current;
@@ -19,18 +19,22 @@ function ConversationHistory({ conversationHistory, isLoading, error, currentIte
19
19
  });
20
20
  return () => cancelAnimationFrame(frameId);
21
21
  }, [conversationHistory, isLoading]);
22
- return (react_1.default.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-history', role: 'log', "aria-label": 'Conversation history' }, conversationHistory.map((entry, index) => {
23
- const isLast = index === conversationHistory.length - 1;
24
- return (react_1.default.createElement("div", { key: entry.id, className: 'cio-pia-conversation-entry' },
25
- react_1.default.createElement("div", { className: 'cio-pia-chat-question' }, entry.question),
26
- isLast && isLoading && (react_1.default.createElement("div", { className: 'cio-pia-conversation-loading', "aria-live": 'polite' },
27
- react_1.default.createElement(LoadingSkeleton_1.default, null))),
28
- isLast && !isLoading && error && (react_1.default.createElement(ErrorBlock_1.default, { message: error.message || 'Unexpected error' })),
29
- entry.answer && (react_1.default.createElement("div", { className: 'cio-pia-answer-container' },
30
- react_1.default.createElement(Answer_1.default, { text: entry.answer, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.answer }),
31
- isLast && currentItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: currentItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks })),
32
- isLast && showFeedback && (react_1.default.createElement(Feedback_1.default, { translations: translations, onFeedback: callbacks === null || callbacks === void 0 ? void 0 : callbacks.onFeedback, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.feedback })))),
33
- isLast && entry.answer && (react_1.default.createElement(Disclaimer_1.default, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.disclaimer }))));
34
- })));
22
+ return (react_1.default.createElement("div", { className: 'cio-pia-conversation-history' },
23
+ react_1.default.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-entries', role: 'log', "aria-label": 'Conversation history' }, conversationHistory.map((entry, index) => {
24
+ const isLast = index === conversationHistory.length - 1;
25
+ const previousEntryItems = showPreviousItems ? entry.items : null;
26
+ const latestEntryItems = currentItems !== undefined ? currentItems : entry.items;
27
+ const carouselItems = isLast ? latestEntryItems : previousEntryItems;
28
+ return (react_1.default.createElement("div", { key: entry.id, className: 'cio-pia-conversation-entry' },
29
+ react_1.default.createElement("div", { className: 'cio-pia-chat-question' }, entry.question),
30
+ isLast && isLoading && (react_1.default.createElement("div", { className: 'cio-pia-conversation-loading', "aria-live": 'polite' },
31
+ react_1.default.createElement(LoadingSkeleton_1.default, null))),
32
+ isLast && !isLoading && error && (react_1.default.createElement(ErrorBlock_1.default, { message: error.message || 'Unexpected error' })),
33
+ entry.answer && (react_1.default.createElement("div", { className: 'cio-pia-answer-container' },
34
+ react_1.default.createElement(Answer_1.default, { text: entry.answer, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.answer }),
35
+ carouselItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: carouselItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks })),
36
+ isLast && showFeedback && (react_1.default.createElement(Feedback_1.default, { translations: translations, onFeedback: callbacks === null || callbacks === void 0 ? void 0 : callbacks.onFeedback, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.feedback }))))));
37
+ })),
38
+ react_1.default.createElement(Disclaimer_1.default, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.disclaimer })));
35
39
  }
36
40
  exports.default = ConversationHistory;
@@ -1,5 +1,12 @@
1
1
  .cio-pia-conversation-history {
2
2
  --conversation-gap: 16px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ flex: 1;
6
+ min-height: 0;
7
+ }
8
+
9
+ .cio-pia-conversation-entries {
3
10
  display: flex;
4
11
  flex-direction: column;
5
12
  gap: var(--conversation-gap);
@@ -7,11 +7,11 @@ const SuggestedQuestionsContainer_1 = tslib_1.__importDefault(require("../Sugges
7
7
  const SuggestedQuestionsSkeleton_1 = tslib_1.__importDefault(require("../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton"));
8
8
  const translate_1 = require("../../utils/translate");
9
9
  const ConversationHistory_1 = tslib_1.__importDefault(require("../ConversationHistory/ConversationHistory"));
10
- function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }) {
10
+ function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }) {
11
11
  const hasHistory = conversationHistory.length > 0;
12
12
  return (react_1.default.createElement("div", { className: 'cio-pia-container cio-pia-conversation', "data-testid": 'cio-pia-container' },
13
13
  !hasHistory && (react_1.default.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, (0, translate_1.translate)('Any questions about this product?', translations))),
14
- react_1.default.createElement(ConversationHistory_1.default, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides }),
14
+ react_1.default.createElement(ConversationHistory_1.default, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides }),
15
15
  react_1.default.createElement("div", { className: 'cio-pia-conversation-footer' },
16
16
  isLoading && !error && react_1.default.createElement(SuggestedQuestionsSkeleton_1.default, null),
17
17
  !isLoading && !error && (react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: handleSubmitQuestion, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions })),
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  const react_1 = require("react");
5
5
  const transformers_1 = require("../utils/transformers");
6
- const extractAndTransformItems = (data) => {
6
+ const extractAndTransformItems = (data, formatImageUrl) => {
7
7
  var _a, _b;
8
8
  if (!((_b = (_a = data === null || data === void 0 ? void 0 : data.item_results) === null || _a === void 0 ? void 0 : _a.response) === null || _b === void 0 ? void 0 : _b.results)) {
9
9
  return null;
@@ -13,7 +13,7 @@ const extractAndTransformItems = (data) => {
13
13
  return null;
14
14
  }
15
15
  const transformedItems = results
16
- .map(transformers_1.transformResultItem)
16
+ .map((item) => (0, transformers_1.transformResultItem)(item, formatImageUrl))
17
17
  .filter((item) => item !== null);
18
18
  return transformedItems.length > 0 ? transformedItems : null;
19
19
  };
@@ -26,7 +26,7 @@ const fetchAnswerResults = ({ client, itemId, question, variationId, threadId, }
26
26
  });
27
27
  return response;
28
28
  });
29
- function useAnswerResults({ itemId, variationId, threadId, cioClient, }) {
29
+ function useAnswerResults({ itemId, variationId, threadId, cioClient, formatImageUrl, }) {
30
30
  const [answerResults, setAnswerResults] = (0, react_1.useState)(null);
31
31
  const [items, setItems] = (0, react_1.useState)(null);
32
32
  const [isLoading, setIsLoading] = (0, react_1.useState)(false);
@@ -39,7 +39,7 @@ function useAnswerResults({ itemId, variationId, threadId, cioClient, }) {
39
39
  fetchAnswerResults({ client: cioClient, itemId, question, variationId, threadId })
40
40
  .then((fetchedAnswerResults) => {
41
41
  setAnswerResults(fetchedAnswerResults);
42
- setItems(extractAndTransformItems(fetchedAnswerResults));
42
+ setItems(extractAndTransformItems(fetchedAnswerResults, formatImageUrl));
43
43
  setError(null);
44
44
  })
45
45
  .catch((err) => {
@@ -50,7 +50,7 @@ function useAnswerResults({ itemId, variationId, threadId, cioClient, }) {
50
50
  .finally(() => {
51
51
  setIsLoading(false);
52
52
  });
53
- }, [cioClient, itemId, variationId, threadId]);
53
+ }, [cioClient, itemId, variationId, threadId, formatImageUrl]);
54
54
  return {
55
55
  data: answerResults,
56
56
  items,
@@ -5,7 +5,7 @@ const useAnswerResults_1 = tslib_1.__importDefault(require("./useAnswerResults")
5
5
  const useCioClient_1 = tslib_1.__importDefault(require("./useCioClient"));
6
6
  const useSuggestedQuestions_1 = tslib_1.__importDefault(require("./useSuggestedQuestions"));
7
7
  function useCioPia(props) {
8
- const { apiKey, itemId, variationId, threadId, cioClient: providedClient, suggestedQuestionsParameters, } = props;
8
+ const { apiKey, itemId, variationId, threadId, cioClient: providedClient, suggestedQuestionsParameters, formatImageUrl, } = props;
9
9
  const defaultClient = (0, useCioClient_1.default)({ apiKey });
10
10
  const client = providedClient || defaultClient;
11
11
  const suggestedQuestions = (0, useSuggestedQuestions_1.default)({
@@ -20,6 +20,7 @@ function useCioPia(props) {
20
20
  variationId,
21
21
  threadId,
22
22
  cioClient: client,
23
+ formatImageUrl,
23
24
  });
24
25
  return {
25
26
  suggestedQuestions,
@@ -51,10 +51,10 @@ function useConversation({ pia, itemId, isConversation, }) {
51
51
  if (prev.length === 0)
52
52
  return prev;
53
53
  const updated = [...prev];
54
- updated[updated.length - 1] = Object.assign(Object.assign({}, updated[updated.length - 1]), { answer: answerValue });
54
+ updated[updated.length - 1] = Object.assign(Object.assign({}, updated[updated.length - 1]), { answer: answerValue, items: answers.items });
55
55
  return updated;
56
56
  });
57
- }, [isConversation, answers.data]);
57
+ }, [isConversation, answers.data, answers.items]);
58
58
  const currentAnswer = (_c = (_b = answers.data) === null || _b === void 0 ? void 0 : _b.value) !== null && _c !== void 0 ? _c : '';
59
59
  const currentItems = (_d = answers.items) !== null && _d !== void 0 ? _d : null;
60
60
  const error = answers.error || suggestedQuestions.error;
@@ -6,18 +6,26 @@ const tslib_1 = require("tslib");
6
6
  * Converts a raw ApiItem from the Get Answers API response into an Item object
7
7
  * that can be used in the Carousel component.
8
8
  */
9
- function transformResultItem(resultItem) {
9
+ function transformResultItem(resultItem, formatImageUrl) {
10
10
  const { value, matched_terms: matchedTerms, data } = resultItem, otherFields = tslib_1.__rest(resultItem, ["value", "matched_terms", "data"]);
11
11
  if (!data || typeof data !== 'object' || !data.id || !value) {
12
12
  return null;
13
13
  }
14
14
  const { id, variation_id: variationId, description, url, image_url: imageUrl, price, sale_price: salePrice, rating, reviews_count: reviewsCount, tags, sl_campaign_id: slCampaignId, sl_campaign_owner: slCampaignOwner, badge } = data, otherMetadataFields = tslib_1.__rest(data, ["id", "variation_id", "description", "url", "image_url", "price", "sale_price", "rating", "reviews_count", "tags", "sl_campaign_id", "sl_campaign_owner", "badge"]);
15
+ // Skip formatting for absent or empty URLs; consumers should handle placeholders separately.
16
+ let formattedImageUrl = imageUrl;
17
+ if (formatImageUrl && imageUrl) {
18
+ try {
19
+ formattedImageUrl = formatImageUrl(imageUrl);
20
+ }
21
+ catch (_a) {
22
+ formattedImageUrl = imageUrl;
23
+ }
24
+ }
15
25
  return Object.assign({ id, name: value, matchedTerms,
16
26
  variationId,
17
27
  description,
18
- url,
19
- imageUrl,
20
- price,
28
+ url, imageUrl: formattedImageUrl, price,
21
29
  salePrice,
22
30
  rating,
23
31
  reviewsCount,
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '2.1.2';
3
+ exports.default = '2.2.0';
@@ -14,8 +14,8 @@ import PiaCustomCarousel from './PiaCustomCarousel';
14
14
  import PiaModal from '../PiaConversation/PiaModal';
15
15
  import PiaConversation from '../PiaConversation/PiaConversation';
16
16
  export default function CioPia(props) {
17
- const { apiKey, itemId, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, children, translations, suggestedQuestionsParameters, } = props;
18
- const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline' } = displayConfigs || {};
17
+ const { apiKey, itemId, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, children, translations, suggestedQuestionsParameters, } = props;
18
+ const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, } = displayConfigs || {};
19
19
  const isConversation = mode === 'conversation' || type === 'modal';
20
20
  const pia = useCioPia({
21
21
  apiKey,
@@ -24,6 +24,7 @@ export default function CioPia(props) {
24
24
  variationId,
25
25
  cioClient,
26
26
  suggestedQuestionsParameters,
27
+ formatImageUrl: formatters?.formatImageUrl,
27
28
  });
28
29
  const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, handleSubmitQuestion, resetState, } = useConversation({ pia, itemId, isConversation });
29
30
  const renderProps = {
@@ -42,6 +43,7 @@ export default function CioPia(props) {
42
43
  error,
43
44
  currentItems,
44
45
  showFeedback,
46
+ showPreviousItems,
45
47
  learnMoreUrl,
46
48
  translations,
47
49
  callbacks,
@@ -5,7 +5,7 @@ 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
- export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, }) {
8
+ export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems = true, learnMoreUrl, translations, callbacks, componentOverrides, }) {
9
9
  const scrollContainerRef = useRef(null);
10
10
  useEffect(() => {
11
11
  const container = scrollContainerRef.current;
@@ -16,17 +16,21 @@ export default function ConversationHistory({ conversationHistory, isLoading, er
16
16
  });
17
17
  return () => cancelAnimationFrame(frameId);
18
18
  }, [conversationHistory, isLoading]);
19
- return (React.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-history', role: 'log', "aria-label": 'Conversation history' }, conversationHistory.map((entry, index) => {
20
- const isLast = index === conversationHistory.length - 1;
21
- return (React.createElement("div", { key: entry.id, className: 'cio-pia-conversation-entry' },
22
- React.createElement("div", { className: 'cio-pia-chat-question' }, entry.question),
23
- isLast && isLoading && (React.createElement("div", { className: 'cio-pia-conversation-loading', "aria-live": 'polite' },
24
- React.createElement(LoadingSkeleton, null))),
25
- isLast && !isLoading && error && (React.createElement(ErrorBlock, { message: error.message || 'Unexpected error' })),
26
- entry.answer && (React.createElement("div", { className: 'cio-pia-answer-container' },
27
- React.createElement(Answer, { text: entry.answer, componentOverride: componentOverrides?.answer }),
28
- isLast && currentItems && (React.createElement(PiaCustomCarousel, { items: currentItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks })),
29
- isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: callbacks?.onFeedback, componentOverride: componentOverrides?.feedback })))),
30
- isLast && entry.answer && (React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer }))));
31
- })));
19
+ return (React.createElement("div", { className: 'cio-pia-conversation-history' },
20
+ React.createElement("div", { ref: scrollContainerRef, className: 'cio-pia-conversation-entries', role: 'log', "aria-label": 'Conversation history' }, conversationHistory.map((entry, index) => {
21
+ const isLast = index === conversationHistory.length - 1;
22
+ const previousEntryItems = showPreviousItems ? entry.items : null;
23
+ const latestEntryItems = currentItems !== undefined ? currentItems : entry.items;
24
+ const carouselItems = isLast ? latestEntryItems : previousEntryItems;
25
+ return (React.createElement("div", { key: entry.id, className: 'cio-pia-conversation-entry' },
26
+ React.createElement("div", { className: 'cio-pia-chat-question' }, entry.question),
27
+ isLast && isLoading && (React.createElement("div", { className: 'cio-pia-conversation-loading', "aria-live": 'polite' },
28
+ React.createElement(LoadingSkeleton, null))),
29
+ isLast && !isLoading && error && (React.createElement(ErrorBlock, { message: error.message || 'Unexpected error' })),
30
+ entry.answer && (React.createElement("div", { className: 'cio-pia-answer-container' },
31
+ React.createElement(Answer, { text: entry.answer, componentOverride: componentOverrides?.answer }),
32
+ carouselItems && (React.createElement(PiaCustomCarousel, { items: carouselItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks })),
33
+ isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: callbacks?.onFeedback, componentOverride: componentOverrides?.feedback }))))));
34
+ })),
35
+ React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer })));
32
36
  }
@@ -1,5 +1,12 @@
1
1
  .cio-pia-conversation-history {
2
2
  --conversation-gap: 16px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ flex: 1;
6
+ min-height: 0;
7
+ }
8
+
9
+ .cio-pia-conversation-entries {
3
10
  display: flex;
4
11
  flex-direction: column;
5
12
  gap: var(--conversation-gap);
@@ -4,11 +4,11 @@ import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/Suggeste
4
4
  import SuggestedQuestionsSkeleton from '../SuggestedQuestionsContainer/SuggestedQuestionsSkeleton';
5
5
  import { translate } from '../../utils/translate';
6
6
  import ConversationHistory from '../ConversationHistory/ConversationHistory';
7
- export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }) {
7
+ export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }) {
8
8
  const hasHistory = conversationHistory.length > 0;
9
9
  return (React.createElement("div", { className: 'cio-pia-container cio-pia-conversation', "data-testid": 'cio-pia-container' },
10
10
  !hasHistory && (React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations))),
11
- React.createElement(ConversationHistory, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides }),
11
+ React.createElement(ConversationHistory, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides }),
12
12
  React.createElement("div", { className: 'cio-pia-conversation-footer' },
13
13
  isLoading && !error && React.createElement(SuggestedQuestionsSkeleton, null),
14
14
  !isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleSubmitQuestion, componentOverride: componentOverrides?.suggestedQuestions })),
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useState } from 'react';
2
2
  import { transformResultItem } from '../utils/transformers';
3
- const extractAndTransformItems = (data) => {
3
+ const extractAndTransformItems = (data, formatImageUrl) => {
4
4
  if (!data?.item_results?.response?.results) {
5
5
  return null;
6
6
  }
@@ -9,7 +9,7 @@ const extractAndTransformItems = (data) => {
9
9
  return null;
10
10
  }
11
11
  const transformedItems = results
12
- .map(transformResultItem)
12
+ .map((item) => transformResultItem(item, formatImageUrl))
13
13
  .filter((item) => item !== null);
14
14
  return transformedItems.length > 0 ? transformedItems : null;
15
15
  };
@@ -22,7 +22,7 @@ const fetchAnswerResults = async ({ client, itemId, question, variationId, threa
22
22
  });
23
23
  return response;
24
24
  };
25
- export default function useAnswerResults({ itemId, variationId, threadId, cioClient, }) {
25
+ export default function useAnswerResults({ itemId, variationId, threadId, cioClient, formatImageUrl, }) {
26
26
  const [answerResults, setAnswerResults] = useState(null);
27
27
  const [items, setItems] = useState(null);
28
28
  const [isLoading, setIsLoading] = useState(false);
@@ -35,7 +35,7 @@ export default function useAnswerResults({ itemId, variationId, threadId, cioCli
35
35
  fetchAnswerResults({ client: cioClient, itemId, question, variationId, threadId })
36
36
  .then((fetchedAnswerResults) => {
37
37
  setAnswerResults(fetchedAnswerResults);
38
- setItems(extractAndTransformItems(fetchedAnswerResults));
38
+ setItems(extractAndTransformItems(fetchedAnswerResults, formatImageUrl));
39
39
  setError(null);
40
40
  })
41
41
  .catch((err) => {
@@ -46,7 +46,7 @@ export default function useAnswerResults({ itemId, variationId, threadId, cioCli
46
46
  .finally(() => {
47
47
  setIsLoading(false);
48
48
  });
49
- }, [cioClient, itemId, variationId, threadId]);
49
+ }, [cioClient, itemId, variationId, threadId, formatImageUrl]);
50
50
  return {
51
51
  data: answerResults,
52
52
  items,
@@ -2,7 +2,7 @@ import useAnswerResults from './useAnswerResults';
2
2
  import useCioClient from './useCioClient';
3
3
  import useSuggestedQuestions from './useSuggestedQuestions';
4
4
  export default function useCioPia(props) {
5
- const { apiKey, itemId, variationId, threadId, cioClient: providedClient, suggestedQuestionsParameters, } = props;
5
+ const { apiKey, itemId, variationId, threadId, cioClient: providedClient, suggestedQuestionsParameters, formatImageUrl, } = props;
6
6
  const defaultClient = useCioClient({ apiKey });
7
7
  const client = providedClient || defaultClient;
8
8
  const suggestedQuestions = useSuggestedQuestions({
@@ -17,6 +17,7 @@ export default function useCioPia(props) {
17
17
  variationId,
18
18
  threadId,
19
19
  cioClient: client,
20
+ formatImageUrl,
20
21
  });
21
22
  return {
22
23
  suggestedQuestions,
@@ -49,10 +49,11 @@ export default function useConversation({ pia, itemId, isConversation, }) {
49
49
  updated[updated.length - 1] = {
50
50
  ...updated[updated.length - 1],
51
51
  answer: answerValue,
52
+ items: answers.items,
52
53
  };
53
54
  return updated;
54
55
  });
55
- }, [isConversation, answers.data]);
56
+ }, [isConversation, answers.data, answers.items]);
56
57
  const currentAnswer = answers.data?.value ?? '';
57
58
  const currentItems = answers.items ?? null;
58
59
  const error = answers.error || suggestedQuestions.error;
@@ -2,12 +2,22 @@
2
2
  * Converts a raw ApiItem from the Get Answers API response into an Item object
3
3
  * that can be used in the Carousel component.
4
4
  */
5
- export function transformResultItem(resultItem) {
5
+ export function transformResultItem(resultItem, formatImageUrl) {
6
6
  const { value, matched_terms: matchedTerms, data, ...otherFields } = resultItem;
7
7
  if (!data || typeof data !== 'object' || !data.id || !value) {
8
8
  return null;
9
9
  }
10
10
  const { id, variation_id: variationId, description, url, image_url: imageUrl, price, sale_price: salePrice, rating, reviews_count: reviewsCount, tags, sl_campaign_id: slCampaignId, sl_campaign_owner: slCampaignOwner, badge, ...otherMetadataFields } = data;
11
+ // Skip formatting for absent or empty URLs; consumers should handle placeholders separately.
12
+ let formattedImageUrl = imageUrl;
13
+ if (formatImageUrl && imageUrl) {
14
+ try {
15
+ formattedImageUrl = formatImageUrl(imageUrl);
16
+ }
17
+ catch {
18
+ formattedImageUrl = imageUrl;
19
+ }
20
+ }
11
21
  return {
12
22
  id,
13
23
  name: value,
@@ -15,7 +25,7 @@ export function transformResultItem(resultItem) {
15
25
  variationId,
16
26
  description,
17
27
  url,
18
- imageUrl,
28
+ imageUrl: formattedImageUrl,
19
29
  price,
20
30
  salePrice,
21
31
  rating,
@@ -1 +1 @@
1
- export default '2.1.2';
1
+ export default '2.2.0';
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { IncludeComponentOverrides, IncludeRenderProps } from '@constructor-io/constructorio-ui-components';
3
3
  import MockConstructorIOClient from '../../hooks/mocks/MockConstructorIOClient';
4
- import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, Translations, SuggestedQuestionsParameters } from '../../types';
4
+ import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, Translations, SuggestedQuestionsParameters, Formatters } from '../../types';
5
5
  export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
6
6
  apiKey: string;
7
7
  itemId: string;
@@ -11,6 +11,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
11
11
  cioClient?: MockConstructorIOClient;
12
12
  displayConfigs?: CioPiaDisplayConfigs;
13
13
  callbacks?: Callbacks;
14
+ /** Define formatter functions outside the component or memoize to avoid unnecessary re-renders. */
15
+ formatters?: Formatters;
14
16
  translations?: Translations;
15
17
  suggestedQuestionsParameters?: SuggestedQuestionsParameters;
16
18
  }
@@ -4,11 +4,22 @@ export interface ConversationHistoryProps {
4
4
  conversationHistory: ConversationEntry[];
5
5
  isLoading: boolean;
6
6
  error: Error | null;
7
+ /**
8
+ * Items for the latest conversation entry's carousel.
9
+ * - `undefined` (not provided): falls back to entry.items
10
+ * - `null`: explicitly no items, hides the carousel
11
+ * - `Item[]`: shows these items, overriding entry.items
12
+ */
7
13
  currentItems?: Item[] | null;
8
14
  showFeedback?: boolean;
15
+ /**
16
+ * Show product carousels on non-last conversation entries. Defaults to true.
17
+ * The last entry always falls back to its own items when currentItems is not provided.
18
+ */
19
+ showPreviousItems?: boolean;
9
20
  learnMoreUrl?: string;
10
21
  translations?: Translations;
11
22
  callbacks?: Callbacks;
12
23
  componentOverrides?: CioPiaComponentOverrides;
13
24
  }
14
- export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, }: ConversationHistoryProps): React.JSX.Element;
25
+ export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, translations, callbacks, componentOverrides, }: ConversationHistoryProps): React.JSX.Element;
@@ -5,4 +5,4 @@ export interface PiaConversationProps extends ConversationHistoryProps {
5
5
  displayedQuestions: Question[];
6
6
  handleSubmitQuestion: (question: string) => void;
7
7
  }
8
- export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }: PiaConversationProps): React.JSX.Element;
8
+ export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }: PiaConversationProps): React.JSX.Element;
@@ -1,12 +1,13 @@
1
1
  import { Nullable } from '@constructor-io/constructorio-client-javascript';
2
2
  import MockConstructorIOClient from './mocks/MockConstructorIOClient';
3
- import { Item, GetAnswerResultsResponse } from '../types';
3
+ import { Formatters, Item, GetAnswerResultsResponse } from '../types';
4
4
  export interface UseAnswerResultsProps {
5
5
  itemId: string;
6
6
  variationId?: string;
7
7
  threadId?: string;
8
8
  cioClient: MockConstructorIOClient;
9
9
  parameters?: Record<string, any>;
10
+ formatImageUrl?: Formatters['formatImageUrl'];
10
11
  }
11
12
  export interface UseAnswerResultsReturn {
12
13
  data: Nullable<GetAnswerResultsResponse>;
@@ -15,4 +16,4 @@ export interface UseAnswerResultsReturn {
15
16
  error: Error | null;
16
17
  getAnswer: (question: string) => void;
17
18
  }
18
- export default function useAnswerResults({ itemId, variationId, threadId, cioClient, }: UseAnswerResultsProps): UseAnswerResultsReturn;
19
+ export default function useAnswerResults({ itemId, variationId, threadId, cioClient, formatImageUrl, }: UseAnswerResultsProps): UseAnswerResultsReturn;
@@ -1,4 +1,4 @@
1
- import { SuggestedQuestionsParameters } from '../types';
1
+ import { Formatters, SuggestedQuestionsParameters } from '../types';
2
2
  import MockConstructorIOClient from './mocks/MockConstructorIOClient';
3
3
  import { UseAnswerResultsReturn } from './useAnswerResults';
4
4
  import { UseSuggestedQuestionsReturn } from './useSuggestedQuestions';
@@ -9,6 +9,8 @@ export interface UseCioPiaProps {
9
9
  threadId?: string;
10
10
  cioClient?: MockConstructorIOClient;
11
11
  suggestedQuestionsParameters?: SuggestedQuestionsParameters;
12
+ /** Define outside the component or wrap with useCallback to avoid unnecessary re-renders. */
13
+ formatImageUrl?: Formatters['formatImageUrl'];
12
14
  }
13
15
  export interface UseCioPiaReturn {
14
16
  suggestedQuestions: UseSuggestedQuestionsReturn;
@@ -32,6 +32,11 @@ export type CioPiaDisplayConfigs = {
32
32
  showFeedback?: boolean;
33
33
  mode?: CioPiaMode;
34
34
  type?: CioPiaType;
35
+ /**
36
+ * Show product carousels on non-last conversation entries. Defaults to true.
37
+ * The last entry always falls back to its own items when currentItems is not provided.
38
+ */
39
+ showPreviousItems?: boolean;
35
40
  };
36
41
  /**
37
42
  * Translations type for internationalizing UI strings.
@@ -50,6 +55,11 @@ export interface Callbacks {
50
55
  onProductCardClick?: (item: Item) => void;
51
56
  onFeedback?: (type: FeedbackType) => void;
52
57
  }
58
+ /** Formatter functions for transforming data before display. */
59
+ export interface Formatters {
60
+ /** Transforms image URLs before rendering (e.g., prepend a CDN base URL). */
61
+ formatImageUrl?: (url: string) => string;
62
+ }
53
63
  /** Extends Product type to include PIA-specific fields */
54
64
  export interface Item extends Product, Record<string, any> {
55
65
  url?: string;
@@ -59,6 +69,7 @@ export interface ConversationEntry {
59
69
  id: number;
60
70
  question: string;
61
71
  answer: string;
72
+ items?: Item[] | null;
62
73
  }
63
74
  /**
64
75
  * Render props passed to CioPia children function
@@ -1,7 +1,7 @@
1
1
  import { Nullable } from '@constructor-io/constructorio-client-javascript';
2
- import { Item, ApiItem } from '../types';
2
+ import { Formatters, Item, ApiItem } from '../types';
3
3
  /**
4
4
  * Converts a raw ApiItem from the Get Answers API response into an Item object
5
5
  * that can be used in the Carousel component.
6
6
  */
7
- export declare function transformResultItem(resultItem: ApiItem): Nullable<Item>;
7
+ export declare function transformResultItem(resultItem: ApiItem, formatImageUrl?: Formatters['formatImageUrl']): Nullable<Item>;
@@ -1,2 +1,2 @@
1
- declare const _default: "2.1.2";
1
+ declare const _default: "2.2.0";
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructor-io/constructorio-ui-pia",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Constructor.io AI Product Insights Agent UI library for web applications",
5
5
  "author": "Constructor.io Corporation",
6
6
  "license": "MIT",