@constructor-io/constructorio-ui-pia 2.7.0 → 2.9.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 (43) hide show
  1. package/dist/constructorio-ui-pia-bundled.js +76 -73
  2. package/lib/cjs/components/CioPia/CioPia.js +27 -5
  3. package/lib/cjs/components/CioPia/PiaCustomCarousel.js +6 -2
  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/mocks/MockConstructorIOClient.js +1 -2
  9. package/lib/cjs/hooks/mocks/agent.js +16 -1
  10. package/lib/cjs/hooks/useCioClient.js +2 -2
  11. package/lib/cjs/hooks/useCioPia.js +12 -3
  12. package/lib/cjs/hooks/useConversation.js +32 -9
  13. package/lib/cjs/hooks/useTracking.js +71 -0
  14. package/lib/cjs/hooks/useViewportTracking.js +92 -0
  15. package/lib/cjs/version.js +1 -1
  16. package/lib/mjs/components/CioPia/CioPia.js +26 -5
  17. package/lib/mjs/components/CioPia/PiaCustomCarousel.js +6 -2
  18. package/lib/mjs/components/ConversationHistory/ConversationHistory.js +3 -3
  19. package/lib/mjs/components/PiaConversation/PiaConversation.js +2 -2
  20. package/lib/mjs/components/PiaInlineAnswer/PiaInlineAnswer.js +3 -3
  21. package/lib/mjs/constants.js +1 -0
  22. package/lib/mjs/hooks/mocks/MockConstructorIOClient.js +5 -2
  23. package/lib/mjs/hooks/mocks/agent.js +16 -1
  24. package/lib/mjs/hooks/useCioClient.js +2 -2
  25. package/lib/mjs/hooks/useCioPia.js +13 -4
  26. package/lib/mjs/hooks/useConversation.js +24 -2
  27. package/lib/mjs/hooks/useTracking.js +107 -0
  28. package/lib/mjs/hooks/useViewportTracking.js +89 -0
  29. package/lib/mjs/version.js +1 -1
  30. package/lib/types/components/CioPia/CioPia.d.ts +2 -0
  31. package/lib/types/components/CioPia/PiaCustomCarousel.d.ts +4 -1
  32. package/lib/types/components/ConversationHistory/ConversationHistory.d.ts +5 -2
  33. package/lib/types/components/PiaConversation/PiaConversation.d.ts +5 -2
  34. package/lib/types/components/PiaInlineAnswer/PiaInlineAnswer.d.ts +6 -2
  35. package/lib/types/constants.d.ts +1 -0
  36. package/lib/types/hooks/useCioClient.d.ts +3 -7
  37. package/lib/types/hooks/useCioPia.d.ts +4 -0
  38. package/lib/types/hooks/useConversation.d.ts +5 -2
  39. package/lib/types/hooks/useTracking.d.ts +27 -0
  40. package/lib/types/hooks/useViewportTracking.d.ts +10 -0
  41. package/lib/types/utils/transformers.d.ts +1 -1
  42. package/lib/types/version.d.ts +1 -1
  43. package/package.json +6 -3
@@ -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, }) {
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 })),
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, }) {
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 }),
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, }) {
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 })),
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 = [
@@ -28,8 +28,11 @@ class MockConstructorIOClient {
28
28
  this.recommendations = cioClient.recommendations;
29
29
  this.tracker = cioClient.tracker;
30
30
  this.quizzes = cioClient.quizzes;
31
- // Use the mock agent instead of the one from the client
32
- this.agent = new MockAgent(this.options);
31
+ this.agent = new MockAgent({
32
+ ...this.options,
33
+ clientId: options.clientId,
34
+ sessionId: options.sessionId,
35
+ });
33
36
  }
34
37
  }
35
38
  export default MockConstructorIOClient;
@@ -1,6 +1,6 @@
1
1
  // Create URL for PIA API
2
2
  function createAgentUrl({ itemId, threadId, variationId, question, isStreaming = false, options, parameters = {}, }) {
3
- const { apiKey, agentServiceUrl } = options;
3
+ const { apiKey, agentServiceUrl, clientId, sessionId, userId, segments, version } = options;
4
4
  if (!agentServiceUrl)
5
5
  throw new Error('Agent service URL is required');
6
6
  let baseUrl = `${agentServiceUrl}/v1/item_questions`;
@@ -19,6 +19,21 @@ function createAgentUrl({ itemId, threadId, variationId, question, isStreaming =
19
19
  if (variationId) {
20
20
  url.searchParams.append('variation_id', variationId);
21
21
  }
22
+ if (clientId) {
23
+ url.searchParams.append('i', clientId);
24
+ }
25
+ if (sessionId != null) {
26
+ url.searchParams.append('s', String(sessionId));
27
+ }
28
+ if (userId) {
29
+ url.searchParams.append('ui', userId);
30
+ }
31
+ if (version) {
32
+ url.searchParams.append('c', version);
33
+ }
34
+ if (segments?.length) {
35
+ segments.forEach((segment) => url.searchParams.append('us', segment));
36
+ }
22
37
  // Any additional parameters
23
38
  Object.entries(parameters).forEach(([key, value]) => {
24
39
  if (value !== undefined) {
@@ -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.7.0';
1
+ export default '2.9.0';
@@ -7,6 +7,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
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. */
@@ -5,6 +5,9 @@ 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;
8
11
  }
9
- export default function PiaCustomCarousel({ items, componentOverrides, callbacks, }: PiaCustomCarouselProps): React.JSX.Element | null;
12
+ export default function PiaCustomCarousel({ items, componentOverrides, callbacks, onResultClick, question, qnaResultId, }: PiaCustomCarouselProps): React.JSX.Element | null;
10
13
  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,8 @@ 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;
25
28
  }
26
- export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, }: ConversationHistoryProps): React.JSX.Element;
29
+ export default function ConversationHistory({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, handleFeedback, onResultClick, qnaResultId, }: 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, }: 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,10 @@ 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;
12
16
  }
13
- export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, }: PiaInlineAnswerProps): React.JSX.Element;
17
+ export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, onFeedback, onResultClick, question, qnaResultId, }: PiaInlineAnswerProps): React.JSX.Element;
14
18
  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;
@@ -1,10 +1,12 @@
1
- import { Callbacks, ConversationEntry, PiaCallbackContext, Question, Item } from '../types';
1
+ import { Callbacks, ConversationEntry, FeedbackType, PiaCallbackContext, Question, Item } from '../types';
2
2
  import { UseCioPiaReturn } from './useCioPia';
3
+ import { UseTrackingReturn } from './useTracking';
3
4
  export interface UseConversationProps {
4
5
  pia: UseCioPiaReturn;
5
6
  itemId: string;
6
7
  isConversation: boolean;
7
8
  callbacks?: Callbacks;
9
+ tracking?: UseTrackingReturn;
8
10
  }
9
11
  export interface UseConversationReturn {
10
12
  currentQuestion: string;
@@ -18,6 +20,7 @@ export interface UseConversationReturn {
18
20
  handleSubmitQuestion: (question: string) => void;
19
21
  handleQuestionClick: (question: string) => void;
20
22
  handleInputFocus: () => void;
23
+ handleFeedback: (type: FeedbackType) => void;
21
24
  resetState: () => void;
22
25
  }
23
- export default function useConversation({ pia, itemId, isConversation, callbacks, }: UseConversationProps): UseConversationReturn;
26
+ export default function useConversation({ pia, itemId, isConversation, callbacks, tracking, }: UseConversationProps): UseConversationReturn;
@@ -0,0 +1,27 @@
1
+ import { Tracker } from '@constructor-io/constructorio-client-javascript/lib/types/constructorio';
2
+ import { FeedbackType, Item, Question, GetAnswerResultsResponse } from '../types';
3
+ export interface TimeSpan {
4
+ start: string;
5
+ end: string;
6
+ }
7
+ export interface UseTrackingProps {
8
+ cioClient?: {
9
+ tracker: Tracker;
10
+ };
11
+ itemId: string;
12
+ itemName: string;
13
+ variationId?: string;
14
+ threadId?: string;
15
+ }
16
+ export interface UseTrackingReturn {
17
+ trackViews: (questions: Question[], viewTimespans: TimeSpan[]) => void;
18
+ trackView: (questions: Question[]) => void;
19
+ trackOutOfView: () => void;
20
+ trackFocus: () => void;
21
+ trackQuestionClick: (question: string) => void;
22
+ trackQuestionSubmit: (question: string) => void;
23
+ trackAnswerView: (question: string, answerData: GetAnswerResultsResponse, items?: Item[] | null) => void;
24
+ trackAnswerFeedback: (feedbackType: FeedbackType, qnaResultId?: string) => void;
25
+ trackResultClick: (clickedItem: Item, position: number, question: string, qnaResultId?: string) => void;
26
+ }
27
+ export default function useTracking({ cioClient, itemId, itemName, variationId, threadId, }: UseTrackingProps): UseTrackingReturn;
@@ -0,0 +1,10 @@
1
+ import { Question } from '../types';
2
+ import { UseTrackingReturn } from './useTracking';
3
+ export interface UseViewportTrackingProps {
4
+ tracking: UseTrackingReturn;
5
+ questions: Question[];
6
+ }
7
+ export interface UseViewportTrackingReturn {
8
+ containerRef: (node: HTMLDivElement | null) => void;
9
+ }
10
+ export default function useViewportTracking({ tracking, questions, }: UseViewportTrackingProps): UseViewportTrackingReturn;
@@ -1,5 +1,5 @@
1
1
  import { Nullable } from '@constructor-io/constructorio-client-javascript';
2
- import { Formatters, Item, ApiItem } from '../types';
2
+ import { ApiItem, Formatters, Item } 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.
@@ -1,2 +1,2 @@
1
- declare const _default: "2.7.0";
1
+ declare const _default: "2.9.0";
2
2
  export default _default;