@constructor-io/constructorio-ui-pia 2.13.0 → 2.14.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 (35) hide show
  1. package/README.md +49 -1
  2. package/dist/constructorio-ui-pia-bundled.js +58 -58
  3. package/lib/cjs/components/CioPia/CioPia.js +6 -1
  4. package/lib/cjs/components/CioPia/CioPiaQna.js +2 -1
  5. package/lib/cjs/components/CioPiaControl/CioPiaControl.js +38 -0
  6. package/lib/cjs/components/CioPiaRecs/CioPiaRecs.js +3 -1
  7. package/lib/cjs/hooks/useAnswerResults.js +12 -3
  8. package/lib/cjs/hooks/useCioPia.js +2 -1
  9. package/lib/cjs/hooks/useConversation.js +14 -2
  10. package/lib/cjs/hooks/usePiaClient.js +14 -7
  11. package/lib/cjs/hooks/useRecsPod.js +15 -7
  12. package/lib/cjs/hooks/useSuggestedQuestions.js +12 -3
  13. package/lib/cjs/hooks/useViewportTracking.js +9 -4
  14. package/lib/cjs/version.js +1 -1
  15. package/lib/mjs/components/CioPia/CioPia.js +5 -0
  16. package/lib/mjs/components/CioPia/CioPiaQna.js +2 -1
  17. package/lib/mjs/components/CioPiaControl/CioPiaControl.js +34 -0
  18. package/lib/mjs/components/CioPiaRecs/CioPiaRecs.js +3 -1
  19. package/lib/mjs/hooks/useAnswerResults.js +13 -4
  20. package/lib/mjs/hooks/useCioPia.js +2 -1
  21. package/lib/mjs/hooks/useConversation.js +14 -2
  22. package/lib/mjs/hooks/usePiaClient.js +15 -3
  23. package/lib/mjs/hooks/useRecsPod.js +15 -7
  24. package/lib/mjs/hooks/useSuggestedQuestions.js +13 -4
  25. package/lib/mjs/hooks/useViewportTracking.js +9 -4
  26. package/lib/mjs/version.js +1 -1
  27. package/lib/types/components/CioPia/types.d.ts +3 -1
  28. package/lib/types/components/CioPiaControl/CioPiaControl.d.ts +9 -0
  29. package/lib/types/hooks/useCioPia.d.ts +2 -0
  30. package/lib/types/hooks/usePiaClient.d.ts +2 -1
  31. package/lib/types/hooks/useViewportTracking.d.ts +6 -1
  32. package/lib/types/types.d.ts +33 -0
  33. package/lib/types/utils/contentTransformers.d.ts +2 -2
  34. package/lib/types/version.d.ts +1 -1
  35. package/package.json +6 -5
@@ -1,15 +1,24 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  export default function useSuggestedQuestions({ itemId, variationId, threadId, cioClient, parameters, }) {
3
3
  const [questions, setQuestions] = useState([]);
4
4
  const [isLoading, setIsLoading] = useState(false);
5
5
  const [error, setError] = useState(null);
6
6
  const serializedParameters = useMemo(() => JSON.stringify(parameters), [parameters]);
7
+ // The client is how the question is asked, not part of the question: a host that
8
+ // rebuilds it on render must not make the widget ask the same thing again.
9
+ const cioClientRef = useRef(cioClient);
10
+ useEffect(() => {
11
+ cioClientRef.current = cioClient;
12
+ }, [cioClient]);
13
+ // A client arriving where there was none is the one client change worth a fetch.
14
+ const hasClient = !!cioClient;
7
15
  const fetchResult = useCallback(() => {
8
- if (!cioClient)
16
+ const client = cioClientRef.current;
17
+ if (!client)
9
18
  return;
10
19
  setIsLoading(true);
11
20
  setError(null);
12
- cioClient.agent.pia
21
+ client.agent.pia
13
22
  .getSuggestedQuestions(itemId, {
14
23
  threadId,
15
24
  variationId,
@@ -27,7 +36,7 @@ export default function useSuggestedQuestions({ itemId, variationId, threadId, c
27
36
  });
28
37
  // parameters is serialized via serializedParameters to prevent refetch on identity changes
29
38
  // eslint-disable-next-line react-hooks/exhaustive-deps
30
- }, [cioClient, itemId, variationId, threadId, serializedParameters]);
39
+ }, [hasClient, itemId, variationId, threadId, serializedParameters]);
31
40
  useEffect(() => {
32
41
  fetchResult();
33
42
  }, [fetchResult]);
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from 'react';
2
2
  const FLUSH_INTERVAL_MS = 5 * 60 * 1000;
3
3
  const DEFAULT_VIEW_THRESHOLD = 0.5;
4
- export default function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, }) {
4
+ export default function useViewportTracking({ tracking, questions, viewThreshold = DEFAULT_VIEW_THRESHOLD, awaitQuestions = true, }) {
5
5
  const nodeRef = useRef(null);
6
6
  const observerRef = useRef(null);
7
7
  const intervalRef = useRef(null);
@@ -9,10 +9,14 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
9
9
  const entryTimeRef = useRef(null);
10
10
  const questionsRef = useRef(questions);
11
11
  const trackingRef = useRef(tracking);
12
+ const awaitQuestionsRef = useRef(awaitQuestions);
13
+ useEffect(() => {
14
+ awaitQuestionsRef.current = awaitQuestions;
15
+ }, [awaitQuestions]);
12
16
  useEffect(() => {
13
17
  const questionsJustArrived = questionsRef.current.length === 0 && questions.length > 0;
14
18
  questionsRef.current = questions;
15
- if (questionsJustArrived && entryTimeRef.current) {
19
+ if (awaitQuestionsRef.current && questionsJustArrived && entryTimeRef.current) {
16
20
  trackingRef.current.trackView(questions);
17
21
  }
18
22
  }, [questions]);
@@ -28,7 +32,8 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
28
32
  });
29
33
  entryTimeRef.current = null;
30
34
  }
31
- if (timespansRef.current.length > 0 && questionsRef.current.length > 0) {
35
+ const hasPayload = !awaitQuestionsRef.current || questionsRef.current.length > 0;
36
+ if (timespansRef.current.length > 0 && hasPayload) {
32
37
  trackingRef.current.trackViews(questionsRef.current, [...timespansRef.current]);
33
38
  timespansRef.current = [];
34
39
  }
@@ -37,7 +42,7 @@ export default function useViewportTracking({ tracking, questions, viewThreshold
37
42
  const observer = new IntersectionObserver(([entry]) => {
38
43
  if (entry.isIntersecting) {
39
44
  entryTimeRef.current = new Date().toISOString();
40
- if (questionsRef.current.length > 0) {
45
+ if (!awaitQuestionsRef.current || questionsRef.current.length > 0) {
41
46
  trackingRef.current.trackView(questionsRef.current);
42
47
  }
43
48
  }
@@ -1 +1 @@
1
- export default '2.13.0';
1
+ export default '2.14.0';
@@ -1,5 +1,5 @@
1
1
  import { IncludeComponentOverrides, IncludeRenderProps } from '@constructor-io/constructorio-ui-components';
2
- import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaDisplayConfigs, CioPiaTrackingConfigs, Translations, SuggestedQuestionsParameters, AnswerRequestParameters, RecsPodParameters, Formatters, ProductCardDisplayProps } from '../../types';
2
+ import { CioPiaRenderProps, CioPiaComponentOverrides, Callbacks, CioPiaAbTest, CioPiaDisplayConfigs, CioPiaTrackingConfigs, Translations, SuggestedQuestionsParameters, AnswerRequestParameters, RecsPodParameters, Formatters, ProductCardDisplayProps } from '../../types';
3
3
  import type { CioClient } from '../../hooks/usePiaClient';
4
4
  export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, IncludeComponentOverrides<CioPiaComponentOverrides> {
5
5
  /** Your Constructor.io API key. */
@@ -18,6 +18,8 @@ export interface CioPiaProps extends IncludeRenderProps<CioPiaRenderProps>, Incl
18
18
  displayConfigs?: CioPiaDisplayConfigs;
19
19
  /** Tracking configuration options (viewThreshold, etc.). */
20
20
  trackingConfigs?: CioPiaTrackingConfigs;
21
+ /** A/B test configuration: test cells to attach to events, and the control-group flag. */
22
+ abTest?: CioPiaAbTest;
21
23
  /** Callback handlers for user interactions (onQuestionSubmit, onProductCardClick, onFeedback). */
22
24
  callbacks?: Callbacks;
23
25
  /** Custom component overrides via reactNode or render props functions. */
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { CioPiaProps } from '../CioPia/types';
3
+ /**
4
+ * What a control-group shopper gets instead of the widget: nothing to look at, but a node the
5
+ * view event can be tied to, so both arms of an A/B test are measured the same way.
6
+ *
7
+ * None of the question or answer hooks mount here, so the control arm cannot issue a request.
8
+ */
9
+ export default function CioPiaControl(props: CioPiaProps): React.JSX.Element;
@@ -32,6 +32,8 @@ export interface UseCioPiaProps {
32
32
  parameters?: Record<string, string | number | boolean>;
33
33
  /** Define outside the component or wrap with useCallback to avoid unnecessary re-renders. */
34
34
  formatImageUrl?: Formatters['formatImageUrl'];
35
+ /** Test cells to attach to tracking events. Ignored when `cioClient` is supplied. */
36
+ testCells?: Record<string, string>;
35
37
  }
36
38
  export interface UseCioPiaReturn {
37
39
  cioClient: {
@@ -4,9 +4,10 @@ export interface UsePiaClientProps {
4
4
  apiKey: string;
5
5
  threadId?: string;
6
6
  cioClient?: CioClient;
7
+ testCells?: Record<string, string>;
7
8
  }
8
9
  export interface UsePiaClientReturn {
9
10
  cioClient: CioClient;
10
11
  threadId: string;
11
12
  }
12
- export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, }: UsePiaClientProps): UsePiaClientReturn;
13
+ export default function usePiaClient({ apiKey, threadId: providedThreadId, cioClient: providedClient, testCells, }: UsePiaClientProps): UsePiaClientReturn;
@@ -4,8 +4,13 @@ export interface UseViewportTrackingProps {
4
4
  tracking: UseTrackingReturn;
5
5
  questions: Question[];
6
6
  viewThreshold?: number;
7
+ /**
8
+ * Whether an empty `questions` array means "not loaded yet". False for a control-group
9
+ * placeholder, where no questions are ever fetched and the view must still be recorded.
10
+ */
11
+ awaitQuestions?: boolean;
7
12
  }
8
13
  export interface UseViewportTrackingReturn {
9
14
  containerRef: (node: HTMLDivElement | null) => void;
10
15
  }
11
- export default function useViewportTracking({ tracking, questions, viewThreshold, }: UseViewportTrackingProps): UseViewportTrackingReturn;
16
+ export default function useViewportTracking({ tracking, questions, viewThreshold, awaitQuestions, }: UseViewportTrackingProps): UseViewportTrackingReturn;
@@ -24,6 +24,11 @@ export interface CioPiaProviderProps {
24
24
  threadId?: string;
25
25
  cioClient?: Nullable<CioClient>;
26
26
  }
27
+ /**
28
+ * Display mode. `'recommendations'` is accepted but **not yet available**: the
29
+ * recommendations endpoint is absent from the client SDK, so that mode renders
30
+ * nothing. Use `'default'` or `'conversation'`.
31
+ */
27
32
  export type CioPiaMode = 'default' | 'conversation' | 'recommendations';
28
33
  export type CioPiaType = 'inline' | 'modal';
29
34
  export type DisclaimerPosition = 'top' | 'bottom';
@@ -34,6 +39,34 @@ export type CioPiaTrackingConfigs = {
34
39
  */
35
40
  viewThreshold?: number;
36
41
  };
42
+ export type CioPiaAbTest = {
43
+ /**
44
+ * Test cells to attach to PIA tracking events, as `{ [testName]: cellName }`. Each entry is
45
+ * sent as an `ef-<testName>` parameter. Source them however you like — Constructor's docs have
46
+ * the page set `window.cnstrc.testCell` to a bare cell name, so label it with your test name:
47
+ * `{ constructorio: window.cnstrc.testCell }`. Some integrations expose a `window.cnstrc.testCells`
48
+ * map instead, which you can pass straight through.
49
+ *
50
+ * Ignored when you supply your own `cioClient`: that client owns its own options, so set
51
+ * `testCells` there instead, as a `ConstructorIOClient` constructor option. Passing both
52
+ * logs a warning.
53
+ *
54
+ * Empty, non-string, or whitespace-only values are silently dropped, so a cell sourced from
55
+ * a global that resolves to `undefined` will simply not be sent.
56
+ */
57
+ testCells?: Record<string, string>;
58
+ /**
59
+ * Mark this shopper as the A/B control group. Renders an invisible, tracking-only placeholder
60
+ * instead of the widget, so both arms of the test record a view event. Takes precedence over
61
+ * `displayConfigs.mode`. The placeholder renders neither `children` nor `componentOverrides`,
62
+ * and does not fire the `onView` / `onOutOfView` callbacks — the control arm reports through
63
+ * Constructor's own view event only.
64
+ *
65
+ * Required: stating the arm per shopper is the one thing only you can know, and leaving it
66
+ * out would silently put every shopper in the test arm with no control group at all.
67
+ */
68
+ isControl: boolean;
69
+ };
37
70
  export type CioPiaDisplayConfigs = {
38
71
  learnMoreUrl?: string;
39
72
  showFeedback?: boolean;
@@ -1,6 +1,6 @@
1
- import DOMPurify from 'dompurify';
1
+ import DOMPurify, { type Config } from 'dompurify';
2
2
  export interface SanitizeOptions {
3
- config: DOMPurify.Config;
3
+ config: Config;
4
4
  }
5
5
  /** Options for {@link renderMarkdown}. */
6
6
  export interface RenderMarkdownOptions {
@@ -1,2 +1,2 @@
1
- declare const _default: "2.13.0";
1
+ declare const _default: "2.14.0";
2
2
  export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructor-io/constructorio-ui-pia",
3
- "version": "2.13.0",
3
+ "version": "2.14.0",
4
4
  "description": "Constructor.io AI Product Insights Agent UI library for web applications",
5
5
  "author": "Constructor.io Corporation",
6
6
  "license": "MIT",
@@ -99,7 +99,6 @@
99
99
  "@storybook/testing-library": "^0.2.1",
100
100
  "@testing-library/jest-dom": "^6.6.3",
101
101
  "@testing-library/react": "^16.2.0",
102
- "@types/dompurify": "^3.0.5",
103
102
  "@types/jest": "^30.0.0",
104
103
  "@types/react": "^19.0.10",
105
104
  "@types/react-dom": "^19.0.4",
@@ -127,17 +126,19 @@
127
126
  "typescript-eslint": "^8.59.2",
128
127
  "vite": "^6.4.3",
129
128
  "vite-plugin-css-injected-by-js": "^3.5.2",
130
- "webpack": "^5.98.0"
129
+ "webpack": "^5.104.1"
131
130
  },
132
131
  "volta": {
133
132
  "node": "22.18.0",
134
133
  "npm": "10.9.3"
135
134
  },
136
135
  "dependencies": {
137
- "dompurify": "^3.4.2",
136
+ "dompurify": "^3.4.15",
138
137
  "marked": "^15.0.12"
139
138
  },
140
139
  "overrides": {
141
- "uuid": "^11.1.1"
140
+ "uuid": "^11.1.1",
141
+ "browserslist": "^4.28.9",
142
+ "postcss-selector-parser": "^7.1.6"
142
143
  }
143
144
  }