@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.
- package/dist/constructorio-ui-asa-bundled.js +14 -14
- package/lib/cjs/components/Button/Button.js +22 -3
- package/lib/cjs/components/Chat/Chat.js +16 -3
- package/lib/cjs/hooks/useAsaResults.js +31 -4
- package/lib/cjs/hooks/useAsaTracking.js +31 -16
- package/lib/cjs/version.js +1 -1
- package/lib/mjs/components/Button/Button.js +26 -3
- package/lib/mjs/components/Chat/Chat.js +16 -3
- package/lib/mjs/hooks/useAsaResults.js +31 -4
- package/lib/mjs/hooks/useAsaTracking.js +40 -16
- package/lib/mjs/version.js +1 -1
- package/lib/types/components/Button/Button.d.ts +3 -2
- package/lib/types/components/Chat/Chat.d.ts +13 -1
- package/lib/types/hooks/useAsaTracking.d.ts +4 -2
- package/lib/types/index.d.ts +2 -2
- package/lib/types/types.d.ts +36 -3
- package/lib/types/version.d.ts +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
-
|
|
8
|
-
|
|
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, () => ({
|
|
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
|
|
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
|
|
163
|
-
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
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
}, [
|
|
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
|
-
|
|
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
|
-
}, [
|
|
42
|
+
}, [tracker, base]);
|
|
30
43
|
const trackResultClick = (0, react_1.useCallback)(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
|
|
31
|
-
|
|
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
|
-
}, [
|
|
46
|
+
}, [tracker, base]);
|
|
34
47
|
const trackResultView = (0, react_1.useCallback)(({ intent, searchResultId, numResultsViewed, intentResultId, items }) => {
|
|
35
|
-
|
|
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
|
-
}, [
|
|
51
|
+
}, [tracker, base]);
|
|
39
52
|
const trackSearchSubmit = (0, react_1.useCallback)(({ intent, searchTerm, userInput, searchResultId, intentResultId, groupId, }) => {
|
|
40
|
-
|
|
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
|
-
}, [
|
|
57
|
+
}, [tracker, base]);
|
|
45
58
|
return (0, react_1.useMemo)(() => {
|
|
46
|
-
if (!
|
|
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
|
-
|
|
71
|
+
tracker,
|
|
72
|
+
trackAgentButtonClick,
|
|
58
73
|
trackSubmit,
|
|
59
74
|
trackResultLoadStarted,
|
|
60
75
|
trackResultLoadFinished,
|
package/lib/cjs/version.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
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, () => ({
|
|
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
|
|
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
|
|
156
|
-
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
47
|
+
tracker?.trackAssistantResultLoadStarted({
|
|
26
48
|
intent,
|
|
27
49
|
...(intentResultId && { intentResultId }),
|
|
28
50
|
...base,
|
|
29
51
|
});
|
|
30
|
-
}, [
|
|
52
|
+
}, [tracker, base]);
|
|
31
53
|
const trackResultLoadFinished = useCallback(({ intent, searchResultCount, intentResultId, threadId: turnThreadId, }) => {
|
|
32
|
-
|
|
54
|
+
tracker?.trackAssistantResultLoadFinished({
|
|
33
55
|
intent,
|
|
34
56
|
searchResultCount,
|
|
35
57
|
...(intentResultId && { intentResultId }),
|
|
36
58
|
...base,
|
|
37
59
|
...(turnThreadId && { threadId: turnThreadId }),
|
|
38
60
|
});
|
|
39
|
-
}, [
|
|
61
|
+
}, [tracker, base]);
|
|
40
62
|
const trackResultClick = useCallback(({ intent, searchResultId, intentResultId, itemId, itemName, variationId, }) => {
|
|
41
|
-
|
|
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
|
-
}, [
|
|
72
|
+
}, [tracker, base]);
|
|
51
73
|
const trackResultView = useCallback(({ intent, searchResultId, numResultsViewed, intentResultId, items }) => {
|
|
52
|
-
|
|
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
|
-
}, [
|
|
82
|
+
}, [tracker, base]);
|
|
61
83
|
const trackSearchSubmit = useCallback(({ intent, searchTerm, userInput, searchResultId, intentResultId, groupId, }) => {
|
|
62
|
-
|
|
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
|
-
}, [
|
|
93
|
+
}, [tracker, base]);
|
|
72
94
|
return useMemo(() => {
|
|
73
|
-
if (!
|
|
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
|
-
|
|
107
|
+
tracker,
|
|
108
|
+
trackAgentButtonClick,
|
|
85
109
|
trackSubmit,
|
|
86
110
|
trackResultLoadStarted,
|
|
87
111
|
trackResultLoadFinished,
|
package/lib/mjs/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '1.
|
|
1
|
+
export default '1.9.0';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
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
|
-
|
|
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;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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';
|
package/lib/types/types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
293
|
-
|
|
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;
|