@constructor-io/constructorio-ui-pia 2.8.0 → 2.10.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 (40) hide show
  1. package/dist/constructorio-ui-pia-bundled.js +75 -72
  2. package/lib/cjs/components/CioPia/CioPia.js +29 -5
  3. package/lib/cjs/components/CioPia/PiaCustomCarousel.js +42 -9
  4. package/lib/cjs/components/ConversationHistory/ConversationHistory.js +3 -3
  5. package/lib/cjs/components/PiaConversation/PiaConversation.js +2 -2
  6. package/lib/cjs/components/PiaInlineAnswer/PiaInlineAnswer.js +3 -3
  7. package/lib/cjs/constants.js +2 -1
  8. package/lib/cjs/hooks/useCioClient.js +2 -2
  9. package/lib/cjs/hooks/useCioPia.js +12 -3
  10. package/lib/cjs/hooks/useConversation.js +32 -9
  11. package/lib/cjs/hooks/useTracking.js +71 -0
  12. package/lib/cjs/hooks/useViewportTracking.js +92 -0
  13. package/lib/cjs/version.js +1 -1
  14. package/lib/mjs/components/CioPia/CioPia.js +28 -5
  15. package/lib/mjs/components/CioPia/PiaCustomCarousel.js +54 -20
  16. package/lib/mjs/components/ConversationHistory/ConversationHistory.js +3 -3
  17. package/lib/mjs/components/PiaConversation/PiaConversation.js +2 -2
  18. package/lib/mjs/components/PiaInlineAnswer/PiaInlineAnswer.js +3 -3
  19. package/lib/mjs/constants.js +1 -0
  20. package/lib/mjs/hooks/useCioClient.js +2 -2
  21. package/lib/mjs/hooks/useCioPia.js +13 -4
  22. package/lib/mjs/hooks/useConversation.js +24 -2
  23. package/lib/mjs/hooks/useTracking.js +107 -0
  24. package/lib/mjs/hooks/useViewportTracking.js +89 -0
  25. package/lib/mjs/version.js +1 -1
  26. package/lib/types/components/CioPia/CioPia.d.ts +5 -1
  27. package/lib/types/components/CioPia/PiaCustomCarousel.d.ts +5 -1
  28. package/lib/types/components/ConversationHistory/ConversationHistory.d.ts +6 -2
  29. package/lib/types/components/PiaConversation/PiaConversation.d.ts +5 -2
  30. package/lib/types/components/PiaInlineAnswer/PiaInlineAnswer.d.ts +7 -2
  31. package/lib/types/constants.d.ts +1 -0
  32. package/lib/types/hooks/useCioClient.d.ts +3 -7
  33. package/lib/types/hooks/useCioPia.d.ts +4 -0
  34. package/lib/types/hooks/useConversation.d.ts +5 -2
  35. package/lib/types/hooks/useTracking.d.ts +27 -0
  36. package/lib/types/hooks/useViewportTracking.d.ts +10 -0
  37. package/lib/types/types.d.ts +8 -0
  38. package/lib/types/utils/transformers.d.ts +1 -1
  39. package/lib/types/version.d.ts +1 -1
  40. package/package.json +6 -3
@@ -7,10 +7,60 @@ function HtmlDescription({ product }) {
7
7
  return null;
8
8
  return (React.createElement("p", { className: 'cio-product-card-description', dangerouslySetInnerHTML: { __html: sanitizeHtml(description) } }));
9
9
  }
10
- export default function PiaCustomCarousel({ items, componentOverrides, callbacks, }) {
10
+ function createPriceSectionOverride(priceCurrency) {
11
+ return function PriceSectionOverride({ product }) {
12
+ const { price, salePrice } = product;
13
+ if (price == null)
14
+ return null;
15
+ return (React.createElement("div", { className: 'cio-product-card-price-section flex items-baseline gap-2' },
16
+ React.createElement("span", { className: 'text-lg font-bold' },
17
+ priceCurrency,
18
+ "\u00A0",
19
+ salePrice ?? price),
20
+ salePrice != null && (React.createElement("span", { className: 'text-sm text-gray-400 line-through' },
21
+ priceCurrency,
22
+ "\u00A0",
23
+ price))));
24
+ };
25
+ }
26
+ function getContentDefaults(content, priceSectionOverride) {
27
+ const defaults = {};
28
+ if (!content?.description) {
29
+ defaults.description = { reactNode: HtmlDescription };
30
+ }
31
+ if (!content?.price && priceSectionOverride) {
32
+ defaults.price = { reactNode: priceSectionOverride };
33
+ }
34
+ return defaults;
35
+ }
36
+ function buildMergedOverrides(componentOverrides, priceSectionOverride) {
37
+ const content = componentOverrides?.item?.productCard?.content;
38
+ const defaults = getContentDefaults(content, priceSectionOverride);
39
+ if (Object.keys(defaults).length === 0) {
40
+ return componentOverrides || {};
41
+ }
42
+ return {
43
+ ...componentOverrides,
44
+ item: {
45
+ ...componentOverrides?.item,
46
+ productCard: {
47
+ ...componentOverrides?.item?.productCard,
48
+ content: {
49
+ ...content,
50
+ ...defaults,
51
+ },
52
+ },
53
+ },
54
+ };
55
+ }
56
+ export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, priceCurrency, }) {
11
57
  const wrapperRef = useRef(null);
12
58
  // Determine to use user-defined click handler or default behavior
13
59
  const productClickHandler = useCallback((item) => {
60
+ const position = items.findIndex((i) => i.id === item.id);
61
+ if (onResultClick && question) {
62
+ onResultClick(item, position >= 0 ? position : 0, question, qnaResultId);
63
+ }
14
64
  const { onProductCardClick } = callbacks || {};
15
65
  if (onProductCardClick) {
16
66
  onProductCardClick(item);
@@ -18,7 +68,7 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
18
68
  else if (item?.url) {
19
69
  window.open(item.url, '_blank', 'noopener,noreferrer');
20
70
  }
21
- }, [callbacks]);
71
+ }, [callbacks, items, onResultClick, question, qnaResultId]);
22
72
  // Set up event listener for product card clicks
23
73
  useEffect(() => {
24
74
  const el = wrapperRef.current;
@@ -35,24 +85,8 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
35
85
  el.removeEventListener(CIO_EVENTS.productCard.click, handleClick);
36
86
  };
37
87
  }, [productClickHandler]);
38
- const mergedOverrides = useMemo(() => {
39
- if (componentOverrides?.item?.productCard?.content?.description) {
40
- return componentOverrides;
41
- }
42
- return {
43
- ...componentOverrides,
44
- item: {
45
- ...componentOverrides?.item,
46
- productCard: {
47
- ...componentOverrides?.item?.productCard,
48
- content: {
49
- ...componentOverrides?.item?.productCard?.content,
50
- description: { reactNode: HtmlDescription },
51
- },
52
- },
53
- },
54
- };
55
- }, [componentOverrides]);
88
+ const priceSectionOverride = useMemo(() => (priceCurrency ? createPriceSectionOverride(priceCurrency) : undefined), [priceCurrency]);
89
+ const mergedOverrides = useMemo(() => buildMergedOverrides(componentOverrides, priceSectionOverride), [componentOverrides, priceSectionOverride]);
56
90
  // If there are no items, do not render the carousel
57
91
  if (items.length === 0) {
58
92
  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, }) {
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,8 +31,8 @@ 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 })),
35
- isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: callbacks?.onFeedback, componentOverride: componentOverrides?.feedback }))))));
34
+ carouselItems && (React.createElement(PiaCustomCarousel, { items: carouselItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: entry.question, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
35
+ isLast && showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: handleFeedback, componentOverride: componentOverrides?.feedback }))))));
36
36
  })),
37
37
  disclaimerPosition === 'bottom' && disclaimer));
38
38
  }
@@ -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, 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 }),
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, }) {
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 })),
12
- showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: callbacks?.onFeedback, componentOverride: componentOverrides?.feedback })),
11
+ currentItems && (React.createElement(PiaCustomCarousel, { items: currentItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks, onResultClick: onResultClick, question: question, qnaResultId: qnaResultId, priceCurrency: priceCurrency })),
12
+ showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: onFeedback || callbacks?.onFeedback, componentOverride: componentOverrides?.feedback })),
13
13
  disclaimerPosition === 'bottom' && disclaimer));
14
14
  }
@@ -1,5 +1,6 @@
1
1
  export const DEMO_API_KEY = 'key_13TO6qN9H3BP3Wrn';
2
2
  export const DEMO_ITEM_ID = '149100215';
3
+ export const DEMO_ITEM_NAME = 'Ensure Original Nutrition Shake';
3
4
  export const DEMO_QUESTION = 'What is the nutritional content of this shake?';
4
5
  export const DEMO_QUESTION_ALTERNATIVE_PRODUCTS = 'What alternative products are available for this item?';
5
6
  export const MOCK_QUESTIONS = [
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
+ import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
2
3
  import version from '../version';
3
- import MockConstructorIOClient from './mocks/MockConstructorIOClient';
4
4
  const useCioClient = ({ apiKey, cioClient, options } = {}) => {
5
5
  if (!apiKey && !cioClient) {
6
6
  throw new Error('Api Key or Constructor Client required');
@@ -9,7 +9,7 @@ const useCioClient = ({ apiKey, cioClient, options } = {}) => {
9
9
  if (cioClient)
10
10
  return cioClient;
11
11
  if (apiKey) {
12
- return new MockConstructorIOClient({
12
+ return new ConstructorIOClient({
13
13
  apiKey,
14
14
  sendTrackingEvents: true,
15
15
  version: `cio-ui-pia-${version}`,
@@ -1,13 +1,21 @@
1
- import { useState } from 'react';
1
+ import { useMemo, useState } from 'react';
2
+ import MockConstructorIOClient from './mocks/MockConstructorIOClient';
2
3
  import useAnswerResults from './useAnswerResults';
3
- import useCioClient from './useCioClient';
4
4
  import useSuggestedQuestions from './useSuggestedQuestions';
5
+ import version from '../version';
5
6
  export default function useCioPia(props) {
6
7
  const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, formatImageUrl, } = props;
7
8
  const [generatedThreadId] = useState(() => crypto.randomUUID());
8
9
  const threadId = providedThreadId || generatedThreadId;
9
- const defaultClient = useCioClient({ apiKey });
10
- const client = providedClient || defaultClient;
10
+ const client = useMemo(() => {
11
+ if (providedClient)
12
+ return providedClient;
13
+ return new MockConstructorIOClient({
14
+ apiKey,
15
+ sendTrackingEvents: true,
16
+ version: `cio-ui-pia-${version}`,
17
+ });
18
+ }, [apiKey, providedClient]);
11
19
  const suggestedQuestions = useSuggestedQuestions({
12
20
  itemId,
13
21
  variationId,
@@ -23,6 +31,7 @@ export default function useCioPia(props) {
23
31
  formatImageUrl,
24
32
  });
25
33
  return {
34
+ cioClient: client,
26
35
  threadId,
27
36
  suggestedQuestions,
28
37
  answers,
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- export default function useConversation({ pia, itemId, isConversation, callbacks, }) {
2
+ export default function useConversation({ pia, itemId, isConversation, callbacks, tracking, }) {
3
3
  const { suggestedQuestions, answers, threadId } = pia;
4
4
  const { getAnswer } = answers;
5
5
  const context = useMemo(() => ({ itemId, threadId }), [itemId, threadId]);
@@ -8,8 +8,11 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
8
8
  const [conversationHistory, setConversationHistory] = useState([]);
9
9
  const entryIdRef = useRef(0);
10
10
  const prevAnswerValueRef = useRef(answers.data?.value);
11
+ const hasTrackedCurrentAnswerRef = useRef(false);
12
+ const answersRef = useRef(answers);
11
13
  const callbacksRef = useRef(callbacks);
12
14
  const contextRef = useRef(context);
15
+ const trackingRef = useRef(tracking);
13
16
  const lastQuestionRef = useRef('');
14
17
  const lastSourceRef = useRef('user');
15
18
  useEffect(() => {
@@ -18,9 +21,16 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
18
21
  useEffect(() => {
19
22
  contextRef.current = context;
20
23
  }, [context]);
24
+ useEffect(() => {
25
+ trackingRef.current = tracking;
26
+ }, [tracking]);
27
+ useEffect(() => {
28
+ answersRef.current = answers;
29
+ }, [answers]);
21
30
  const submitQuestion = useCallback((question, source) => {
22
31
  lastSourceRef.current = source;
23
32
  lastQuestionRef.current = question;
33
+ hasTrackedCurrentAnswerRef.current = false;
24
34
  setCurrentQuestion(question);
25
35
  getAnswer(question);
26
36
  if (isConversation) {
@@ -30,16 +40,23 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
30
40
  }
31
41
  }, [getAnswer, isConversation]);
32
42
  const handleSubmitQuestion = useCallback((question) => {
43
+ trackingRef.current?.trackQuestionSubmit(question);
33
44
  callbacksRef.current?.onQuestionSubmit?.(question, contextRef.current, 'user');
34
45
  submitQuestion(question, 'user');
35
46
  }, [submitQuestion]);
36
47
  const handleQuestionClick = useCallback((question) => {
48
+ trackingRef.current?.trackQuestionClick(question);
37
49
  callbacksRef.current?.onQuestionSubmit?.(question, contextRef.current, 'suggestion');
38
50
  submitQuestion(question, 'suggestion');
39
51
  }, [submitQuestion]);
40
52
  const handleInputFocus = useCallback(() => {
53
+ trackingRef.current?.trackFocus();
41
54
  callbacksRef.current?.onFocus?.(contextRef.current);
42
55
  }, []);
56
+ const handleFeedback = useCallback((type) => {
57
+ trackingRef.current?.trackAnswerFeedback(type, answersRef.current.data?.qna_result_id);
58
+ callbacksRef.current?.onFeedback?.(type);
59
+ }, []);
43
60
  const resetState = useCallback(() => {
44
61
  setCurrentQuestion('');
45
62
  setDisplayedQuestions(suggestedQuestions.data);
@@ -58,7 +75,11 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
58
75
  useEffect(() => {
59
76
  if (answers.data?.follow_up_questions)
60
77
  setDisplayedQuestions(answers.data.follow_up_questions);
61
- }, [answers.data]);
78
+ if (answers.data && lastQuestionRef.current && !hasTrackedCurrentAnswerRef.current) {
79
+ hasTrackedCurrentAnswerRef.current = true;
80
+ trackingRef.current?.trackAnswerView(lastQuestionRef.current, answers.data, answers.items);
81
+ }
82
+ }, [answers.data, answers.items]);
62
83
  useEffect(() => {
63
84
  const answerValue = answers.data?.value ?? '';
64
85
  if (!answerValue)
@@ -113,6 +134,7 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
113
134
  handleSubmitQuestion,
114
135
  handleQuestionClick,
115
136
  handleInputFocus,
137
+ handleFeedback,
116
138
  resetState,
117
139
  };
118
140
  }
@@ -0,0 +1,107 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { FeedbackType } from '../types';
3
+ export default function useTracking({ cioClient, itemId, itemName, variationId, threadId, }) {
4
+ const tracker = cioClient?.tracker;
5
+ const baseParams = useMemo(() => ({
6
+ itemId,
7
+ itemName,
8
+ ...(variationId && { variationId }),
9
+ ...(threadId && { threadId }),
10
+ }), [itemId, itemName, variationId, threadId]);
11
+ const mapQuestions = useCallback((questions) => questions.map((q) => ({ question: q.value })), []);
12
+ const trackViews = useCallback((questions, viewTimespans) => {
13
+ tracker?.trackProductInsightsAgentViews({
14
+ ...baseParams,
15
+ questions: mapQuestions(questions),
16
+ viewTimespans,
17
+ });
18
+ }, [tracker, baseParams, mapQuestions]);
19
+ const trackView = useCallback((questions) => {
20
+ tracker?.trackProductInsightsAgentView({
21
+ ...baseParams,
22
+ questions: mapQuestions(questions),
23
+ });
24
+ }, [tracker, baseParams, mapQuestions]);
25
+ const trackOutOfView = useCallback(() => {
26
+ tracker?.trackProductInsightsAgentOutOfView(baseParams);
27
+ }, [tracker, baseParams]);
28
+ const trackFocus = useCallback(() => {
29
+ tracker?.trackProductInsightsAgentFocus(baseParams);
30
+ }, [tracker, baseParams]);
31
+ const trackQuestionClick = useCallback((question) => {
32
+ tracker?.trackProductInsightsAgentQuestionClick({
33
+ ...baseParams,
34
+ question,
35
+ });
36
+ }, [tracker, baseParams]);
37
+ const trackQuestionSubmit = useCallback((question) => {
38
+ tracker?.trackProductInsightsAgentQuestionSubmit({
39
+ ...baseParams,
40
+ question,
41
+ });
42
+ }, [tracker, baseParams]);
43
+ const trackAnswerView = useCallback((question, answerData, items) => {
44
+ tracker?.trackProductInsightsAgentAnswerView({
45
+ ...baseParams,
46
+ question,
47
+ answerText: answerData.value,
48
+ qnaResultId: answerData.qna_result_id,
49
+ ...(items &&
50
+ items.length > 0 && {
51
+ items: items.map((item) => ({
52
+ itemId: item.id,
53
+ itemName: item.name,
54
+ })),
55
+ }),
56
+ ...(answerData.follow_up_questions &&
57
+ answerData.follow_up_questions.length > 0 && {
58
+ followUpQuestions: answerData.follow_up_questions.map((q) => ({
59
+ value: q.value,
60
+ })),
61
+ }),
62
+ });
63
+ }, [tracker, baseParams]);
64
+ const trackAnswerFeedback = useCallback((feedbackType, qnaResultId) => {
65
+ const feedbackLabel = feedbackType === FeedbackType.UP ? 'thumbs_up' : 'thumbs_down';
66
+ tracker?.trackProductInsightsAgentAnswerFeedback({
67
+ ...baseParams,
68
+ feedbackLabel,
69
+ ...(qnaResultId && { qnaResultId }),
70
+ });
71
+ }, [tracker, baseParams]);
72
+ const trackResultClick = useCallback((clickedItem, position, question, qnaResultId) => {
73
+ tracker?.trackProductInsightsAgentResultClick({
74
+ itemId: clickedItem.id,
75
+ itemName: clickedItem.name,
76
+ ...(clickedItem.variationId && { variationId: clickedItem.variationId }),
77
+ question,
78
+ seedItemId: itemId,
79
+ seedItemName: itemName,
80
+ ...(variationId && { seedVariationId: variationId }),
81
+ ...(qnaResultId && { qnaResultId }),
82
+ ...(threadId && { threadId }),
83
+ position,
84
+ });
85
+ }, [tracker, itemId, itemName, variationId, threadId]);
86
+ return useMemo(() => ({
87
+ trackViews,
88
+ trackView,
89
+ trackOutOfView,
90
+ trackFocus,
91
+ trackQuestionClick,
92
+ trackQuestionSubmit,
93
+ trackAnswerView,
94
+ trackAnswerFeedback,
95
+ trackResultClick,
96
+ }), [
97
+ trackViews,
98
+ trackView,
99
+ trackOutOfView,
100
+ trackFocus,
101
+ trackQuestionClick,
102
+ trackQuestionSubmit,
103
+ trackAnswerView,
104
+ trackAnswerFeedback,
105
+ trackResultClick,
106
+ ]);
107
+ }
@@ -0,0 +1,89 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
3
+ export default function useViewportTracking({ tracking, questions, }) {
4
+ const nodeRef = useRef(null);
5
+ const observerRef = useRef(null);
6
+ const intervalRef = useRef(null);
7
+ const timespansRef = useRef([]);
8
+ const entryTimeRef = useRef(null);
9
+ const questionsRef = useRef(questions);
10
+ const trackingRef = useRef(tracking);
11
+ useEffect(() => {
12
+ const questionsJustArrived = questionsRef.current.length === 0 && questions.length > 0;
13
+ questionsRef.current = questions;
14
+ if (questionsJustArrived && entryTimeRef.current) {
15
+ trackingRef.current.trackView(questions);
16
+ }
17
+ }, [questions]);
18
+ useEffect(() => {
19
+ trackingRef.current = tracking;
20
+ }, [tracking]);
21
+ // Stable: only reads refs
22
+ const flush = useCallback(() => {
23
+ if (entryTimeRef.current) {
24
+ timespansRef.current.push({
25
+ start: entryTimeRef.current,
26
+ end: new Date().toISOString(),
27
+ });
28
+ entryTimeRef.current = null;
29
+ }
30
+ if (timespansRef.current.length > 0 && questionsRef.current.length > 0) {
31
+ trackingRef.current.trackViews(questionsRef.current, [...timespansRef.current]);
32
+ timespansRef.current = [];
33
+ }
34
+ }, []);
35
+ const setupObserver = useCallback((element) => {
36
+ const observer = new IntersectionObserver(([entry]) => {
37
+ if (entry.isIntersecting) {
38
+ entryTimeRef.current = new Date().toISOString();
39
+ if (questionsRef.current.length > 0) {
40
+ trackingRef.current.trackView(questionsRef.current);
41
+ }
42
+ }
43
+ else if (entryTimeRef.current) {
44
+ timespansRef.current.push({
45
+ start: entryTimeRef.current,
46
+ end: new Date().toISOString(),
47
+ });
48
+ entryTimeRef.current = null;
49
+ trackingRef.current.trackOutOfView();
50
+ }
51
+ }, { threshold: 0.5 });
52
+ observer.observe(element);
53
+ observerRef.current = observer;
54
+ intervalRef.current = setInterval(flush, FLUSH_INTERVAL_MS);
55
+ const handleVisibilityChange = () => {
56
+ if (document.visibilityState === 'hidden') {
57
+ flush();
58
+ }
59
+ };
60
+ document.addEventListener('visibilitychange', handleVisibilityChange);
61
+ return () => {
62
+ observer.disconnect();
63
+ if (intervalRef.current)
64
+ clearInterval(intervalRef.current);
65
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
66
+ flush();
67
+ };
68
+ }, [flush]);
69
+ const cleanupRef = useRef(null);
70
+ const containerRef = useCallback((node) => {
71
+ if (cleanupRef.current) {
72
+ cleanupRef.current();
73
+ cleanupRef.current = null;
74
+ }
75
+ nodeRef.current = node;
76
+ if (node) {
77
+ cleanupRef.current = setupObserver(node);
78
+ }
79
+ }, [setupObserver]);
80
+ useEffect(() => {
81
+ return () => {
82
+ if (cleanupRef.current) {
83
+ cleanupRef.current();
84
+ cleanupRef.current = null;
85
+ }
86
+ };
87
+ }, []);
88
+ return { containerRef };
89
+ }
@@ -1 +1 @@
1
- export default '2.8.0';
1
+ export default '2.10.0';
@@ -1,12 +1,14 @@
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;
8
8
  /** The product item ID to fetch insights for. */
9
9
  itemId: string;
10
+ /** The product display name, sent with tracking events. */
11
+ itemName: string;
10
12
  /** Thread ID for conversation context. Must be a valid UUID (e.g., "550e8400-e29b-41d4-a716-446655440000"). */
11
13
  threadId?: string;
12
14
  /** Optional variation ID for the product. */
@@ -21,6 +23,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
21
23
  componentOverrides?: CioPiaComponentOverrides;
22
24
  /** Formatter functions for transforming data before display. */
23
25
  formatters?: Formatters;
26
+ /** Props forwarded to the ProductCard rendered inside the carousel. */
27
+ productCardProps?: ProductCardDisplayProps;
24
28
  /** UI string translations for internationalization. */
25
29
  translations?: Translations;
26
30
  /** Parameters for the suggested questions request. */
@@ -5,6 +5,10 @@ interface PiaCustomCarouselProps {
5
5
  items: Array<Item>;
6
6
  componentOverrides?: CarouselOverrides<Item>;
7
7
  callbacks?: Callbacks;
8
+ onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
9
+ question?: string;
10
+ qnaResultId?: string;
11
+ priceCurrency?: string;
8
12
  }
9
- export default function PiaCustomCarousel({ items, componentOverrides, callbacks, }: PiaCustomCarouselProps): React.JSX.Element | null;
13
+ export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, priceCurrency, }: PiaCustomCarouselProps): React.JSX.Element | null;
10
14
  export {};
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ConversationEntry, Translations, Item, Callbacks, CioPiaComponentOverrides, DisclaimerPosition } from '../../types';
2
+ import { ConversationEntry, FeedbackType, Translations, Item, Callbacks, CioPiaComponentOverrides, DisclaimerPosition } from '../../types';
3
3
  export interface ConversationHistoryProps {
4
4
  conversationHistory: ConversationEntry[];
5
5
  isLoading: boolean;
@@ -22,5 +22,9 @@ export interface ConversationHistoryProps {
22
22
  translations?: Translations;
23
23
  callbacks?: Callbacks;
24
24
  componentOverrides?: CioPiaComponentOverrides;
25
+ handleFeedback?: (type: FeedbackType) => void;
26
+ onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
27
+ qnaResultId?: string;
28
+ priceCurrency?: string;
25
29
  }
26
- export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, }: 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;
@@ -1,11 +1,14 @@
1
1
  import React from 'react';
2
- import { Question } from '../../types';
2
+ import { FeedbackType, Item, Question } from '../../types';
3
3
  import { ConversationHistoryProps } from '../ConversationHistory/ConversationHistory';
4
4
  export interface PiaConversationProps extends ConversationHistoryProps {
5
5
  displayedQuestions: Question[];
6
6
  handleSubmitQuestion: (question: string) => void;
7
7
  handleQuestionClick: (question: string) => void;
8
8
  containerRef?: (node: HTMLDivElement | null) => void;
9
+ handleFeedback?: (type: FeedbackType) => void;
10
+ onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
11
+ qnaResultId?: string;
9
12
  onInputFocus?: () => void;
10
13
  }
11
- export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, 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;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { Callbacks, CioPiaComponentOverrides, DisclaimerPosition, Translations, Item } from '../../types';
2
+ import { Callbacks, CioPiaComponentOverrides, DisclaimerPosition, FeedbackType, Translations, Item } from '../../types';
3
3
  interface PiaInlineAnswerProps {
4
4
  currentAnswer: string;
5
5
  currentItems: Item[] | null;
@@ -9,6 +9,11 @@ interface PiaInlineAnswerProps {
9
9
  translations?: Translations;
10
10
  callbacks?: Callbacks;
11
11
  componentOverrides?: CioPiaComponentOverrides;
12
+ onFeedback?: (type: FeedbackType) => void;
13
+ onResultClick?: (item: Item, position: number, question: string, qnaResultId?: string) => void;
14
+ question?: string;
15
+ qnaResultId?: string;
16
+ priceCurrency?: string;
12
17
  }
13
- export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, }: 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;
14
19
  export {};
@@ -1,5 +1,6 @@
1
1
  export declare const DEMO_API_KEY = "key_13TO6qN9H3BP3Wrn";
2
2
  export declare const DEMO_ITEM_ID = "149100215";
3
+ export declare const DEMO_ITEM_NAME = "Ensure Original Nutrition Shake";
3
4
  export declare const DEMO_QUESTION = "What is the nutritional content of this shake?";
4
5
  export declare const DEMO_QUESTION_ALTERNATIVE_PRODUCTS = "What alternative products are available for this item?";
5
6
  export declare const MOCK_QUESTIONS: {
@@ -1,13 +1,9 @@
1
- import { ConstructorClientOptions, Nullable } from '@constructor-io/constructorio-client-javascript';
2
- import MockConstructorIOClient from './mocks/MockConstructorIOClient';
3
- /**
4
- * Uses MockConstructorIOClient for now, to be replaced with the actual Client JS object
5
- */
1
+ import ConstructorIOClient, { ConstructorClientOptions, Nullable } from '@constructor-io/constructorio-client-javascript';
6
2
  export type UseCioClientProps = {
7
3
  apiKey?: string;
8
- cioClient?: Nullable<MockConstructorIOClient>;
4
+ cioClient?: Nullable<ConstructorIOClient>;
9
5
  options?: Omit<ConstructorClientOptions, 'apiKey' | 'sendTrackingEvents' | 'version'>;
10
6
  };
11
- type UseCioClient = (props: UseCioClientProps) => Nullable<MockConstructorIOClient> | never;
7
+ type UseCioClient = (props: UseCioClientProps) => Nullable<ConstructorIOClient> | never;
12
8
  declare const useCioClient: UseCioClient;
13
9
  export default useCioClient;
@@ -1,3 +1,4 @@
1
+ import { Tracker } from '@constructor-io/constructorio-client-javascript/lib/types/constructorio';
1
2
  import { Formatters, SuggestedQuestionsParameters } from '../types';
2
3
  import MockConstructorIOClient from './mocks/MockConstructorIOClient';
3
4
  import { UseAnswerResultsReturn } from './useAnswerResults';
@@ -13,6 +14,9 @@ export interface UseCioPiaProps {
13
14
  formatImageUrl?: Formatters['formatImageUrl'];
14
15
  }
15
16
  export interface UseCioPiaReturn {
17
+ cioClient: {
18
+ tracker: Tracker;
19
+ };
16
20
  threadId: string;
17
21
  suggestedQuestions: UseSuggestedQuestionsReturn;
18
22
  answers: UseAnswerResultsReturn;