@constructor-io/constructorio-ui-asa 1.8.0 → 1.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.
@@ -1,11 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
- const react_1 = tslib_1.__importDefault(require("react"));
4
+ const react_1 = tslib_1.__importStar(require("react"));
5
5
  const constructorio_ui_components_1 = require("@constructor-io/constructorio-ui-components");
6
6
  const icons_1 = require("../icons");
7
- function Button({ theme = 'dark', size = 'sm', onClick, label = 'Shopping assistant', }) {
8
- return (react_1.default.createElement(constructorio_ui_components_1.Button, { className: `cio-asa-button cio-asa-button--${theme} cio-asa-button--${size}`, size: size === 'lg' ? 'default' : 'sm', onClick: onClick },
7
+ const useCioAsaContext_1 = require("../../hooks/useCioAsaContext");
8
+ const useAsaTracking_1 = tslib_1.__importDefault(require("../../hooks/useAsaTracking"));
9
+ const AGENT_BUTTON_CLICK_MODE = 'chat';
10
+ function Button({ theme = 'dark', size = 'sm', onClick, label = 'Shopping assistant', positionOnPage, pageType, instanceId, }) {
11
+ var _a, _b, _c;
12
+ const context = (0, useCioAsaContext_1.useCioAsaContext)();
13
+ const agentDomain = (_a = context === null || context === void 0 ? void 0 : context.staticRequestConfigs) === null || _a === void 0 ? void 0 : _a.domain;
14
+ const tracking = (0, useAsaTracking_1.default)({
15
+ tracker: (_c = (_b = context === null || context === void 0 ? void 0 : context.cioClient) === null || _b === void 0 ? void 0 : _b.tracker) !== null && _c !== void 0 ? _c : undefined,
16
+ section: context === null || context === void 0 ? void 0 : context.section,
17
+ });
18
+ const handleClick = (0, react_1.useCallback)(() => {
19
+ var _a, _b;
20
+ if (agentDomain) {
21
+ const payload = Object.assign(Object.assign(Object.assign({ mode: AGENT_BUTTON_CLICK_MODE, agentDomain }, (positionOnPage && { positionOnPage })), (pageType && { pageType })), (instanceId && { instanceId }));
22
+ tracking.trackAgentButtonClick(payload);
23
+ (_b = (_a = context === null || context === void 0 ? void 0 : context.callbacks) === null || _a === void 0 ? void 0 : _a.onAgentButtonClick) === null || _b === void 0 ? void 0 : _b.call(_a, payload);
24
+ }
25
+ onClick === null || onClick === void 0 ? void 0 : onClick();
26
+ }, [agentDomain, positionOnPage, pageType, instanceId, tracking, context, onClick]);
27
+ return (react_1.default.createElement(constructorio_ui_components_1.Button, { className: `cio-asa-button cio-asa-button--${theme} cio-asa-button--${size}`, size: size === 'lg' ? 'default' : 'sm', onClick: handleClick },
9
28
  theme === 'dark' ? react_1.default.createElement(icons_1.ChatBubbleDarkIcon, null) : react_1.default.createElement(icons_1.ChatBubbleLightIcon, null),
10
29
  react_1.default.createElement("span", { className: 'cio-asa-button__label' }, label)));
11
30
  }
@@ -29,7 +29,7 @@ function getAnnouncement(messages, translations) {
29
29
  }
30
30
  return '';
31
31
  }
32
- const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, onThreadsChange, showStopButton = false, }, ref) => {
32
+ const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, initialPrompt, onThreadsChange, showStopButton = false, }, ref) => {
33
33
  var _a;
34
34
  const { messages, sendMessage, isStreaming, canAbort, abort, clearHistory, isHydrating, threads, activeThreadId, newThread, switchThread, } = (0, useAsaResults_1.default)({
35
35
  initialThreadId,
@@ -49,12 +49,25 @@ const Chat = (0, react_1.forwardRef)(({ onClose, className, initialSuggestions,
49
49
  const showChatFrame = showChat || (isHydrating && lastViewRef.current === 'chat');
50
50
  const isModal = typeof onClose === 'function';
51
51
  const announcement = getAnnouncement(messages, translations);
52
- (0, react_1.useImperativeHandle)(ref, () => ({ abort, clearHistory, newThread, switchThread }), [
52
+ (0, react_1.useImperativeHandle)(ref, () => ({
53
53
  abort,
54
54
  clearHistory,
55
55
  newThread,
56
56
  switchThread,
57
- ]);
57
+ sendMessage: (text, options) => sendMessage(text, 'external', options),
58
+ }), [abort, clearHistory, newThread, switchThread, sendMessage]);
59
+ // Sent from a timer so StrictMode's mount, unmount, mount cannot cancel the stream it starts.
60
+ const initialPromptRef = (0, react_1.useRef)(initialPrompt);
61
+ (0, react_1.useEffect)(() => {
62
+ const prompt = initialPromptRef.current;
63
+ if (isHydrating || !prompt)
64
+ return undefined;
65
+ const timer = setTimeout(() => {
66
+ initialPromptRef.current = undefined;
67
+ sendMessage(prompt, 'external');
68
+ });
69
+ return () => clearTimeout(timer);
70
+ }, [isHydrating, sendMessage]);
58
71
  const hasPersistence = Boolean((_a = (0, useCioAsaContext_1.useCioAsaContext)()) === null || _a === void 0 ? void 0 : _a.persistence);
59
72
  const onThreadsChangeRef = (0, useLatest_1.default)(onThreadsChange);
60
73
  (0, react_1.useEffect)(() => {
@@ -8,6 +8,7 @@ const useChatPersistence_1 = tslib_1.__importDefault(require("./persistence/useC
8
8
  const agentStream_1 = require("./agentStream");
9
9
  const chatSession_1 = require("../utils/chatSession");
10
10
  const productNormalizer_1 = require("../utils/productNormalizer");
11
+ const chatThreads_1 = require("../utils/chatThreads");
11
12
  const useLatest_1 = tslib_1.__importDefault(require("./useLatest"));
12
13
  function useAsaResults(options) {
13
14
  var _a;
@@ -58,13 +59,13 @@ function useAsaResults(options) {
58
59
  });
59
60
  const { beginTurn, screenOwner } = store;
60
61
  (0, react_1.useEffect)(() => cancelStream, [cancelStream]);
61
- const sendMessage = (0, react_1.useCallback)((text, source = 'input') => {
62
+ const sendToCurrentThread = (0, react_1.useCallback)((text, source) => {
62
63
  var _a, _b;
63
64
  const intent = text.trim();
64
65
  if (!intent || session.isStreaming || session.foreignInFlight)
65
66
  return;
66
67
  beginTurn();
67
- trackingRef.current.trackSubmit(intent);
68
+ trackingRef.current.trackSubmit(intent, source);
68
69
  (_b = (_a = callbacksRef.current) === null || _a === void 0 ? void 0 : _a.onAssistantSubmit) === null || _b === void 0 ? void 0 : _b.call(_a, { intent, source });
69
70
  const userMessage = {
70
71
  id: (0, chatSession_1.nextMessageId)(session),
@@ -113,6 +114,32 @@ function useAsaResults(options) {
113
114
  callbacksRef,
114
115
  staticRequestConfigsRef,
115
116
  ]);
117
+ const { newThread, switchThread } = store;
118
+ /**
119
+ * Sends `text`. With `threadId`, first moves to that conversation, loading it from storage
120
+ * when persistence has it and otherwise continuing it on the server with an empty screen.
121
+ */
122
+ const sendMessage = (0, react_1.useCallback)((text, source = 'input', { threadId } = {}) => {
123
+ if (!threadId ||
124
+ threadId === session.serverThreadId ||
125
+ threadId === session.storageThreadId) {
126
+ sendToCurrentThread(text, source);
127
+ return;
128
+ }
129
+ (() => tslib_1.__awaiter(this, void 0, void 0, function* () {
130
+ if (persistence)
131
+ yield switchThread(threadId);
132
+ else
133
+ newThread();
134
+ // A turn that started meanwhile (or one still streaming in the background) owns the screen.
135
+ if (session.isStreaming || session.foreignInFlight)
136
+ return;
137
+ if (!session.serverThreadId && !(0, chatThreads_1.isLocalThreadId)(threadId)) {
138
+ session.serverThreadId = threadId;
139
+ }
140
+ sendToCurrentThread(text, source);
141
+ }))();
142
+ }, [session, persistence, newThread, switchThread, sendToCurrentThread]);
116
143
  /**
117
144
  * Cancels the in-flight request while keeping the conversation: the partial reply is
118
145
  * settled as `done` (so the typing indicator stops and streamed text survives) and the
@@ -159,8 +186,8 @@ function useAsaResults(options) {
159
186
  isHydrating: store.isHydrating,
160
187
  threads: store.threads,
161
188
  activeThreadId: store.activeThreadId,
162
- newThread: store.newThread,
163
- switchThread: store.switchThread,
189
+ newThread,
190
+ switchThread,
164
191
  };
165
192
  }
166
193
  exports.default = useAsaResults;
@@ -1,7 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
+ /** Beacon `source` values, aligned with the ones the behavioral-actions API documents. */
5
+ const BEACON_SUBMIT_SOURCES = {
6
+ input: 'input',
7
+ suggestion: 'suggestion',
8
+ refinement: 'follow_up',
9
+ // A prompt the host page sent, typically a suggested question clicked outside the chat.
10
+ external: 'suggestion',
11
+ };
4
12
  const NOOP_TRACKING = {
13
+ trackAgentButtonClick: () => { },
5
14
  trackSubmit: () => { },
6
15
  trackResultLoadStarted: () => { },
7
16
  trackResultLoadFinished: () => { },
@@ -15,37 +24,42 @@ const NOOP_TRACKING = {
15
24
  * Mirrors the tracking-hook pattern used by constructorio-ui-pia.
16
25
  */
17
26
  function useAsaTracking({ tracker, section, threadId, }) {
18
- const assistant = tracker;
19
27
  const base = (0, react_1.useMemo)(() => (Object.assign(Object.assign({}, (section && { section })), (threadId && { threadId }))), [section, threadId]);
20
- const trackSubmit = (0, react_1.useCallback)((intent) => {
21
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantSubmit(Object.assign({ intent }, base));
22
- }, [assistant, base]);
28
+ const trackAgentButtonClick = (0, react_1.useCallback)(({ mode, agentDomain, positionOnPage, pageType, instanceId }) => {
29
+ var _a;
30
+ (_a = tracker === null || tracker === void 0 ? void 0 : tracker.trackAgentButtonClick) === null || _a === void 0 ? void 0 : _a.call(tracker, Object.assign(Object.assign(Object.assign(Object.assign({ mode,
31
+ agentDomain }, (positionOnPage && { positionOnPage })), (pageType && { pageType })), (instanceId && { instanceId })), (section && { section })));
32
+ }, [tracker, section]);
33
+ const trackSubmit = (0, react_1.useCallback)((intent, source) => {
34
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantSubmit(Object.assign(Object.assign({ intent }, (source && { source: BEACON_SUBMIT_SOURCES[source] })), base));
35
+ }, [tracker, base]);
23
36
  const trackResultLoadStarted = (0, react_1.useCallback)(({ intent, intentResultId }) => {
24
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadStarted(Object.assign(Object.assign({ intent }, (intentResultId && { intentResultId })), base));
25
- }, [assistant, base]);
37
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultLoadStarted(Object.assign(Object.assign({ intent }, (intentResultId && { intentResultId })), base));
38
+ }, [tracker, base]);
26
39
  const trackResultLoadFinished = (0, react_1.useCallback)(({ intent, searchResultCount, intentResultId, threadId: turnThreadId, }) => {
27
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultLoadFinished(Object.assign(Object.assign(Object.assign({ intent,
40
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultLoadFinished(Object.assign(Object.assign(Object.assign({ intent,
28
41
  searchResultCount }, (intentResultId && { intentResultId })), base), (turnThreadId && { threadId: turnThreadId })));
29
- }, [assistant, base]);
42
+ }, [tracker, base]);
30
43
  const trackResultClick = (0, react_1.useCallback)(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
31
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultClick(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ intent,
44
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultClick(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ intent,
32
45
  searchResultId }, (intentResultId && { intentResultId })), (itemId && { itemId })), (itemName && { itemName })), (variationId && { variationId })), base));
33
- }, [assistant, base]);
46
+ }, [tracker, base]);
34
47
  const trackResultView = (0, react_1.useCallback)(({ intent, searchResultId, numResultsViewed, intentResultId, items }) => {
35
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantResultView(Object.assign(Object.assign(Object.assign({ intent,
48
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantResultView(Object.assign(Object.assign(Object.assign({ intent,
36
49
  searchResultId,
37
50
  numResultsViewed }, (intentResultId && { intentResultId })), (items && items.length > 0 && { items })), base));
38
- }, [assistant, base]);
51
+ }, [tracker, base]);
39
52
  const trackSearchSubmit = (0, react_1.useCallback)(({ intent, searchTerm, userInput, searchResultId, intentResultId, groupId, }) => {
40
- assistant === null || assistant === void 0 ? void 0 : assistant.trackAssistantSearchSubmit(Object.assign(Object.assign(Object.assign({ intent,
53
+ tracker === null || tracker === void 0 ? void 0 : tracker.trackAssistantSearchSubmit(Object.assign(Object.assign(Object.assign({ intent,
41
54
  searchTerm,
42
55
  userInput,
43
56
  searchResultId }, (intentResultId && { intentResultId })), (groupId && { groupId })), base));
44
- }, [assistant, base]);
57
+ }, [tracker, base]);
45
58
  return (0, react_1.useMemo)(() => {
46
- if (!assistant)
59
+ if (!tracker)
47
60
  return NOOP_TRACKING;
48
61
  return {
62
+ trackAgentButtonClick,
49
63
  trackSubmit,
50
64
  trackResultLoadStarted,
51
65
  trackResultLoadFinished,
@@ -54,7 +68,8 @@ function useAsaTracking({ tracker, section, threadId, }) {
54
68
  trackSearchSubmit,
55
69
  };
56
70
  }, [
57
- assistant,
71
+ tracker,
72
+ trackAgentButtonClick,
58
73
  trackSubmit,
59
74
  trackResultLoadStarted,
60
75
  trackResultLoadFinished,
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = '1.8.0';
3
+ exports.default = '1.9.0';
@@ -1,8 +1,31 @@
1
- import React from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import { Button as CioButton } from '@constructor-io/constructorio-ui-components';
3
3
  import { ChatBubbleDarkIcon, ChatBubbleLightIcon } from '../icons';
4
- export default function Button({ theme = 'dark', size = 'sm', onClick, label = 'Shopping assistant', }) {
5
- return (React.createElement(CioButton, { className: `cio-asa-button cio-asa-button--${theme} cio-asa-button--${size}`, size: size === 'lg' ? 'default' : 'sm', onClick: onClick },
4
+ import { useCioAsaContext } from '../../hooks/useCioAsaContext';
5
+ import useAsaTracking from '../../hooks/useAsaTracking';
6
+ const AGENT_BUTTON_CLICK_MODE = 'chat';
7
+ export default function Button({ theme = 'dark', size = 'sm', onClick, label = 'Shopping assistant', positionOnPage, pageType, instanceId, }) {
8
+ const context = useCioAsaContext();
9
+ const agentDomain = context?.staticRequestConfigs?.domain;
10
+ const tracking = useAsaTracking({
11
+ tracker: context?.cioClient?.tracker ?? undefined,
12
+ section: context?.section,
13
+ });
14
+ const handleClick = useCallback(() => {
15
+ if (agentDomain) {
16
+ const payload = {
17
+ mode: AGENT_BUTTON_CLICK_MODE,
18
+ agentDomain,
19
+ ...(positionOnPage && { positionOnPage }),
20
+ ...(pageType && { pageType }),
21
+ ...(instanceId && { instanceId }),
22
+ };
23
+ tracking.trackAgentButtonClick(payload);
24
+ context?.callbacks?.onAgentButtonClick?.(payload);
25
+ }
26
+ onClick?.();
27
+ }, [agentDomain, positionOnPage, pageType, instanceId, tracking, context, onClick]);
28
+ return (React.createElement(CioButton, { className: `cio-asa-button cio-asa-button--${theme} cio-asa-button--${size}`, size: size === 'lg' ? 'default' : 'sm', onClick: handleClick },
6
29
  theme === 'dark' ? React.createElement(ChatBubbleDarkIcon, null) : React.createElement(ChatBubbleLightIcon, null),
7
30
  React.createElement("span", { className: 'cio-asa-button__label' }, label)));
8
31
  }
@@ -25,7 +25,7 @@ function getAnnouncement(messages, translations) {
25
25
  }
26
26
  return '';
27
27
  }
28
- const Chat = forwardRef(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, onThreadsChange, showStopButton = false, }, ref) => {
28
+ const Chat = forwardRef(({ onClose, className, initialSuggestions, termsText, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, componentOverrides, translations, initialThreadId, initialPrompt, onThreadsChange, showStopButton = false, }, ref) => {
29
29
  const { messages, sendMessage, isStreaming, canAbort, abort, clearHistory, isHydrating, threads, activeThreadId, newThread, switchThread, } = useAsaResults({
30
30
  initialThreadId,
31
31
  normalizeItem: (item) => (normalizeItem ?? normalizeItemToProduct)(item, {
@@ -44,12 +44,25 @@ const Chat = forwardRef(({ onClose, className, initialSuggestions, termsText, on
44
44
  const showChatFrame = showChat || (isHydrating && lastViewRef.current === 'chat');
45
45
  const isModal = typeof onClose === 'function';
46
46
  const announcement = getAnnouncement(messages, translations);
47
- useImperativeHandle(ref, () => ({ abort, clearHistory, newThread, switchThread }), [
47
+ useImperativeHandle(ref, () => ({
48
48
  abort,
49
49
  clearHistory,
50
50
  newThread,
51
51
  switchThread,
52
- ]);
52
+ sendMessage: (text, options) => sendMessage(text, 'external', options),
53
+ }), [abort, clearHistory, newThread, switchThread, sendMessage]);
54
+ // Sent from a timer so StrictMode's mount, unmount, mount cannot cancel the stream it starts.
55
+ const initialPromptRef = useRef(initialPrompt);
56
+ useEffect(() => {
57
+ const prompt = initialPromptRef.current;
58
+ if (isHydrating || !prompt)
59
+ return undefined;
60
+ const timer = setTimeout(() => {
61
+ initialPromptRef.current = undefined;
62
+ sendMessage(prompt, 'external');
63
+ });
64
+ return () => clearTimeout(timer);
65
+ }, [isHydrating, sendMessage]);
53
66
  const hasPersistence = Boolean(useCioAsaContext()?.persistence);
54
67
  const onThreadsChangeRef = useLatest(onThreadsChange);
55
68
  useEffect(() => {
@@ -5,6 +5,7 @@ import useChatPersistence from './persistence/useChatPersistence';
5
5
  import { startTurn } from './agentStream';
6
6
  import { createChatSession, nextMessageId } from '../utils/chatSession';
7
7
  import { normalizeItemToProduct } from '../utils/productNormalizer';
8
+ import { isLocalThreadId } from '../utils/chatThreads';
8
9
  import useLatest from './useLatest';
9
10
  export default function useAsaResults(options) {
10
11
  const contextValue = useCioAsaContext();
@@ -53,12 +54,12 @@ export default function useAsaResults(options) {
53
54
  });
54
55
  const { beginTurn, screenOwner } = store;
55
56
  useEffect(() => cancelStream, [cancelStream]);
56
- const sendMessage = useCallback((text, source = 'input') => {
57
+ const sendToCurrentThread = useCallback((text, source) => {
57
58
  const intent = text.trim();
58
59
  if (!intent || session.isStreaming || session.foreignInFlight)
59
60
  return;
60
61
  beginTurn();
61
- trackingRef.current.trackSubmit(intent);
62
+ trackingRef.current.trackSubmit(intent, source);
62
63
  callbacksRef.current?.onAssistantSubmit?.({ intent, source });
63
64
  const userMessage = {
64
65
  id: nextMessageId(session),
@@ -108,6 +109,32 @@ export default function useAsaResults(options) {
108
109
  callbacksRef,
109
110
  staticRequestConfigsRef,
110
111
  ]);
112
+ const { newThread, switchThread } = store;
113
+ /**
114
+ * Sends `text`. With `threadId`, first moves to that conversation, loading it from storage
115
+ * when persistence has it and otherwise continuing it on the server with an empty screen.
116
+ */
117
+ const sendMessage = useCallback((text, source = 'input', { threadId } = {}) => {
118
+ if (!threadId ||
119
+ threadId === session.serverThreadId ||
120
+ threadId === session.storageThreadId) {
121
+ sendToCurrentThread(text, source);
122
+ return;
123
+ }
124
+ (async () => {
125
+ if (persistence)
126
+ await switchThread(threadId);
127
+ else
128
+ newThread();
129
+ // A turn that started meanwhile (or one still streaming in the background) owns the screen.
130
+ if (session.isStreaming || session.foreignInFlight)
131
+ return;
132
+ if (!session.serverThreadId && !isLocalThreadId(threadId)) {
133
+ session.serverThreadId = threadId;
134
+ }
135
+ sendToCurrentThread(text, source);
136
+ })();
137
+ }, [session, persistence, newThread, switchThread, sendToCurrentThread]);
111
138
  /**
112
139
  * Cancels the in-flight request while keeping the conversation: the partial reply is
113
140
  * settled as `done` (so the typing indicator stops and streamed text survives) and the
@@ -152,7 +179,7 @@ export default function useAsaResults(options) {
152
179
  isHydrating: store.isHydrating,
153
180
  threads: store.threads,
154
181
  activeThreadId: store.activeThreadId,
155
- newThread: store.newThread,
156
- switchThread: store.switchThread,
182
+ newThread,
183
+ switchThread,
157
184
  };
158
185
  }
@@ -1,5 +1,14 @@
1
1
  import { useCallback, useMemo } from 'react';
2
+ /** Beacon `source` values, aligned with the ones the behavioral-actions API documents. */
3
+ const BEACON_SUBMIT_SOURCES = {
4
+ input: 'input',
5
+ suggestion: 'suggestion',
6
+ refinement: 'follow_up',
7
+ // A prompt the host page sent, typically a suggested question clicked outside the chat.
8
+ external: 'suggestion',
9
+ };
2
10
  const NOOP_TRACKING = {
11
+ trackAgentButtonClick: () => { },
3
12
  trackSubmit: () => { },
4
13
  trackResultLoadStarted: () => { },
5
14
  trackResultLoadFinished: () => { },
@@ -13,32 +22,45 @@ const NOOP_TRACKING = {
13
22
  * Mirrors the tracking-hook pattern used by constructorio-ui-pia.
14
23
  */
15
24
  export default function useAsaTracking({ tracker, section, threadId, }) {
16
- const assistant = tracker;
17
25
  const base = useMemo(() => ({
18
26
  ...(section && { section }),
19
27
  ...(threadId && { threadId }),
20
28
  }), [section, threadId]);
21
- const trackSubmit = useCallback((intent) => {
22
- assistant?.trackAssistantSubmit({ intent, ...base });
23
- }, [assistant, base]);
29
+ const trackAgentButtonClick = useCallback(({ mode, agentDomain, positionOnPage, pageType, instanceId }) => {
30
+ tracker?.trackAgentButtonClick?.({
31
+ mode,
32
+ agentDomain,
33
+ ...(positionOnPage && { positionOnPage }),
34
+ ...(pageType && { pageType }),
35
+ ...(instanceId && { instanceId }),
36
+ ...(section && { section }),
37
+ });
38
+ }, [tracker, section]);
39
+ const trackSubmit = useCallback((intent, source) => {
40
+ tracker?.trackAssistantSubmit({
41
+ intent,
42
+ ...(source && { source: BEACON_SUBMIT_SOURCES[source] }),
43
+ ...base,
44
+ });
45
+ }, [tracker, base]);
24
46
  const trackResultLoadStarted = useCallback(({ intent, intentResultId }) => {
25
- assistant?.trackAssistantResultLoadStarted({
47
+ tracker?.trackAssistantResultLoadStarted({
26
48
  intent,
27
49
  ...(intentResultId && { intentResultId }),
28
50
  ...base,
29
51
  });
30
- }, [assistant, base]);
52
+ }, [tracker, base]);
31
53
  const trackResultLoadFinished = useCallback(({ intent, searchResultCount, intentResultId, threadId: turnThreadId, }) => {
32
- assistant?.trackAssistantResultLoadFinished({
54
+ tracker?.trackAssistantResultLoadFinished({
33
55
  intent,
34
56
  searchResultCount,
35
57
  ...(intentResultId && { intentResultId }),
36
58
  ...base,
37
59
  ...(turnThreadId && { threadId: turnThreadId }),
38
60
  });
39
- }, [assistant, base]);
61
+ }, [tracker, base]);
40
62
  const trackResultClick = useCallback(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
41
- assistant?.trackAssistantResultClick({
63
+ tracker?.trackAssistantResultClick({
42
64
  intent,
43
65
  searchResultId,
44
66
  ...(intentResultId && { intentResultId }),
@@ -47,9 +69,9 @@ export default function useAsaTracking({ tracker, section, threadId, }) {
47
69
  ...(variationId && { variationId }),
48
70
  ...base,
49
71
  });
50
- }, [assistant, base]);
72
+ }, [tracker, base]);
51
73
  const trackResultView = useCallback(({ intent, searchResultId, numResultsViewed, intentResultId, items }) => {
52
- assistant?.trackAssistantResultView({
74
+ tracker?.trackAssistantResultView({
53
75
  intent,
54
76
  searchResultId,
55
77
  numResultsViewed,
@@ -57,9 +79,9 @@ export default function useAsaTracking({ tracker, section, threadId, }) {
57
79
  ...(items && items.length > 0 && { items }),
58
80
  ...base,
59
81
  });
60
- }, [assistant, base]);
82
+ }, [tracker, base]);
61
83
  const trackSearchSubmit = useCallback(({ intent, searchTerm, userInput, searchResultId, intentResultId, groupId, }) => {
62
- assistant?.trackAssistantSearchSubmit({
84
+ tracker?.trackAssistantSearchSubmit({
63
85
  intent,
64
86
  searchTerm,
65
87
  userInput,
@@ -68,11 +90,12 @@ export default function useAsaTracking({ tracker, section, threadId, }) {
68
90
  ...(groupId && { groupId }),
69
91
  ...base,
70
92
  });
71
- }, [assistant, base]);
93
+ }, [tracker, base]);
72
94
  return useMemo(() => {
73
- if (!assistant)
95
+ if (!tracker)
74
96
  return NOOP_TRACKING;
75
97
  return {
98
+ trackAgentButtonClick,
76
99
  trackSubmit,
77
100
  trackResultLoadStarted,
78
101
  trackResultLoadFinished,
@@ -81,7 +104,8 @@ export default function useAsaTracking({ tracker, section, threadId, }) {
81
104
  trackSearchSubmit,
82
105
  };
83
106
  }, [
84
- assistant,
107
+ tracker,
108
+ trackAgentButtonClick,
85
109
  trackSubmit,
86
110
  trackResultLoadStarted,
87
111
  trackResultLoadFinished,
@@ -1 +1 @@
1
- export default '1.8.0';
1
+ export default '1.9.0';
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
- export interface ButtonProps {
2
+ import { AgentButtonClickPlacement } from '../../types';
3
+ export interface ButtonProps extends AgentButtonClickPlacement {
3
4
  /** Color scheme: dark (for light backgrounds) or light (for dark backgrounds) */
4
5
  theme?: 'dark' | 'light';
5
6
  /** Button size: sm (small) or lg (large) */
@@ -9,4 +10,4 @@ export interface ButtonProps {
9
10
  /** Button label text */
10
11
  label?: string;
11
12
  }
12
- export default function Button({ theme, size, onClick, label, }: ButtonProps): React.JSX.Element;
13
+ export default function Button({ theme, size, onClick, label, positionOnPage, pageType, instanceId, }: ButtonProps): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { ChatComponentOverrides, ResultGroupMeta, ThreadSummary, Translations } from '../../types';
2
+ import { ChatComponentOverrides, ResultGroupMeta, SendMessageOptions, ThreadSummary, Translations } from '../../types';
3
3
  import { Product, NormalizeOptions } from '../../utils/productNormalizer';
4
4
  import { AspectRatio } from '../ResultsBlock/ResultsBlock';
5
5
  export interface ChatHandle {
@@ -15,6 +15,12 @@ export interface ChatHandle {
15
15
  newThread: () => void;
16
16
  /** Load a stored conversation. No-op when persistence is off. */
17
17
  switchThread: (threadId: string) => Promise<void>;
18
+ /**
19
+ * Send a prompt from outside the chat, e.g. one the shopper clicked elsewhere on the page.
20
+ * Pass `threadId` to send it into that conversation instead of the one on screen. Skipped
21
+ * while an answer is streaming. Opening the chat stays with you, as with `onClose`.
22
+ */
23
+ sendMessage: (text: string, options?: SendMessageOptions) => void;
18
24
  }
19
25
  export interface ChatProps {
20
26
  /** Called when the close button (✕) is clicked. The consumer controls visibility. */
@@ -46,6 +52,12 @@ export interface ChatProps {
46
52
  translations?: Translations;
47
53
  /** Seed the thread id (e.g. loaded from browser storage) to resume a prior conversation. Read once on mount. */
48
54
  initialThreadId?: string;
55
+ /**
56
+ * A prompt sent once on mount, after any stored conversation loads, e.g. one the shopper
57
+ * clicked elsewhere before you opened the chat. Goes into `initialThreadId` when set.
58
+ * Read once on mount; use `sendMessage` on the ref once the chat is already open.
59
+ */
60
+ initialPrompt?: string;
49
61
  /** Fires with the stored conversations and the active one whenever either changes. Requires persistence. */
50
62
  onThreadsChange?: (threads: ThreadSummary[], activeThreadId: string | null) => void;
51
63
  /**
@@ -1,5 +1,5 @@
1
1
  import { Tracker } from '@constructor-io/constructorio-client-javascript/lib/types/constructorio';
2
- import { AssistantTrackedItem } from '../types';
2
+ import { AgentButtonClickPayload, AssistantSubmitSource, AssistantTrackedItem } from '../types';
3
3
  export interface UseAsaTrackingProps {
4
4
  tracker?: Tracker;
5
5
  section?: string;
@@ -40,8 +40,10 @@ export interface TrackSearchSubmitArgs {
40
40
  intentResultId?: string;
41
41
  groupId?: string;
42
42
  }
43
+ export type TrackAgentButtonClickArgs = AgentButtonClickPayload;
43
44
  export interface UseAsaTrackingReturn {
44
- trackSubmit: (intent: string) => void;
45
+ trackAgentButtonClick: (args: TrackAgentButtonClickArgs) => void;
46
+ trackSubmit: (intent: string, source?: AssistantSubmitSource) => void;
45
47
  trackResultLoadStarted: (args: TrackResultLoadStartedArgs) => void;
46
48
  trackResultLoadFinished: (args: TrackResultLoadFinishedArgs) => void;
47
49
  trackResultClick: (args: TrackResultClickArgs) => void;
@@ -10,9 +10,9 @@ export type { ButtonProps } from './components/Button/Button';
10
10
  export { default as useAsaResults } from './hooks/useAsaResults';
11
11
  export { default as useAsaTracking } from './hooks/useAsaTracking';
12
12
  export { default as useCioAsaContext } from './hooks/useCioAsaContext';
13
- export type { UseAsaTrackingProps, UseAsaTrackingReturn } from './hooks/useAsaTracking';
13
+ export type { UseAsaTrackingProps, UseAsaTrackingReturn, TrackAgentButtonClickArgs, } from './hooks/useAsaTracking';
14
14
  export { normalizeItemToProduct } from './utils/productNormalizer';
15
15
  export { clearPersistedConversations } from './utils/localStoragePersistence';
16
16
  export type { Product, NormalizeOptions } from './utils/productNormalizer';
17
17
  export { ConstructorIOClient } from './types';
18
- export type { Nullable, CioAsaProviderProps, PersistConversationOptions, AsaContextValue, CioClientOptions, RequestConfigs, Formatters, UrlHelpers, QueryParamEncodingOptions, DefaultQueryStringMap, Translations, ChatMessage, ChatMessageStatus, FollowUpRefinement, ResultGroup, ResultGroupMeta, SearchResultEventRequest, UseAsaResultsOptions, UseChatReturn, ClearPersistedConversationsOptions, ThreadSummary, AsaCallbacks, AssistantSubmitSource, AssistantTrackedItem, ChatComponentOverrides, WelcomeScreenOverrides, ChatInputOverrides, AiMessageOverrides, ResultsBlockOverrides, ComponentOverrideProps, IncludeComponentOverrides, ChatHeaderRenderProps, ChatInputRenderProps, WelcomeScreenTitleRenderProps, WelcomeScreenInputRenderProps, SuggestedQuestionsRenderProps, UserMessageRenderProps, AiMessageLoaderRenderProps, AiMessageTextRenderProps, FollowUpRefinementRenderProps, ResultsGroupTitleRenderProps, ResultsViewMoreRenderProps, } from './types';
18
+ export type { Nullable, CioAsaProviderProps, PersistConversationOptions, AsaContextValue, CioClientOptions, RequestConfigs, Formatters, UrlHelpers, QueryParamEncodingOptions, DefaultQueryStringMap, Translations, ChatMessage, ChatMessageStatus, FollowUpRefinement, ResultGroup, ResultGroupMeta, SearchResultEventRequest, UseAsaResultsOptions, UseChatReturn, SendMessageOptions, ClearPersistedConversationsOptions, ThreadSummary, AsaCallbacks, AssistantSubmitSource, AssistantTrackedItem, AgentButtonClickMode, AgentButtonClickPayload, AgentButtonClickPageType, AgentButtonClickPlacement, ChatComponentOverrides, WelcomeScreenOverrides, ChatInputOverrides, AiMessageOverrides, ResultsBlockOverrides, ComponentOverrideProps, IncludeComponentOverrides, ChatHeaderRenderProps, ChatInputRenderProps, WelcomeScreenTitleRenderProps, WelcomeScreenInputRenderProps, SuggestedQuestionsRenderProps, UserMessageRenderProps, AiMessageLoaderRenderProps, AiMessageTextRenderProps, FollowUpRefinementRenderProps, ResultsGroupTitleRenderProps, ResultsViewMoreRenderProps, } from './types';
@@ -179,9 +179,18 @@ export interface ResultGroup {
179
179
  searchResultId?: string;
180
180
  intentResultId?: string;
181
181
  }
182
+ export interface SendMessageOptions {
183
+ /**
184
+ * Conversation to send into, e.g. one started elsewhere on the page. The chat moves to it
185
+ * first: loaded from storage when persistence has it, otherwise continued on the server
186
+ * with an empty screen. Omit to send into the conversation on screen.
187
+ */
188
+ threadId?: string;
189
+ }
182
190
  export interface UseChatReturn {
183
191
  messages: ChatMessage[];
184
- sendMessage: (text: string, source?: AssistantSubmitSource) => void;
192
+ /** Skipped while another answer streams. */
193
+ sendMessage: (text: string, source?: AssistantSubmitSource, options?: SendMessageOptions) => void;
185
194
  /** True while an answer is streaming, here or, with persistence on, in another tab on the same thread. */
186
195
  isStreaming: boolean;
187
196
  /** True while this tab's own answer is streaming, the one `abort` can stop. */
@@ -289,20 +298,44 @@ export interface LocalStoragePersistenceOptions {
289
298
  /** Storage to use instead of the browser's; takes precedence over `storageArea`. */
290
299
  storage?: Storage;
291
300
  }
292
- /** How an intent was submitted: typed input, a welcome-screen suggestion chip, or a refinement chip. */
293
- export type AssistantSubmitSource = 'input' | 'suggestion' | 'refinement';
301
+ /**
302
+ * How an intent was submitted: typed input, a welcome-screen suggestion chip, a refinement chip,
303
+ * or the host page (`ChatHandle.sendMessage` or the `initialPrompt` prop).
304
+ */
305
+ export type AssistantSubmitSource = 'input' | 'suggestion' | 'refinement' | 'external';
294
306
  /** An item within a viewed/clicked search_result pod. */
295
307
  export interface AssistantTrackedItem {
296
308
  itemId?: string;
297
309
  itemName?: string;
298
310
  variationId?: string;
299
311
  }
312
+ /** Agent mode a CTA opens or starts. */
313
+ export type AgentButtonClickMode = 'chat' | 'search' | 'qna' | 'recommendations' | 'query_refinement';
314
+ /** Page surface an agent CTA was clicked on. */
315
+ export type AgentButtonClickPageType = 'home' | 'plp' | 'pdp' | 'search' | 'collection' | 'email_campaign' | 'cart';
316
+ /** Where an agent CTA sits on the page. All fields are optional. */
317
+ export interface AgentButtonClickPlacement {
318
+ /** Stable label describing where the CTA is placed, e.g. `'header'` or `'search_bar'`. */
319
+ positionOnPage?: string;
320
+ /** Page surface where the CTA was clicked. */
321
+ pageType?: AgentButtonClickPageType;
322
+ /** 1-based index distinguishing CTA instances with the same mode and position during one page view. */
323
+ instanceId?: number;
324
+ }
325
+ /** Fields sent with `ai_agent_button_click` and passed to `onAgentButtonClick`. */
326
+ export interface AgentButtonClickPayload extends AgentButtonClickPlacement {
327
+ mode: AgentButtonClickMode;
328
+ /** Agent experience the CTA opens, sent as `agent_domain`. */
329
+ agentDomain: string;
330
+ }
300
331
  /**
301
332
  * Optional consumer callbacks fired alongside the built-in behavioral tracking.
302
333
  * Each fires immediately after its corresponding `trackAssistant*` beacon is sent,
303
334
  * so consumers can mirror ASA analytics into their own systems. All are optional.
304
335
  */
305
336
  export interface AsaCallbacks {
337
+ /** A CTA that opens the assistant (the built-in `Button`) was clicked. */
338
+ onAgentButtonClick?: (payload: AgentButtonClickPayload) => void;
306
339
  /** User submitted an intent (typed) or clicked a suggestion / refinement chip. */
307
340
  onAssistantSubmit?: (payload: {
308
341
  intent: string;