@constructor-io/constructorio-ui-pia 2.3.0 → 2.6.2

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 (54) hide show
  1. package/dist/constructorio-ui-pia-bundled.js +77 -21
  2. package/lib/cjs/components/Answer/Answer.js +2 -1
  3. package/lib/cjs/components/CioPia/CioPia.js +12 -16
  4. package/lib/cjs/components/CioPia/PiaCustomCarousel.js +15 -1
  5. package/lib/cjs/components/Input/Input.js +2 -2
  6. package/lib/cjs/components/PiaConversation/PiaConversation.js +4 -4
  7. package/lib/cjs/components/PiaConversation/PiaModal.js +8 -4
  8. package/lib/cjs/components/PiaInlineAnswer/PiaInlineAnswer.js +18 -0
  9. package/lib/cjs/constants.js +0 -1
  10. package/lib/cjs/hooks/useAnswerResults.js +2 -4
  11. package/lib/cjs/hooks/useCioPia.js +5 -1
  12. package/lib/cjs/hooks/useConversation.js +61 -15
  13. package/lib/cjs/hooks/useViewportCallbacks.js +50 -0
  14. package/lib/cjs/index.js +5 -1
  15. package/lib/cjs/stories/assets/storybook-styles.css +89 -0
  16. package/lib/cjs/utils/contentTransformers.js +22 -0
  17. package/lib/cjs/utils/index.js +4 -0
  18. package/lib/cjs/utils/styleHelpers.js +0 -1
  19. package/lib/cjs/utils/translate.js +0 -1
  20. package/lib/cjs/version.js +1 -1
  21. package/lib/mjs/components/Answer/Answer.js +2 -1
  22. package/lib/mjs/components/CioPia/CioPia.js +12 -16
  23. package/lib/mjs/components/CioPia/PiaCustomCarousel.js +27 -2
  24. package/lib/mjs/components/Input/Input.js +2 -2
  25. package/lib/mjs/components/PiaConversation/PiaConversation.js +4 -4
  26. package/lib/mjs/components/PiaConversation/PiaModal.js +8 -4
  27. package/lib/mjs/components/PiaInlineAnswer/PiaInlineAnswer.js +14 -0
  28. package/lib/mjs/constants.js +0 -1
  29. package/lib/mjs/hooks/useAnswerResults.js +3 -5
  30. package/lib/mjs/hooks/useCioPia.js +5 -1
  31. package/lib/mjs/hooks/useConversation.js +60 -15
  32. package/lib/mjs/hooks/useViewportCallbacks.js +45 -0
  33. package/lib/mjs/index.js +2 -0
  34. package/lib/mjs/stories/assets/storybook-styles.css +89 -0
  35. package/lib/mjs/utils/contentTransformers.js +16 -0
  36. package/lib/mjs/utils/index.js +1 -0
  37. package/lib/mjs/utils/styleHelpers.js +0 -1
  38. package/lib/mjs/utils/translate.js +0 -1
  39. package/lib/mjs/version.js +1 -1
  40. package/lib/types/components/CioPia/CioPia.d.ts +12 -14
  41. package/lib/types/components/Input/Input.d.ts +2 -1
  42. package/lib/types/components/PiaConversation/PiaConversation.d.ts +4 -1
  43. package/lib/types/components/PiaConversation/PiaModal.d.ts +4 -1
  44. package/lib/types/components/PiaInlineAnswer/PiaInlineAnswer.d.ts +14 -0
  45. package/lib/types/hooks/mocks/MockConstructorIOClient.d.ts +1 -1
  46. package/lib/types/hooks/useCioPia.d.ts +1 -0
  47. package/lib/types/hooks/useConversation.d.ts +4 -1
  48. package/lib/types/hooks/useViewportCallbacks.d.ts +9 -0
  49. package/lib/types/index.d.ts +1 -0
  50. package/lib/types/types.d.ts +20 -4
  51. package/lib/types/utils/contentTransformers.d.ts +2 -0
  52. package/lib/types/utils/index.d.ts +1 -0
  53. package/lib/types/version.d.ts +1 -1
  54. package/package.json +20 -16
@@ -1,16 +1,14 @@
1
1
  import React from 'react';
2
2
  import { RenderPropsWrapper, } from '@constructor-io/constructorio-ui-components';
3
- import Disclaimer from './Disclaimer';
4
3
  import Input from '../Input/Input';
5
4
  import SuggestedQuestionsContainer from '../SuggestedQuestionsContainer/SuggestedQuestionsContainer';
6
- import Answer from '../Answer/Answer';
7
- import Feedback from '../Feedback/Feedback';
8
5
  import useCioPia from '../../hooks/useCioPia';
9
6
  import useConversation from '../../hooks/useConversation';
7
+ import useViewportCallbacks from '../../hooks/useViewportCallbacks';
10
8
  import ErrorBlock from '../Error/ErrorBlock';
11
9
  import LoadingSkeleton from '../LoadingSkeleton/LoadingSkeleton';
12
10
  import { translate } from '../../utils/translate';
13
- import PiaCustomCarousel from './PiaCustomCarousel';
11
+ import PiaInlineAnswer from '../PiaInlineAnswer/PiaInlineAnswer';
14
12
  import PiaModal from '../PiaConversation/PiaModal';
15
13
  import PiaConversation from '../PiaConversation/PiaConversation';
16
14
  export default function CioPia(props) {
@@ -26,7 +24,8 @@ export default function CioPia(props) {
26
24
  suggestedQuestionsParameters,
27
25
  formatImageUrl: formatters?.formatImageUrl,
28
26
  });
29
- const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, handleSubmitQuestion, resetState, } = useConversation({ pia, itemId, isConversation, callbacks });
27
+ const { currentQuestion, displayedQuestions, conversationHistory, currentAnswer, currentItems, isLoading, error, context, handleSubmitQuestion, handleQuestionClick, handleInputFocus, resetState, } = useConversation({ pia, itemId, isConversation, callbacks });
28
+ const { containerRef } = useViewportCallbacks({ callbacks, context });
30
29
  const renderProps = {
31
30
  items: currentItems,
32
31
  isLoading,
@@ -51,27 +50,24 @@ export default function CioPia(props) {
51
50
  componentOverrides,
52
51
  displayedQuestions,
53
52
  handleSubmitQuestion,
53
+ handleQuestionClick,
54
+ containerRef,
55
+ onInputFocus: handleInputFocus,
54
56
  };
55
57
  if (type === 'modal') {
56
- return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onClose: resetState },
58
+ return (React.createElement(PiaModal, { initialQuestions: pia.suggestedQuestions.data, handleSubmitQuestion: handleSubmitQuestion, handleQuestionClick: handleQuestionClick, containerRef: containerRef, isLoading: isLoading, componentOverrides: componentOverrides, translations: translations, onInputFocus: handleInputFocus, onClose: resetState },
57
59
  React.createElement(PiaConversation, { ...conversationHistoryProps })));
58
60
  }
59
61
  if (isConversation)
60
62
  return React.createElement(PiaConversation, { ...conversationHistoryProps });
61
- const disclaimer = (React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer }));
62
63
  // Default inline mode
63
- return (React.createElement("div", { className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
64
+ return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
64
65
  React.createElement(RenderPropsWrapper, { props: renderProps, override: children || componentOverrides?.reactNode },
65
66
  React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
66
- React.createElement(Input, { onSubmit: handleSubmitQuestion, value: currentQuestion, translations: translations }),
67
+ React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: handleInputFocus, value: currentQuestion, translations: translations }),
67
68
  isLoading && React.createElement(LoadingSkeleton, null),
68
69
  !isLoading && error && React.createElement(ErrorBlock, { message: error?.message || 'Unexpected error' }),
69
70
  !isLoading && !error && (React.createElement(React.Fragment, null,
70
- currentAnswer && (React.createElement("div", { className: 'cio-pia-answer-container' },
71
- disclaimerPosition === 'top' && disclaimer,
72
- React.createElement(Answer, { text: currentAnswer, componentOverride: componentOverrides?.answer }),
73
- currentItems && (React.createElement(PiaCustomCarousel, { items: currentItems, componentOverrides: componentOverrides?.carousel, callbacks: callbacks })),
74
- showFeedback && (React.createElement(Feedback, { translations: translations, onFeedback: callbacks?.onFeedback, componentOverride: componentOverrides?.feedback })),
75
- disclaimerPosition === 'bottom' && disclaimer)),
76
- React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleSubmitQuestion, componentOverride: componentOverrides?.suggestedQuestions }))))));
71
+ currentAnswer && (React.createElement(PiaInlineAnswer, { currentAnswer: currentAnswer, currentItems: currentItems, showFeedback: showFeedback, learnMoreUrl: learnMoreUrl, disclaimerPosition: disclaimerPosition, translations: translations, callbacks: callbacks, componentOverrides: componentOverrides })),
72
+ React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions }))))));
77
73
  }
@@ -1,5 +1,12 @@
1
- import React, { useCallback, useRef, useEffect } from 'react';
1
+ import React, { useCallback, useRef, useEffect, useMemo } from 'react';
2
2
  import { Carousel, CIO_EVENTS, } from '@constructor-io/constructorio-ui-components';
3
+ import { sanitizeHtml } from '../../utils/contentTransformers';
4
+ function HtmlDescription({ product }) {
5
+ const { description } = product;
6
+ if (!description)
7
+ return null;
8
+ return (React.createElement("p", { className: 'cio-product-card-description', dangerouslySetInnerHTML: { __html: sanitizeHtml(description) } }));
9
+ }
3
10
  export default function PiaCustomCarousel({ items, componentOverrides, callbacks, }) {
4
11
  const wrapperRef = useRef(null);
5
12
  // Determine to use user-defined click handler or default behavior
@@ -28,10 +35,28 @@ export default function PiaCustomCarousel({ items, componentOverrides, callbacks
28
35
  el.removeEventListener(CIO_EVENTS.productCard.click, handleClick);
29
36
  };
30
37
  }, [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]);
31
56
  // If there are no items, do not render the carousel
32
57
  if (items.length === 0) {
33
58
  return null;
34
59
  }
35
60
  return (React.createElement("div", { ref: wrapperRef },
36
- React.createElement(Carousel, { items: items, componentOverrides: componentOverrides })));
61
+ React.createElement(Carousel, { items: items, componentOverrides: mergedOverrides })));
37
62
  }
@@ -5,7 +5,7 @@ function SendIcon() {
5
5
  React.createElement("g", { id: '.icon-send' },
6
6
  React.createElement("path", { id: 'Vector', d: 'M14.8934 7.09533L14.8884 7.09314L1.54219 1.55752C1.42993 1.51053 1.30778 1.49211 1.18666 1.50388C1.06554 1.51566 0.949225 1.55728 0.848125 1.62502C0.741311 1.69501 0.653573 1.79045 0.592805 1.90277C0.532037 2.01509 0.500145 2.14075 0.5 2.26845V5.80877C0.50006 5.98335 0.561019 6.15243 0.67237 6.28689C0.783722 6.42134 0.938491 6.51274 1.11 6.54533L8.38906 7.89127C8.41767 7.89669 8.44348 7.91193 8.46205 7.93435C8.48062 7.95677 8.49078 7.98497 8.49078 8.01408C8.49078 8.04319 8.48062 8.07139 8.46205 8.09381C8.44348 8.11623 8.41767 8.13147 8.38906 8.13689L1.11031 9.48283C0.938851 9.51533 0.784092 9.60661 0.67269 9.74094C0.561288 9.87528 0.500219 10.0443 0.5 10.2188V13.7597C0.499917 13.8816 0.530111 14.0017 0.587871 14.1091C0.645632 14.2165 0.729152 14.3079 0.830938 14.375C0.953375 14.4564 1.09706 14.4998 1.24406 14.5C1.34626 14.4999 1.4474 14.4794 1.54156 14.4397L14.8875 8.93564L14.8934 8.93283C15.0731 8.85563 15.2262 8.72745 15.3337 8.56415C15.4413 8.40086 15.4986 8.20961 15.4986 8.01408C15.4986 7.81855 15.4413 7.6273 15.3337 7.46401C15.2262 7.30071 15.0731 7.17253 14.8934 7.09533Z', fill: 'white' }))));
7
7
  }
8
- function Input({ value: providedValue, placeholder, disabled = false, onSubmit, translations, }) {
8
+ function Input({ value: providedValue, placeholder, disabled = false, onSubmit, onFocus, translations, }) {
9
9
  const [value, setValue] = useState(providedValue || '');
10
10
  useEffect(() => {
11
11
  if (providedValue) {
@@ -29,7 +29,7 @@ function Input({ value: providedValue, placeholder, disabled = false, onSubmit,
29
29
  ? translate('Ask anything', translations)
30
30
  : (placeholder ?? translate('Ask anything'));
31
31
  return (React.createElement("div", { className: 'cio-pia-input-container' },
32
- React.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, placeholder: resolvedPlaceholder, disabled: disabled, className: 'cio-pia-input' }),
32
+ React.createElement("input", { type: 'text', value: value, onChange: (e) => setValue(e.target.value), onKeyDown: handleSubmitOnEnter, onFocus: onFocus, placeholder: resolvedPlaceholder, disabled: disabled, className: 'cio-pia-input' }),
33
33
  React.createElement("button", { type: 'button', onClick: handleSubmit, className: 'cio-pia-send-button', disabled: disabled },
34
34
  translate('Send', translations),
35
35
  React.createElement(SendIcon, null))));
@@ -4,13 +4,13 @@ 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, }) {
7
+ export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, onInputFocus, }) {
8
8
  const hasHistory = conversationHistory.length > 0;
9
- return (React.createElement("div", { className: 'cio-pia-container cio-pia-conversation', "data-testid": 'cio-pia-container' },
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
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 }),
12
12
  React.createElement("div", { className: 'cio-pia-conversation-footer' },
13
13
  isLoading && !error && React.createElement(SuggestedQuestionsSkeleton, null),
14
- !isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleSubmitQuestion, componentOverride: componentOverrides?.suggestedQuestions })),
15
- React.createElement(Input, { onSubmit: handleSubmitQuestion, disabled: isLoading, translations: translations }))));
14
+ !isLoading && !error && (React.createElement(SuggestedQuestionsContainer, { questions: displayedQuestions, onQuestionClick: handleQuestionClick, componentOverride: componentOverrides?.suggestedQuestions })),
15
+ React.createElement(Input, { onSubmit: handleSubmitQuestion, onFocus: onInputFocus, disabled: isLoading, translations: translations }))));
16
16
  }
@@ -7,7 +7,7 @@ function CloseIcon() {
7
7
  return (React.createElement("svg", { width: '20', height: '20', viewBox: '0 0 20 20', fill: 'none', xmlns: 'http://www.w3.org/2000/svg' },
8
8
  React.createElement("path", { d: 'M15.8334 5.34166L14.6584 4.16666L10.0001 8.82499L5.34175 4.16666L4.16675 5.34166L8.82508 9.99999L4.16675 14.6583L5.34175 15.8333L10.0001 11.175L14.6584 15.8333L15.8334 14.6583L11.1751 9.99999L15.8334 5.34166Z', fill: 'currentColor' })));
9
9
  }
10
- export default function PiaModal({ initialQuestions, handleSubmitQuestion, isLoading, componentOverrides, translations, onClose, children, }) {
10
+ export default function PiaModal({ initialQuestions, handleSubmitQuestion, handleQuestionClick, containerRef, isLoading, componentOverrides, translations, onInputFocus, onClose, children, }) {
11
11
  const [isOpen, setIsOpen] = useState(false);
12
12
  const dialogRef = useRef(null);
13
13
  const openModal = useCallback(() => {
@@ -56,11 +56,15 @@ export default function PiaModal({ initialQuestions, handleSubmitQuestion, isLoa
56
56
  handleSubmitQuestion(question);
57
57
  openModal();
58
58
  }, [handleSubmitQuestion, openModal]);
59
- return (React.createElement("div", { className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
59
+ const handleQuestionClicked = useCallback((question) => {
60
+ handleQuestionClick(question);
61
+ openModal();
62
+ }, [handleQuestionClick, openModal]);
63
+ return (React.createElement("div", { ref: containerRef, className: 'cio-pia-container', "data-testid": 'cio-pia-container' },
60
64
  React.createElement("p", { className: 'cio-pia-title', "data-testid": 'cio-pia-title' }, translate('Any questions about this product?', translations)),
61
65
  React.createElement("div", { className: 'cio-pia-conversation-footer' },
62
- initialQuestions.length > 0 && (React.createElement(SuggestedQuestionsContainer, { questions: initialQuestions, onQuestionClick: handleQuestion, componentOverride: componentOverrides?.suggestedQuestions })),
63
- React.createElement(Input, { onSubmit: handleQuestion, disabled: isLoading || isOpen, translations: translations })),
66
+ initialQuestions.length > 0 && (React.createElement(SuggestedQuestionsContainer, { questions: initialQuestions, onQuestionClick: handleQuestionClicked, componentOverride: componentOverrides?.suggestedQuestions })),
67
+ React.createElement(Input, { onSubmit: handleQuestion, onFocus: onInputFocus, disabled: isLoading || isOpen, translations: translations })),
64
68
  React.createElement("dialog", { ref: dialogRef, className: 'cio-pia-modal', "aria-labelledby": 'cio-pia-modal-title' },
65
69
  React.createElement("div", { className: 'cio-pia-modal-content' },
66
70
  React.createElement("div", { className: 'cio-pia-modal-header' },
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import Answer from '../Answer/Answer';
3
+ import Feedback from '../Feedback/Feedback';
4
+ import Disclaimer from '../CioPia/Disclaimer';
5
+ import PiaCustomCarousel from '../CioPia/PiaCustomCarousel';
6
+ export default function PiaInlineAnswer({ currentAnswer, currentItems, showFeedback, learnMoreUrl, disclaimerPosition = 'bottom', translations, callbacks, componentOverrides, }) {
7
+ const disclaimer = (React.createElement(Disclaimer, { learnMoreUrl: learnMoreUrl, translations: translations, componentOverride: componentOverrides?.disclaimer }));
8
+ return (React.createElement("div", { className: 'cio-pia-answer-container' },
9
+ disclaimerPosition === 'top' && disclaimer,
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 })),
13
+ disclaimerPosition === 'bottom' && disclaimer));
14
+ }
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line import/prefer-default-export
2
1
  export const DEMO_API_KEY = 'key_13TO6qN9H3BP3Wrn';
3
2
  export const DEMO_ITEM_ID = '149100215';
4
3
  export const DEMO_QUESTION = 'What is the nutritional content of this shake?';
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react';
1
+ import { useCallback, useMemo, useState } from 'react';
2
2
  import { transformResultItem } from '../utils/transformers';
3
3
  const extractAndTransformItems = (data, formatImageUrl) => {
4
4
  if (!data?.item_results?.response?.results) {
@@ -24,9 +24,9 @@ const fetchAnswerResults = async ({ client, itemId, question, variationId, threa
24
24
  };
25
25
  export default function useAnswerResults({ itemId, variationId, threadId, cioClient, formatImageUrl, }) {
26
26
  const [answerResults, setAnswerResults] = useState(null);
27
- const [items, setItems] = useState(null);
28
27
  const [isLoading, setIsLoading] = useState(false);
29
28
  const [error, setError] = useState(null);
29
+ const items = useMemo(() => extractAndTransformItems(answerResults, formatImageUrl), [answerResults, formatImageUrl]);
30
30
  const fetchResult = useCallback((question) => {
31
31
  if (!cioClient)
32
32
  return;
@@ -35,18 +35,16 @@ export default function useAnswerResults({ itemId, variationId, threadId, cioCli
35
35
  fetchAnswerResults({ client: cioClient, itemId, question, variationId, threadId })
36
36
  .then((fetchedAnswerResults) => {
37
37
  setAnswerResults(fetchedAnswerResults);
38
- setItems(extractAndTransformItems(fetchedAnswerResults, formatImageUrl));
39
38
  setError(null);
40
39
  })
41
40
  .catch((err) => {
42
41
  setError(err instanceof Error ? err : new Error('Error fetching answer'));
43
42
  setAnswerResults(null);
44
- setItems(null);
45
43
  })
46
44
  .finally(() => {
47
45
  setIsLoading(false);
48
46
  });
49
- }, [cioClient, itemId, variationId, threadId, formatImageUrl]);
47
+ }, [cioClient, itemId, variationId, threadId]);
50
48
  return {
51
49
  data: answerResults,
52
50
  items,
@@ -1,8 +1,11 @@
1
+ import { useState } from 'react';
1
2
  import useAnswerResults from './useAnswerResults';
2
3
  import useCioClient from './useCioClient';
3
4
  import useSuggestedQuestions from './useSuggestedQuestions';
4
5
  export default function useCioPia(props) {
5
- const { apiKey, itemId, variationId, threadId, cioClient: providedClient, suggestedQuestionsParameters, formatImageUrl, } = props;
6
+ const { apiKey, itemId, variationId, threadId: providedThreadId, cioClient: providedClient, suggestedQuestionsParameters, formatImageUrl, } = props;
7
+ const [generatedThreadId] = useState(() => crypto.randomUUID());
8
+ const threadId = providedThreadId || generatedThreadId;
6
9
  const defaultClient = useCioClient({ apiKey });
7
10
  const client = providedClient || defaultClient;
8
11
  const suggestedQuestions = useSuggestedQuestions({
@@ -20,6 +23,7 @@ export default function useCioPia(props) {
20
23
  formatImageUrl,
21
24
  });
22
25
  return {
26
+ threadId,
23
27
  suggestedQuestions,
24
28
  answers,
25
29
  };
@@ -1,22 +1,45 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  export default function useConversation({ pia, itemId, isConversation, callbacks, }) {
3
- const { suggestedQuestions, answers } = pia;
3
+ const { suggestedQuestions, answers, threadId } = pia;
4
4
  const { getAnswer } = answers;
5
+ const context = useMemo(() => ({ itemId, threadId }), [itemId, threadId]);
5
6
  const [currentQuestion, setCurrentQuestion] = useState('');
6
7
  const [displayedQuestions, setDisplayedQuestions] = useState([]);
7
8
  const [conversationHistory, setConversationHistory] = useState([]);
8
9
  const entryIdRef = useRef(0);
9
10
  const prevAnswerValueRef = useRef(answers.data?.value);
10
- const handleSubmitQuestion = useCallback((question) => {
11
- callbacks?.onQuestionSubmit?.(question);
11
+ const callbacksRef = useRef(callbacks);
12
+ const contextRef = useRef(context);
13
+ const lastQuestionRef = useRef('');
14
+ const lastSourceRef = useRef('user');
15
+ useEffect(() => {
16
+ callbacksRef.current = callbacks;
17
+ }, [callbacks]);
18
+ useEffect(() => {
19
+ contextRef.current = context;
20
+ }, [context]);
21
+ const submitQuestion = useCallback((question, source) => {
22
+ lastSourceRef.current = source;
23
+ lastQuestionRef.current = question;
12
24
  setCurrentQuestion(question);
13
25
  getAnswer(question);
14
26
  if (isConversation) {
15
27
  entryIdRef.current += 1;
16
28
  const id = entryIdRef.current;
17
- setConversationHistory((prev) => [...prev, { id, question, answer: '' }]);
29
+ setConversationHistory((prev) => [...prev, { id, question, answer: '', source }]);
18
30
  }
19
- }, [getAnswer, isConversation, callbacks]);
31
+ }, [getAnswer, isConversation]);
32
+ const handleSubmitQuestion = useCallback((question) => {
33
+ callbacksRef.current?.onQuestionSubmit?.(question, contextRef.current, 'user');
34
+ submitQuestion(question, 'user');
35
+ }, [submitQuestion]);
36
+ const handleQuestionClick = useCallback((question) => {
37
+ callbacksRef.current?.onQuestionSubmit?.(question, contextRef.current, 'suggestion');
38
+ submitQuestion(question, 'suggestion');
39
+ }, [submitQuestion]);
40
+ const handleInputFocus = useCallback(() => {
41
+ callbacksRef.current?.onFocus?.(contextRef.current);
42
+ }, []);
20
43
  const resetState = useCallback(() => {
21
44
  setCurrentQuestion('');
22
45
  setDisplayedQuestions(suggestedQuestions.data);
@@ -38,22 +61,41 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
38
61
  }, [answers.data]);
39
62
  useEffect(() => {
40
63
  const answerValue = answers.data?.value ?? '';
41
- if (!isConversation || !answerValue)
64
+ if (!answerValue)
42
65
  return;
43
66
  if (answerValue === prevAnswerValueRef.current)
44
67
  return;
45
68
  prevAnswerValueRef.current = answerValue;
46
- setConversationHistory((prev) => {
47
- if (prev.length === 0)
48
- return prev;
49
- const updated = [...prev];
50
- updated[updated.length - 1] = {
51
- ...updated[updated.length - 1],
69
+ const answerThreadId = answers.data?.thread_id;
70
+ const qnaResultId = answers.data?.qna_result_id;
71
+ if (isConversation) {
72
+ setConversationHistory((prev) => {
73
+ if (prev.length === 0)
74
+ return prev;
75
+ const updated = [...prev];
76
+ updated[updated.length - 1] = {
77
+ ...updated[updated.length - 1],
78
+ answer: answerValue,
79
+ items: answers.items,
80
+ threadId: answerThreadId,
81
+ qnaResultId,
82
+ };
83
+ callbacksRef.current?.onAnswer?.(updated, contextRef.current);
84
+ return updated;
85
+ });
86
+ }
87
+ else {
88
+ const entry = {
89
+ id: entryIdRef.current,
90
+ question: lastQuestionRef.current,
52
91
  answer: answerValue,
92
+ source: lastSourceRef.current,
53
93
  items: answers.items,
94
+ threadId: answerThreadId,
95
+ qnaResultId,
54
96
  };
55
- return updated;
56
- });
97
+ callbacksRef.current?.onAnswer?.([entry], contextRef.current);
98
+ }
57
99
  }, [isConversation, answers.data, answers.items]);
58
100
  const currentAnswer = answers.data?.value ?? '';
59
101
  const currentItems = answers.items ?? null;
@@ -67,7 +109,10 @@ export default function useConversation({ pia, itemId, isConversation, callbacks
67
109
  currentItems,
68
110
  isLoading,
69
111
  error,
112
+ context,
70
113
  handleSubmitQuestion,
114
+ handleQuestionClick,
115
+ handleInputFocus,
71
116
  resetState,
72
117
  };
73
118
  }
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ export default function useViewportCallbacks({ callbacks, context, }) {
3
+ const callbacksRef = useRef(callbacks);
4
+ const contextRef = useRef(context);
5
+ const hasBeenVisibleRef = useRef(false);
6
+ const hasFiredOutOfViewRef = useRef(false);
7
+ const observerRef = useRef(null);
8
+ useEffect(() => {
9
+ callbacksRef.current = callbacks;
10
+ }, [callbacks]);
11
+ useEffect(() => {
12
+ contextRef.current = context;
13
+ }, [context]);
14
+ useEffect(() => {
15
+ return () => {
16
+ observerRef.current?.disconnect();
17
+ };
18
+ }, []);
19
+ const containerRef = useCallback((node) => {
20
+ if (observerRef.current) {
21
+ observerRef.current.disconnect();
22
+ observerRef.current = null;
23
+ }
24
+ if (!node)
25
+ return;
26
+ hasBeenVisibleRef.current = false;
27
+ hasFiredOutOfViewRef.current = false;
28
+ const observer = new IntersectionObserver(([entry]) => {
29
+ if (entry.isIntersecting && !hasBeenVisibleRef.current) {
30
+ hasBeenVisibleRef.current = true;
31
+ callbacksRef.current?.onView?.(contextRef.current);
32
+ }
33
+ else if (!entry.isIntersecting &&
34
+ hasBeenVisibleRef.current &&
35
+ !hasFiredOutOfViewRef.current) {
36
+ hasFiredOutOfViewRef.current = true;
37
+ callbacksRef.current?.onOutOfView?.(contextRef.current);
38
+ observer.disconnect();
39
+ }
40
+ }, { threshold: 0.5 });
41
+ observer.observe(node);
42
+ observerRef.current = observer;
43
+ }, []);
44
+ return { containerRef };
45
+ }
package/lib/mjs/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import CioPia from './components/CioPia';
2
2
  // Components
3
3
  export { default as CioPia } from './components/CioPia';
4
+ // Utilities
5
+ export { sanitizeHtml, renderMarkdown } from './utils/contentTransformers';
4
6
  // Default
5
7
  export * from './types';
6
8
  export default CioPia;
@@ -110,3 +110,92 @@ div.badge-wrapper code {
110
110
  background: #d8e8fd;
111
111
  color: #3c70bf;
112
112
  }
113
+
114
+ .cio-flow-diagram {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 4px;
118
+ margin: 24px 0 0;
119
+ font-family: system-ui, sans-serif;
120
+ font-size: 14px;
121
+ }
122
+
123
+ .cio-flow-diagram .flow-step {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 12px;
127
+ }
128
+
129
+ .cio-flow-diagram .flow-box {
130
+ padding: 10px 16px;
131
+ border-radius: 8px;
132
+ min-width: 220px;
133
+ text-align: center;
134
+ }
135
+
136
+ .cio-flow-diagram .flow-box--consumer {
137
+ background: #eff6ff;
138
+ border: 1px solid #bfdbfe;
139
+ }
140
+
141
+ .cio-flow-diagram .flow-box--internal {
142
+ background: #f5f3ff;
143
+ border: 1px solid #ddd6fe;
144
+ }
145
+
146
+ .cio-flow-diagram .flow-box--output {
147
+ background: #f0fdf4;
148
+ border: 1px solid #bbf7d0;
149
+ }
150
+
151
+ .cio-flow-diagram .flow-arrow {
152
+ margin-left: 110px;
153
+ color: #9ca3af;
154
+ }
155
+
156
+ .cio-flow-diagram .flow-label {
157
+ font-size: 12px;
158
+ color: #9ca3af;
159
+ font-style: italic;
160
+ }
161
+
162
+ .cio-flow-diagram .flow-callback {
163
+ font-size: 12px;
164
+ color: #059669;
165
+ }
166
+
167
+ .cio-flow-diagram .flow-box-subtitle {
168
+ font-size: 12px;
169
+ color: #6b7280;
170
+ }
171
+
172
+ .cio-flow-legend {
173
+ display: flex;
174
+ gap: 16px;
175
+ margin: 32px 0 16px;
176
+ font-size: 12px;
177
+ }
178
+
179
+ .cio-flow-legend .legend-swatch {
180
+ display: inline-block;
181
+ width: 12px;
182
+ height: 12px;
183
+ border-radius: 3px;
184
+ margin-right: 4px;
185
+ vertical-align: middle;
186
+ }
187
+
188
+ .cio-flow-legend .legend-swatch--consumer {
189
+ background: #eff6ff;
190
+ border: 1px solid #bfdbfe;
191
+ }
192
+
193
+ .cio-flow-legend .legend-swatch--internal {
194
+ background: #f5f3ff;
195
+ border: 1px solid #ddd6fe;
196
+ }
197
+
198
+ .cio-flow-legend .legend-swatch--output {
199
+ background: #f0fdf4;
200
+ border: 1px solid #bbf7d0;
201
+ }
@@ -0,0 +1,16 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { marked } from 'marked';
3
+ const purifyConfig = {
4
+ FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
5
+ };
6
+ export function sanitizeHtml(html) {
7
+ if (!html)
8
+ return '';
9
+ return DOMPurify.sanitize(html, purifyConfig);
10
+ }
11
+ export function renderMarkdown(content) {
12
+ if (!content)
13
+ return '';
14
+ const html = marked.parse(content, { async: false, breaks: true });
15
+ return DOMPurify.sanitize(html, purifyConfig);
16
+ }
@@ -1 +1,2 @@
1
1
  export * from './styleHelpers';
2
+ export { sanitizeHtml, renderMarkdown } from './contentTransformers';
@@ -1,4 +1,3 @@
1
- // eslint-disable-next-line import/prefer-default-export
2
1
  export function getPreferredColorScheme() {
3
2
  let colorScheme = 'light';
4
3
  // Check if the dark-mode Media-Query matches
@@ -16,7 +16,6 @@ const defaultTranslations = {
16
16
  * @param translations - Optional user-provided translations object
17
17
  * @returns The translated string or the original word if no translation exists
18
18
  */
19
- // eslint-disable-next-line import/prefer-default-export
20
19
  export const translate = (word, translations) => {
21
20
  if (translations && translations[word] !== undefined) {
22
21
  return translations[word];
@@ -1 +1 @@
1
- export default '2.3.0';
1
+ export default '2.6.2';
@@ -3,29 +3,27 @@ import { IncludeComponentOverrides, IncludeRenderProps } from '@constructor-io/c
3
3
  import MockConstructorIOClient from '../../hooks/mocks/MockConstructorIOClient';
4
4
  import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, Translations, SuggestedQuestionsParameters, Formatters } from '../../types';
5
5
  export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
6
+ /** Your Constructor.io API key. */
6
7
  apiKey: string;
8
+ /** The product item ID to fetch insights for. */
7
9
  itemId: string;
8
- /** Thread ID for conversation context. Must be a valid UUID (e.g., "550e8400-e29b-41d4-a716-446655440000") */
10
+ /** Thread ID for conversation context. Must be a valid UUID (e.g., "550e8400-e29b-41d4-a716-446655440000"). */
9
11
  threadId?: string;
12
+ /** Optional variation ID for the product. */
10
13
  variationId?: string;
14
+ /** Optional Constructor.io client instance. If not provided, one will be created internally. */
11
15
  cioClient?: MockConstructorIOClient;
16
+ /** Display configuration options (mode, type, showFeedback, etc.). */
12
17
  displayConfigs?: CioPiaDisplayConfigs;
13
- /**
14
- * Callback handlers for user interactions:
15
- *
16
- * `onQuestionSubmit: (question: string) => void`
17
- * Called when a question is submitted (via Enter key, Send button, or suggested question click).
18
- *
19
- * `onProductCardClick: (item: Item) => void`
20
- * Called when a product card in the carousel is clicked.
21
- *
22
- * `onFeedback: (type: 'up' | 'down') => void`
23
- * Called when the user submits positive or negative feedback on an answer.
24
- */
18
+ /** Callback handlers for user interactions (onQuestionSubmit, onProductCardClick, onFeedback). */
25
19
  callbacks?: Callbacks;
26
- /** Define formatter functions outside the component or memoize to avoid unnecessary re-renders. */
20
+ /** Custom component overrides via reactNode or render props functions. */
21
+ componentOverrides?: CioPiaComponentOverrides;
22
+ /** Formatter functions for transforming data before display. */
27
23
  formatters?: Formatters;
24
+ /** UI string translations for internationalization. */
28
25
  translations?: Translations;
26
+ /** Parameters for the suggested questions request. */
29
27
  suggestedQuestionsParameters?: SuggestedQuestionsParameters;
30
28
  }
31
29
  export default function CioPia(props: CioPiaProps): React.JSX.Element;
@@ -6,7 +6,8 @@ interface InputProps {
6
6
  /** @deprecated Use the `translations` prop instead. */
7
7
  placeholder?: string;
8
8
  onSubmit: (value: string) => void;
9
+ onFocus?: () => void;
9
10
  translations?: Translations;
10
11
  }
11
- declare function Input({ value: providedValue, placeholder, disabled, onSubmit, translations, }: InputProps): React.JSX.Element;
12
+ declare function Input({ value: providedValue, placeholder, disabled, onSubmit, onFocus, translations, }: InputProps): React.JSX.Element;
12
13
  export default Input;
@@ -4,5 +4,8 @@ import { ConversationHistoryProps } from '../ConversationHistory/ConversationHis
4
4
  export interface PiaConversationProps extends ConversationHistoryProps {
5
5
  displayedQuestions: Question[];
6
6
  handleSubmitQuestion: (question: string) => void;
7
+ handleQuestionClick: (question: string) => void;
8
+ containerRef?: (node: HTMLDivElement | null) => void;
9
+ onInputFocus?: () => void;
7
10
  }
8
- export default function PiaConversation({ conversationHistory, isLoading, error, currentItems, showFeedback, showPreviousItems, learnMoreUrl, disclaimerPosition, translations, callbacks, componentOverrides, displayedQuestions, handleSubmitQuestion, }: PiaConversationProps): React.JSX.Element;
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;