@constructor-io/constructorio-ui-pia 2.9.0 → 2.10.1

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.
@@ -19,9 +19,10 @@ const PiaConversation_1 = tslib_1.__importDefault(require("../PiaConversation/Pi
19
19
  // eslint-disable-next-line complexity
20
20
  function CioPia(props) {
21
21
  var _a;
22
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, children, translations, suggestedQuestionsParameters, } = props;
22
+ const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, } = props;
23
23
  const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
24
24
  const isConversation = mode === 'conversation' || type === 'modal';
25
+ const { priceCurrency } = productCardProps || {};
25
26
  const pia = (0, useCioPia_1.default)({
26
27
  apiKey,
27
28
  itemId,
@@ -79,6 +80,7 @@ function CioPia(props) {
79
80
  qnaResultId,
80
81
  containerRef,
81
82
  onInputFocus: handleInputFocus,
83
+ priceCurrency,
82
84
  };
83
85
  if (type === 'modal') {
84
86
  return (react_1.default.createElement(PiaModal_1.default, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
@@ -94,7 +96,7 @@ function CioPia(props) {
94
96
  isLoading && react_1.default.createElement(LoadingSkeleton_1.default, { componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.loading }),
95
97
  !isLoading && error && react_1.default.createElement(ErrorBlock_1.default, { message: (error === null || error === void 0 ? void 0 : error.message) || 'Unexpected error' }),
96
98
  !isLoading && !error && (react_1.default.createElement(react_1.default.Fragment, null,
97
- currentAnswer && (react_1.default.createElement(PiaInlineAnswer_1.default, { 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 })),
99
+ currentAnswer && (react_1.default.createElement(PiaInlineAnswer_1.default, { 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 })),
98
100
  react_1.default.createElement(SuggestedQuestionsContainer_1.default, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions }))))));
99
101
  }
100
102
  exports.default = CioPia;
@@ -4,14 +4,53 @@ const tslib_1 = require("tslib");
4
4
  const react_1 = tslib_1.__importStar(require("react"));
5
5
  const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
6
  const contentTransformers_1 = require("../../utils/contentTransformers");
7
+ const translate_1 = require("../../utils/translate");
7
8
  function HtmlDescription({ product }) {
8
9
  const { description } = product;
9
10
  if (!description)
10
11
  return null;
11
12
  return (react_1.default.createElement("p", { className: 'cio-product-card-description', dangerouslySetInnerHTML: { __html: (0, contentTransformers_1.sanitizeHtml)(description) } }));
12
13
  }
13
- function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, }) {
14
+ function createPriceSectionOverride(priceCurrency) {
15
+ return function PriceSectionOverride({ product }) {
16
+ const { price, salePrice } = product;
17
+ if (price == null)
18
+ return null;
19
+ return (react_1.default.createElement("div", { className: 'cio-product-card-price-section flex items-baseline gap-2' },
20
+ react_1.default.createElement("span", { className: 'text-lg font-bold' },
21
+ priceCurrency,
22
+ "\u00A0", salePrice !== null && salePrice !== void 0 ? salePrice : price),
23
+ salePrice != null && (react_1.default.createElement("span", { className: 'text-sm text-gray-400 line-through' },
24
+ priceCurrency,
25
+ "\u00A0",
26
+ price))));
27
+ };
28
+ }
29
+ function getContentDefaults(content, priceSectionOverride) {
30
+ const defaults = {};
31
+ if (!(content === null || content === void 0 ? void 0 : content.description)) {
32
+ defaults.description = { reactNode: HtmlDescription };
33
+ }
34
+ if (!(content === null || content === void 0 ? void 0 : content.price) && priceSectionOverride) {
35
+ defaults.price = { reactNode: priceSectionOverride };
36
+ }
37
+ return defaults;
38
+ }
39
+ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
40
+ var _a, _b, _c;
41
+ const content = (_b = (_a = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _a === void 0 ? void 0 : _a.productCard) === null || _b === void 0 ? void 0 : _b.content;
42
+ const defaults = getContentDefaults(content, priceSectionOverride);
43
+ if (Object.keys(defaults).length === 0) {
44
+ return componentOverrides || {};
45
+ }
46
+ return Object.assign(Object.assign({}, componentOverrides), { item: Object.assign(Object.assign({}, componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item), { productCard: Object.assign(Object.assign({}, (_c = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _c === void 0 ? void 0 : _c.productCard), { content: Object.assign(Object.assign({}, content), defaults) }) }) });
47
+ }
48
+ function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
14
49
  const wrapperRef = (0, react_1.useRef)(null);
50
+ const { onAddToCart } = callbacks || {};
51
+ const addToCartHandler = (0, react_1.useCallback)((event, product) => {
52
+ onAddToCart === null || onAddToCart === void 0 ? void 0 : onAddToCart(product, event);
53
+ }, [onAddToCart]);
15
54
  // Determine to use user-defined click handler or default behavior
16
55
  const productClickHandler = (0, react_1.useCallback)((item) => {
17
56
  const position = items.findIndex((i) => i.id === item.id);
@@ -42,13 +81,19 @@ function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick
42
81
  el.removeEventListener(constructorio_ui_components_1.CIO_EVENTS.productCard.click, handleClick);
43
82
  };
44
83
  }, [productClickHandler]);
84
+ const priceSectionOverride = (0, react_1.useMemo)(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
45
85
  const mergedOverrides = (0, react_1.useMemo)(() => {
46
- var _a, _b, _c, _d, _e, _f;
47
- if ((_c = (_b = (_a = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _a === void 0 ? void 0 : _a.productCard) === null || _b === void 0 ? void 0 : _b.content) === null || _c === void 0 ? void 0 : _c.description) {
48
- return componentOverrides;
86
+ var _a;
87
+ const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
88
+ const cardOverrides = (_a = baseOverrides.item) === null || _a === void 0 ? void 0 : _a.productCard;
89
+ // The carousel never passes onAddToCart down and the card hides the button without it, so
90
+ // re-render the card with the handler. A full card override owns its own layout - skip it.
91
+ if (!onAddToCart || (cardOverrides === null || cardOverrides === void 0 ? void 0 : cardOverrides.reactNode)) {
92
+ return baseOverrides;
49
93
  }
50
- return Object.assign(Object.assign({}, componentOverrides), { item: Object.assign(Object.assign({}, componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item), { productCard: Object.assign(Object.assign({}, (_d = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _d === void 0 ? void 0 : _d.productCard), { content: Object.assign(Object.assign({}, (_f = (_e = componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.item) === null || _e === void 0 ? void 0 : _e.productCard) === null || _f === void 0 ? void 0 : _f.content), { description: { reactNode: HtmlDescription } }) }) }) });
51
- }, [componentOverrides]);
94
+ const addToCartText = (0, translate_1.translate)('Add to Cart', translations);
95
+ return Object.assign(Object.assign({}, baseOverrides), { item: Object.assign(Object.assign({}, baseOverrides.item), { productCard: Object.assign(Object.assign({}, cardOverrides), { reactNode: ({ product }) => (react_1.default.createElement(constructorio_ui_components_1.ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: addToCartHandler, componentOverrides: cardOverrides })) }) }) });
96
+ }, [componentOverrides, priceSectionOverride, onAddToCart, translations, addToCartHandler]);
52
97
  // If there are no items, do not render the carousel
53
98
  if (items.length === 0) {
54
99
  return null;
@@ -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, showPreviousItems = true, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, }) {
11
+ function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems = true, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, priceCurrency, }) {
12
12
  const scrollContainerRef = (0, react_1.useRef)(null);
13
13
  (0, react_1.useEffect)(() => {
14
14
  const container = scrollContainerRef.current;
@@ -34,7 +34,7 @@ function ConversationHistory({ conversationHistory, isLoading, error, currentIte
34
34
  isLast && !isLoading && error && (react_1.default.createElement(ErrorBlock_1.default, { message: error.message || 'Unexpected error' })),
35
35
  entry.answer && (react_1.default.createElement("div", { className: 'cio-pia-answer-container' },
36
36
  react_1.default.createElement(Answer_1.default, { text: entry.answer, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.answer }),
37
- carouselItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: carouselItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId })),
37
+ carouselItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: carouselItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId, translations: translations, priceCurrency: priceCurrency })),
38
38
  isLast && showFeedback && (react_1.default.createElement(Feedback_1.default, { translations: translations, onFeedback: handleFeedback, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.feedback }))))));
39
39
  })),
40
40
  disclaimerPosition === 'bottom' && disclaimer));
@@ -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, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, }) {
10
+ function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, priceCurrency, }) {
11
11
  const hasHistory = conversationHistory.length > 0;
12
12
  return (react_1.default.createElement("div", { ref: containerRef, 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, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId }),
14
+ react_1.default.createElement(ConversationHistory_1.default, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId, priceCurrency: priceCurrency }),
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: handleQuestionClick, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.suggestedQuestions })),
@@ -6,12 +6,12 @@ const Answer_1 = tslib_1.__importDefault(require("../Answer/Answer"));
6
6
  const Feedback_1 = tslib_1.__importDefault(require("../Feedback/Feedback"));
7
7
  const Disclaimer_1 = tslib_1.__importDefault(require("../CioPia/Disclaimer"));
8
8
  const PiaCustomCarousel_1 = tslib_1.__importDefault(require("../CioPia/PiaCustomCarousel"));
9
- function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, }) {
9
+ function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, priceCurrency, }) {
10
10
  const disclaimer = (react_1.default.createElement(Disclaimer_1.default, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.disclaimer }));
11
11
  return (react_1.default.createElement("div", { className: 'cio-pia-answer-container' },
12
12
  disclaimerPosition === 'top' && disclaimer,
13
13
  react_1.default.createElement(Answer_1.default, { text: currentAnswer, componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.answer }),
14
- currentItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: currentItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks, onResultClick: onResultClick, question: question, qnaResultId: qnaResultId })),
14
+ currentItems && (react_1.default.createElement(PiaCustomCarousel_1.default, { items: currentItems, componentOverrides: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.carousel, callbacks: callbacks, onResultClick: onResultClick, question: question, qnaResultId: qnaResultId, translations: translations, priceCurrency: priceCurrency })),
15
15
  showFeedback && (react_1.default.createElement(Feedback_1.default, { translations: translations, onFeedback: onFeedback || (callbacks === null || callbacks === void 0 ? void 0 : callbacks.onFeedback), componentOverride: componentOverrides === null || componentOverrides === void 0 ? void 0 : componentOverrides.feedback })),
16
16
  disclaimerPosition === 'bottom' && disclaimer));
17
17
  }
@@ -6,14 +6,19 @@ const agent_1 = tslib_1.__importDefault(require("./agent"));
6
6
  const version_1 = tslib_1.__importDefault(require("../../version"));
7
7
  class MockConstructorIOClient {
8
8
  constructor(options) {
9
- this.options = Object.assign({ version: options.version || `cio-ui-pia-${version_1.default}`, serviceUrl: options.serviceUrl || 'https://ac.cnstrc.com', quizzesServiceUrl: options.quizzesServiceUrl || 'https://quizzes.cnstrc.com', agentServiceUrl: options.agentServiceUrl || 'https://agent.cnstrc.com', sessionId: options.sessionId || 0, clientId: options.clientId || 'this-is-a-random-client-id', sendTrackingEvents: options.sendTrackingEvents !== undefined ? options.sendTrackingEvents : true, beaconMode: options.beaconMode !== undefined ? options.beaconMode : true, networkParameters: options.networkParameters || {} }, options);
10
- const cioClient = new constructorio_client_javascript_1.default(this.options);
9
+ const isServer = typeof window === 'undefined' || typeof document === 'undefined';
10
+ const clientOptions = Object.assign(Object.assign(Object.assign({ version: options.version || `cio-ui-pia-${version_1.default}`, serviceUrl: options.serviceUrl || 'https://ac.cnstrc.com', quizzesServiceUrl: options.quizzesServiceUrl || 'https://quizzes.cnstrc.com', agentServiceUrl: options.agentServiceUrl || 'https://agent.cnstrc.com' }, (isServer && {
11
+ clientId: options.clientId || 'this-is-a-random-client-id',
12
+ })), { sendTrackingEvents: options.sendTrackingEvents !== undefined ? options.sendTrackingEvents : true, beaconMode: options.beaconMode !== undefined ? options.beaconMode : true, networkParameters: options.networkParameters || {} }), options);
13
+ const cioClient = new constructorio_client_javascript_1.default(clientOptions);
14
+ const { clientId, sessionId } = cioClient.agent.options;
15
+ this.options = Object.assign(Object.assign({}, clientOptions), { clientId, sessionId });
11
16
  this.search = cioClient.search;
12
17
  this.browse = cioClient.browse;
13
18
  this.recommendations = cioClient.recommendations;
14
19
  this.tracker = cioClient.tracker;
15
20
  this.quizzes = cioClient.quizzes;
16
- this.agent = new agent_1.default(Object.assign(Object.assign({}, this.options), { clientId: options.clientId, sessionId: options.sessionId }));
21
+ this.agent = new agent_1.default(this.options);
17
22
  }
18
23
  }
19
24
  exports.default = MockConstructorIOClient;
@@ -10,6 +10,7 @@ const defaultTranslations = {
10
10
  'Is this answer useful?': 'Is this answer useful?',
11
11
  'Learn More.': 'Learn More.',
12
12
  'Ask about this product': 'Ask about this product',
13
+ 'Add to Cart': 'Add to Cart',
13
14
  };
14
15
  /**
15
16
  * Translates a word using the provided translations object.
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '2.9.0';
3
+ exports.default = '2.10.1';
@@ -15,9 +15,10 @@ import PiaModal from '../PiaConversation/PiaModal';
15
15
  import PiaConversation from '../PiaConversation/PiaConversation';
16
16
  // eslint-disable-next-line complexity
17
17
  export default function CioPia(props) {
18
- const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, children, translations, suggestedQuestionsParameters, } = props;
18
+ const { apiKey, itemId, itemName, threadId, variationId, cioClient, displayConfigs, componentOverrides, callbacks, formatters, productCardProps, children, translations, suggestedQuestionsParameters, } = props;
19
19
  const { learnMoreUrl, showFeedback, mode = 'default', type = 'inline', showPreviousItems, disclaimerPosition = 'bottom', } = displayConfigs || {};
20
20
  const isConversation = mode === 'conversation' || type === 'modal';
21
+ const { priceCurrency } = productCardProps || {};
21
22
  const pia = useCioPia({
22
23
  apiKey,
23
24
  itemId,
@@ -75,6 +76,7 @@ export default function CioPia(props) {
75
76
  qnaResultId,
76
77
  containerRef,
77
78
  onInputFocus: handleInputFocus,
79
+ priceCurrency,
78
80
  };
79
81
  if (type === 'modal') {
80
82
  return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
@@ -90,6 +92,6 @@ export default function CioPia(props) {
90
92
  isLoading && React.createElement(LoadingSkeleton, { componentOverride: componentOverrides?.loading }),
91
93
  !isLoading && error && React.createElement(ErrorBlock, { message: error?.message || 'Unexpected error' }),
92
94
  !isLoading && !error && (React.createElement(React.Fragment, null,
93
- 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 })),
95
+ 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 })),
94
96
  React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions }))))));
95
97
  }
@@ -1,14 +1,65 @@
1
1
  import React, { useCallback, useRef, useEffect, useMemo } from 'react';
2
- import { Carousel, CIO_EVENTS, } from '@constructor-io/constructorio-ui-components';
2
+ import { Carousel, ProductCard, CIO_EVENTS, } from '@constructor-io/constructorio-ui-components';
3
3
  import { sanitizeHtml } from '../../utils/contentTransformers';
4
+ import { translate } from '../../utils/translate';
4
5
  function HtmlDescription({ product }) {
5
6
  const { description } = product;
6
7
  if (!description)
7
8
  return null;
8
9
  return (React.createElement("p", { className: 'cio-product-card-description', dangerouslySetInnerHTML: { __html: sanitizeHtml(description) } }));
9
10
  }
10
- export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, }) {
11
+ function createPriceSectionOverride(priceCurrency) {
12
+ return function PriceSectionOverride({ product }) {
13
+ const { price, salePrice } = product;
14
+ if (price == null)
15
+ return null;
16
+ return (React.createElement("div", { className: 'cio-product-card-price-section flex items-baseline gap-2' },
17
+ React.createElement("span", { className: 'text-lg font-bold' },
18
+ priceCurrency,
19
+ "\u00A0",
20
+ salePrice ?? price),
21
+ salePrice != null && (React.createElement("span", { className: 'text-sm text-gray-400 line-through' },
22
+ priceCurrency,
23
+ "\u00A0",
24
+ price))));
25
+ };
26
+ }
27
+ function getContentDefaults(content, priceSectionOverride) {
28
+ const defaults = {};
29
+ if (!content?.description) {
30
+ defaults.description = { reactNode: HtmlDescription };
31
+ }
32
+ if (!content?.price && priceSectionOverride) {
33
+ defaults.price = { reactNode: priceSectionOverride };
34
+ }
35
+ return defaults;
36
+ }
37
+ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
38
+ const content = componentOverrides?.item?.productCard?.content;
39
+ const defaults = getContentDefaults(content, priceSectionOverride);
40
+ if (Object.keys(defaults).length === 0) {
41
+ return componentOverrides || {};
42
+ }
43
+ return {
44
+ ...componentOverrides,
45
+ item: {
46
+ ...componentOverrides?.item,
47
+ productCard: {
48
+ ...componentOverrides?.item?.productCard,
49
+ content: {
50
+ ...content,
51
+ ...defaults,
52
+ },
53
+ },
54
+ },
55
+ };
56
+ }
57
+ export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }) {
11
58
  const wrapperRef = useRef(null);
59
+ const { onAddToCart } = callbacks || {};
60
+ const addToCartHandler = useCallback((event, product) => {
61
+ onAddToCart?.(product, event);
62
+ }, [onAddToCart]);
12
63
  // Determine to use user-defined click handler or default behavior
13
64
  const productClickHandler = useCallback((item) => {
14
65
  const position = items.findIndex((i) => i.id === item.id);
@@ -39,24 +90,27 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
39
90
  el.removeEventListener(CIO_EVENTS.productCard.click, handleClick);
40
91
  };
41
92
  }, [productClickHandler]);
93
+ const priceSectionOverride = useMemo(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
42
94
  const mergedOverrides = useMemo(() => {
43
- if (componentOverrides?.item?.productCard?.content?.description) {
44
- return componentOverrides;
95
+ const baseOverrides = buildMergedOverrides(componentOverrides, priceSectionOverride);
96
+ const cardOverrides = baseOverrides.item?.productCard;
97
+ // The carousel never passes onAddToCart down and the card hides the button without it, so
98
+ // re-render the card with the handler. A full card override owns its own layout - skip it.
99
+ if (!onAddToCart || cardOverrides?.reactNode) {
100
+ return baseOverrides;
45
101
  }
102
+ const addToCartText = translate('Add to Cart', translations);
46
103
  return {
47
- ...componentOverrides,
104
+ ...baseOverrides,
48
105
  item: {
49
- ...componentOverrides?.item,
106
+ ...baseOverrides.item,
50
107
  productCard: {
51
- ...componentOverrides?.item?.productCard,
52
- content: {
53
- ...componentOverrides?.item?.productCard?.content,
54
- description: { reactNode: HtmlDescription },
55
- },
108
+ ...cardOverrides,
109
+ reactNode: ({ product }) => (React.createElement(ProductCard, { product: product, className: 'w-full h-full', addToCartText: addToCartText, onAddToCart: addToCartHandler, componentOverrides: cardOverrides })),
56
110
  },
57
111
  },
58
112
  };
59
- }, [componentOverrides]);
113
+ }, [componentOverrides, priceSectionOverride, onAddToCart, translations, addToCartHandler]);
60
114
  // If there are no items, do not render the carousel
61
115
  if (items.length === 0) {
62
116
  return null;
@@ -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, showPreviousItems = true, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, }) {
8
+ export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems = true, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, priceCurrency, }) {
9
9
  const scrollContainerRef = useRef(null);
10
10
  useEffect(() => {
11
11
  const container = scrollContainerRef.current;
@@ -31,7 +31,7 @@ export default function ConversationHistory({ conversationHistory, isLoading, er
31
31
  isLast && !isLoading && error && (React.createElement(ErrorBlock, { message: error.message || 'Unexpected error' })),
32
32
  entry.answer && (React.createElement("div", { className: 'cio-pia-answer-container' },
33
33
  React.createElement(Answer, { text: entry.answer, componentOverride: componentOverrides?.answer }),
34
- carouselItems && (React.createElement(PiaCustomCarousel, { items: carouselItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId })),
34
+ carouselItems && (React.createElement(PiaCustomCarousel, { items: carouselItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId, translations: translations, priceCurrency: priceCurrency })),
35
35
  isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: handleFeedback, componentOverride: componentOverrides?.feedback }))))));
36
36
  })),
37
37
  disclaimerPosition === 'bottom' && disclaimer));
@@ -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, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, }) {
7
+ export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, priceCurrency, }) {
8
8
  const hasHistory = conversationHistory.length > 0;
9
9
  return (React.createElement("div", { ref: containerRef, 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, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId }),
11
+ React.createElement(ConversationHistory, { conversationHistory: conversationHistory, isLoading: isLoading, error: error, currentItems: currentItems, showFeedback: showFeedback, showPreviousItems: showPreviousItems, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides, handleFeedback: handleFeedback, onResultClick: onResultClick, qnaResultId: qnaResultId, priceCurrency: priceCurrency }),
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: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })),
@@ -3,12 +3,12 @@ import Answer from '../Answer/Answer';
3
3
  import Feedback from '../Feedback/Feedback';
4
4
  import Disclaimer from '../CioPia/Disclaimer';
5
5
  import PiaCustomCarousel from '../CioPia/PiaCustomCarousel';
6
- export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, }) {
6
+ export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, priceCurrency, }) {
7
7
  const disclaimer = (React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer }));
8
8
  return (React.createElement("div", { className: 'cio-pia-answer-container' },
9
9
  disclaimerPosition === 'top' && disclaimer,
10
10
  React.createElement(Answer, { text: currentAnswer, componentOverride: componentOverrides?.answer }),
11
- currentItems && (React.createElement(PiaCustomCarousel, { items: currentItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: question, qnaResultId: qnaResultId })),
11
+ currentItems && (React.createElement(PiaCustomCarousel, { items: currentItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: question, qnaResultId: qnaResultId, translations: translations, priceCurrency: priceCurrency })),
12
12
  showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: onFeedback || callbacks?.onFeedback, componentOverride: componentOverrides?.feedback })),
13
13
  disclaimerPosition === 'bottom' && disclaimer));
14
14
  }
@@ -10,29 +10,29 @@ class MockConstructorIOClient {
10
10
  quizzes;
11
11
  agent;
12
12
  constructor(options) {
13
- this.options = {
13
+ const isServer = typeof window === 'undefined' || typeof document === 'undefined';
14
+ const clientOptions = {
14
15
  version: options.version || `cio-ui-pia-${version}`,
15
16
  serviceUrl: options.serviceUrl || 'https://ac.cnstrc.com',
16
17
  quizzesServiceUrl: options.quizzesServiceUrl || 'https://quizzes.cnstrc.com',
17
18
  agentServiceUrl: options.agentServiceUrl || 'https://agent.cnstrc.com',
18
- sessionId: options.sessionId || 0,
19
- clientId: options.clientId || 'this-is-a-random-client-id',
19
+ ...(isServer && {
20
+ clientId: options.clientId || 'this-is-a-random-client-id',
21
+ }),
20
22
  sendTrackingEvents: options.sendTrackingEvents !== undefined ? options.sendTrackingEvents : true,
21
23
  beaconMode: options.beaconMode !== undefined ? options.beaconMode : true,
22
24
  networkParameters: options.networkParameters || {},
23
25
  ...options,
24
26
  };
25
- const cioClient = new ConstructorioClient(this.options);
27
+ const cioClient = new ConstructorioClient(clientOptions);
28
+ const { clientId, sessionId } = cioClient.agent.options;
29
+ this.options = { ...clientOptions, clientId, sessionId };
26
30
  this.search = cioClient.search;
27
31
  this.browse = cioClient.browse;
28
32
  this.recommendations = cioClient.recommendations;
29
33
  this.tracker = cioClient.tracker;
30
34
  this.quizzes = cioClient.quizzes;
31
- this.agent = new MockAgent({
32
- ...this.options,
33
- clientId: options.clientId,
34
- sessionId: options.sessionId,
35
- });
35
+ this.agent = new MockAgent(this.options);
36
36
  }
37
37
  }
38
38
  export default MockConstructorIOClient;
@@ -7,6 +7,7 @@ const defaultTranslations = {
7
7
  'Is this answer useful?': 'Is this answer useful?',
8
8
  'Learn More.': 'Learn More.',
9
9
  'Ask about this product': 'Ask about this product',
10
+ 'Add to Cart': 'Add to Cart',
10
11
  };
11
12
  /**
12
13
  * Translates a word using the provided translations object.
@@ -1 +1 @@
1
- export default '2.9.0';
1
+ export default '2.10.1';
@@ -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, Formatters } from '../../types';
4
+ import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, Translations, SuggestedQuestionsParameters, Formatters, ProductCardDisplayProps } from '../../types';
5
5
  export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
6
6
  /** Your Constructor.io API key. */
7
7
  apiKey: string;
@@ -23,6 +23,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
23
23
  componentOverrides?: CioPiaComponentOverrides;
24
24
  /** Formatter functions for transforming data before display. */
25
25
  formatters?: Formatters;
26
+ /** Props forwarded to the ProductCard rendered inside the carousel. */
27
+ productCardProps?: ProductCardDisplayProps;
26
28
  /** UI string translations for internationalization. */
27
29
  translations?: Translations;
28
30
  /** Parameters for the suggested questions request. */
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { CarouselOverrides } from '@constructor-io/constructorio-ui-components';
3
- import { Callbacks, Item } from '../../types';
3
+ import { Callbacks, Item, Translations } from '../../types';
4
4
  interface PiaCustomCarouselProps {
5
5
  items: Array<Item>;
6
6
  componentOverrides?: CarouselOverrides<Item>;
@@ -8,6 +8,8 @@ interface PiaCustomCarouselProps {
8
8
  onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
9
9
  question?: string;
10
10
  qnaResultId?: string;
11
+ translations?: Translations;
12
+ priceCurrency?: string;
11
13
  }
12
- export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, }: PiaCustomCarouselProps): React.JSX.Element | null;
14
+ export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, translations, priceCurrency, }: PiaCustomCarouselProps): React.JSX.Element | null;
13
15
  export {};
@@ -25,5 +25,6 @@ export interface ConversationHistoryProps {
25
25
  handleFeedback?: (type: FeedbackType) => void;
26
26
  onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
27
27
  qnaResultId?: string;
28
+ priceCurrency?: string;
28
29
  }
29
- export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, }: ConversationHistoryProps): React.JSX.Element;
30
+ export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, priceCurrency, }: ConversationHistoryProps): React.JSX.Element;
@@ -11,4 +11,4 @@ export interface PiaConversationProps extends ConversationHistoryProps {
11
11
  qnaResultId?: string;
12
12
  onInputFocus?: () => void;
13
13
  }
14
- export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, }: PiaConversationProps): React.JSX.Element;
14
+ export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, handleFeedback, onResultClick, qnaResultId, onInputFocus, priceCurrency, }: PiaConversationProps): React.JSX.Element;
@@ -13,6 +13,7 @@ interface PiaInlineAnswerProps {
13
13
  onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
14
14
  question?: string;
15
15
  qnaResultId?: string;
16
+ priceCurrency?: string;
16
17
  }
17
- export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, }: PiaInlineAnswerProps): React.JSX.Element;
18
+ export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, priceCurrency, }: PiaInlineAnswerProps): React.JSX.Element;
18
19
  export {};
@@ -59,6 +59,7 @@ export type Translations = {
59
59
  'Is this answer useful?'?: string;
60
60
  'Learn More.'?: string;
61
61
  'Ask about this product'?: string;
62
+ 'Add to Cart'?: string;
62
63
  };
63
64
  export type QuestionSource = 'user' | 'suggestion';
64
65
  export interface PiaCallbackContext {
@@ -70,6 +71,12 @@ export interface Callbacks {
70
71
  onQuestionSubmit?: (question: string, context: PiaCallbackContext, source: QuestionSource) => void;
71
72
  /** Called when a product card in the carousel is clicked. */
72
73
  onProductCardClick?: (item: Item) => void;
74
+ /**
75
+ * Called when the "Add to Cart" button on a product card is clicked.
76
+ * Providing this callback is what renders the button.
77
+ * Clicking the button does not fire `onProductCardClick`.
78
+ */
79
+ onAddToCart?: (item: Item, event: React.MouseEvent) => void;
73
80
  /** Called when the user submits positive or negative feedback on an answer. */
74
81
  onFeedback?: (type: FeedbackType) => void;
75
82
  /** Called when a new answer is received. Passes the full conversation history. */
@@ -86,6 +93,14 @@ export interface Formatters {
86
93
  /** Transforms image URLs before rendering (e.g., prepend a CDN base URL). */
87
94
  formatImageUrl?: (url: string) => string;
88
95
  }
96
+ /** Props forwarded to the ProductCard rendered inside the carousel. */
97
+ export interface ProductCardDisplayProps {
98
+ /**
99
+ * Currency symbol to display next to product prices (e.g., "€", "£").
100
+ * When omitted, the default ProductCard price rendering is used.
101
+ */
102
+ priceCurrency?: string;
103
+ }
89
104
  /** Extends Product type to include PIA-specific fields */
90
105
  export interface Item extends Product, Record<string, any> {
91
106
  url?: string;
@@ -1,2 +1,2 @@
1
- declare const _default: "2.9.0";
1
+ declare const _default: "2.10.1";
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.9.0",
3
+ "version": "2.10.1",
4
4
  "description": "Constructor.io AI Product Insights Agent UI library for web applications",
5
5
  "author": "Constructor.io Corporation",
6
6
  "license": "MIT",
@@ -26,8 +26,10 @@
26
26
  "module": "lib/mjs/index.js",
27
27
  "exports": {
28
28
  "./styles.css": {
29
+ "style": "./lib/mjs/styles.css",
29
30
  "import": "./lib/mjs/styles.css",
30
- "require": "./lib/cjs/styles.css"
31
+ "require": "./lib/cjs/styles.css",
32
+ "default": "./lib/mjs/styles.css"
31
33
  },
32
34
  "./constructorio-ui-pia-bundled": {
33
35
  "import": "./dist/constructorio-ui-pia-bundled.js",