@constructor-io/constructorio-ui-asa 1.0.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/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/constructorio-ui-asa-bundled.js +33 -0
- package/lib/cjs/components/Button/Button.css +61 -0
- package/lib/cjs/components/Button/Button.js +12 -0
- package/lib/cjs/components/Chat/AiMessage.css +35 -0
- package/lib/cjs/components/Chat/AiMessage.js +34 -0
- package/lib/cjs/components/Chat/Chat.css +103 -0
- package/lib/cjs/components/Chat/Chat.js +34 -0
- package/lib/cjs/components/Chat/ChatHeader.css +41 -0
- package/lib/cjs/components/Chat/ChatHeader.js +18 -0
- package/lib/cjs/components/Chat/ChatInput.css +83 -0
- package/lib/cjs/components/Chat/ChatInput.js +30 -0
- package/lib/cjs/components/Chat/ChatMessageList.css +23 -0
- package/lib/cjs/components/Chat/ChatMessageList.js +46 -0
- package/lib/cjs/components/Chat/TypingIndicator.css +51 -0
- package/lib/cjs/components/Chat/TypingIndicator.js +14 -0
- package/lib/cjs/components/Chat/UserMessage.css +25 -0
- package/lib/cjs/components/Chat/UserMessage.js +14 -0
- package/lib/cjs/components/Chat/WelcomeScreen.css +205 -0
- package/lib/cjs/components/Chat/WelcomeScreen.js +43 -0
- package/lib/cjs/components/CioAsaProvider/CioAsaProvider.js +23 -0
- package/lib/cjs/components/ResultsBlock/ResultsBlock.css +217 -0
- package/lib/cjs/components/ResultsBlock/ResultsBlock.js +73 -0
- package/lib/cjs/components/ResultsBlock/constants.js +11 -0
- package/lib/cjs/components/ResultsBlock/renderProductCard.js +21 -0
- package/lib/cjs/components/icons/ArrowRightIcon.js +9 -0
- package/lib/cjs/components/icons/ChatBubbleDarkIcon.js +9 -0
- package/lib/cjs/components/icons/ChatBubbleLightIcon.js +9 -0
- package/lib/cjs/components/icons/SendArrowIcon.js +9 -0
- package/lib/cjs/components/icons/SendPlaneIcon.js +9 -0
- package/lib/cjs/components/icons/index.js +16 -0
- package/lib/cjs/constants.js +6 -0
- package/lib/cjs/hooks/asaStreamHandlers.js +34 -0
- package/lib/cjs/hooks/useAsaResults.js +120 -0
- package/lib/cjs/hooks/useCioAsaContext.js +15 -0
- package/lib/cjs/hooks/useCioClient.js +21 -0
- package/lib/cjs/hooks/useFocusTrap.js +38 -0
- package/lib/cjs/hooks/useMessageInput.js +22 -0
- package/lib/cjs/hooks/useRequestConfigs.js +27 -0
- package/lib/cjs/index.js +24 -0
- package/lib/cjs/styles.css +34 -0
- package/lib/cjs/types.js +6 -0
- package/lib/cjs/utils/formatters.js +11 -0
- package/lib/cjs/utils/productNormalizer.js +20 -0
- package/lib/cjs/utils/translate.js +30 -0
- package/lib/cjs/utils/typeHelpers.js +2 -0
- package/lib/cjs/utils/urlHelpers.js +102 -0
- package/lib/cjs/version.js +3 -0
- package/lib/mjs/components/Button/Button.css +61 -0
- package/lib/mjs/components/Button/Button.js +8 -0
- package/lib/mjs/components/Chat/AiMessage.css +35 -0
- package/lib/mjs/components/Chat/AiMessage.js +29 -0
- package/lib/mjs/components/Chat/Chat.css +103 -0
- package/lib/mjs/components/Chat/Chat.js +31 -0
- package/lib/mjs/components/Chat/ChatHeader.css +41 -0
- package/lib/mjs/components/Chat/ChatHeader.js +14 -0
- package/lib/mjs/components/Chat/ChatInput.css +83 -0
- package/lib/mjs/components/Chat/ChatInput.js +26 -0
- package/lib/mjs/components/Chat/ChatMessageList.css +23 -0
- package/lib/mjs/components/Chat/ChatMessageList.js +41 -0
- package/lib/mjs/components/Chat/TypingIndicator.css +51 -0
- package/lib/mjs/components/Chat/TypingIndicator.js +10 -0
- package/lib/mjs/components/Chat/UserMessage.css +25 -0
- package/lib/mjs/components/Chat/UserMessage.js +10 -0
- package/lib/mjs/components/Chat/WelcomeScreen.css +205 -0
- package/lib/mjs/components/Chat/WelcomeScreen.js +38 -0
- package/lib/mjs/components/CioAsaProvider/CioAsaProvider.js +19 -0
- package/lib/mjs/components/ResultsBlock/ResultsBlock.css +217 -0
- package/lib/mjs/components/ResultsBlock/ResultsBlock.js +76 -0
- package/lib/mjs/components/ResultsBlock/constants.js +8 -0
- package/lib/mjs/components/ResultsBlock/renderProductCard.js +17 -0
- package/lib/mjs/components/icons/ArrowRightIcon.js +5 -0
- package/lib/mjs/components/icons/ChatBubbleDarkIcon.js +5 -0
- package/lib/mjs/components/icons/ChatBubbleLightIcon.js +5 -0
- package/lib/mjs/components/icons/SendArrowIcon.js +5 -0
- package/lib/mjs/components/icons/SendPlaneIcon.js +5 -0
- package/lib/mjs/components/icons/index.js +5 -0
- package/lib/mjs/constants.js +3 -0
- package/lib/mjs/hooks/asaStreamHandlers.js +39 -0
- package/lib/mjs/hooks/useAsaResults.js +118 -0
- package/lib/mjs/hooks/useCioAsaContext.js +11 -0
- package/lib/mjs/hooks/useCioClient.js +23 -0
- package/lib/mjs/hooks/useFocusTrap.js +35 -0
- package/lib/mjs/hooks/useMessageInput.js +19 -0
- package/lib/mjs/hooks/useRequestConfigs.js +24 -0
- package/lib/mjs/index.js +11 -0
- package/lib/mjs/styles.css +34 -0
- package/lib/mjs/types.js +2 -0
- package/lib/mjs/utils/formatters.js +7 -0
- package/lib/mjs/utils/productNormalizer.js +15 -0
- package/lib/mjs/utils/translate.js +26 -0
- package/lib/mjs/utils/typeHelpers.js +1 -0
- package/lib/mjs/utils/urlHelpers.js +90 -0
- package/lib/mjs/version.js +1 -0
- package/lib/types/components/Button/Button.d.ts +12 -0
- package/lib/types/components/Chat/AiMessage.d.ts +12 -0
- package/lib/types/components/Chat/Chat.d.ts +38 -0
- package/lib/types/components/Chat/ChatHeader.d.ts +12 -0
- package/lib/types/components/Chat/ChatInput.d.ts +13 -0
- package/lib/types/components/Chat/ChatMessageList.d.ts +24 -0
- package/lib/types/components/Chat/TypingIndicator.d.ts +10 -0
- package/lib/types/components/Chat/UserMessage.d.ts +12 -0
- package/lib/types/components/Chat/WelcomeScreen.d.ts +16 -0
- package/lib/types/components/CioAsaProvider/CioAsaProvider.d.ts +3 -0
- package/lib/types/components/ResultsBlock/ResultsBlock.d.ts +32 -0
- package/lib/types/components/ResultsBlock/constants.d.ts +3 -0
- package/lib/types/components/ResultsBlock/renderProductCard.d.ts +12 -0
- package/lib/types/components/icons/ArrowRightIcon.d.ts +2 -0
- package/lib/types/components/icons/ChatBubbleDarkIcon.d.ts +2 -0
- package/lib/types/components/icons/ChatBubbleLightIcon.d.ts +2 -0
- package/lib/types/components/icons/SendArrowIcon.d.ts +2 -0
- package/lib/types/components/icons/SendPlaneIcon.d.ts +2 -0
- package/lib/types/components/icons/index.d.ts +5 -0
- package/lib/types/constants.d.ts +2 -0
- package/lib/types/hooks/asaStreamHandlers.d.ts +8 -0
- package/lib/types/hooks/useAsaResults.d.ts +2 -0
- package/lib/types/hooks/useCioAsaContext.d.ts +9 -0
- package/lib/types/hooks/useCioClient.d.ts +6 -0
- package/lib/types/hooks/useFocusTrap.d.ts +11 -0
- package/lib/types/hooks/useMessageInput.d.ts +15 -0
- package/lib/types/hooks/useRequestConfigs.d.ts +7 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/types.d.ts +178 -0
- package/lib/types/utils/formatters.d.ts +1 -0
- package/lib/types/utils/productNormalizer.d.ts +14 -0
- package/lib/types/utils/translate.d.ts +2 -0
- package/lib/types/utils/typeHelpers.d.ts +4 -0
- package/lib/types/utils/urlHelpers.d.ts +8 -0
- package/lib/types/version.d.ts +2 -0
- package/package.json +150 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useCioAsaContext } from './useCioAsaContext';
|
|
3
|
+
import { handleSearchResult, handleMessage, handleServerError, handleStreamEnd, handleStreamError, } from './asaStreamHandlers';
|
|
4
|
+
export default function useAsaResults() {
|
|
5
|
+
const [messages, setMessages] = useState([]);
|
|
6
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
7
|
+
const isStreamingRef = useRef(false);
|
|
8
|
+
const killSwitchRef = useRef(false);
|
|
9
|
+
const readerRef = useRef(null);
|
|
10
|
+
const threadIdRef = useRef(null);
|
|
11
|
+
const idCounterRef = useRef(0);
|
|
12
|
+
const contextValue = useCioAsaContext();
|
|
13
|
+
if (!contextValue) {
|
|
14
|
+
throw new Error('useAsaResults must be used within a CioAsaProvider.');
|
|
15
|
+
}
|
|
16
|
+
const { cioClient, staticRequestConfigs } = contextValue;
|
|
17
|
+
const { domain } = staticRequestConfigs || {};
|
|
18
|
+
if (!cioClient || !domain) {
|
|
19
|
+
throw new Error('useAsaResults requires a configured cioClient and domain. Check your CioAsaProvider props.');
|
|
20
|
+
}
|
|
21
|
+
const nextMessageId = useCallback(() => {
|
|
22
|
+
idCounterRef.current += 1;
|
|
23
|
+
return `msg-${idCounterRef.current}-${Date.now()}`;
|
|
24
|
+
}, []);
|
|
25
|
+
const sendMessage = useCallback((text) => {
|
|
26
|
+
if (!text.trim() || isStreamingRef.current)
|
|
27
|
+
return;
|
|
28
|
+
const userMessage = {
|
|
29
|
+
id: nextMessageId(),
|
|
30
|
+
role: 'user',
|
|
31
|
+
text: text.trim(),
|
|
32
|
+
status: 'done',
|
|
33
|
+
};
|
|
34
|
+
const assistantMessage = {
|
|
35
|
+
id: nextMessageId(),
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
text: '',
|
|
38
|
+
groups: [],
|
|
39
|
+
status: 'loading',
|
|
40
|
+
};
|
|
41
|
+
setMessages((prev) => [...prev, userMessage, assistantMessage]);
|
|
42
|
+
setIsStreaming(true);
|
|
43
|
+
isStreamingRef.current = true;
|
|
44
|
+
killSwitchRef.current = false;
|
|
45
|
+
const stream = cioClient.agent.getAgentResultsStream(text.trim(), {
|
|
46
|
+
domain,
|
|
47
|
+
...(threadIdRef.current && { threadId: threadIdRef.current }),
|
|
48
|
+
});
|
|
49
|
+
const reader = stream.getReader();
|
|
50
|
+
readerRef.current = reader;
|
|
51
|
+
(async () => {
|
|
52
|
+
let pendingGroup = null;
|
|
53
|
+
try {
|
|
54
|
+
// eslint-disable-next-line no-await-in-loop
|
|
55
|
+
while (!killSwitchRef.current) {
|
|
56
|
+
// eslint-disable-next-line no-await-in-loop
|
|
57
|
+
const res = await reader.read();
|
|
58
|
+
if (killSwitchRef.current)
|
|
59
|
+
break;
|
|
60
|
+
if (res.done) {
|
|
61
|
+
handleStreamEnd(assistantMessage.id, setMessages);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
const { type, data } = res.value;
|
|
65
|
+
if (type === 'start' && data?.thread_id) {
|
|
66
|
+
threadIdRef.current = data.thread_id;
|
|
67
|
+
}
|
|
68
|
+
else if (type === 'group') {
|
|
69
|
+
pendingGroup = {
|
|
70
|
+
display_name: data?.display_name ?? data?.group ?? '',
|
|
71
|
+
value: data?.value ?? data?.group ?? '',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else if (type === 'search_result') {
|
|
75
|
+
pendingGroup = handleSearchResult(data, pendingGroup, assistantMessage.id, setMessages);
|
|
76
|
+
}
|
|
77
|
+
else if (type === 'message') {
|
|
78
|
+
handleMessage(data, assistantMessage.id, setMessages);
|
|
79
|
+
}
|
|
80
|
+
else if (type === 'server_error') {
|
|
81
|
+
handleServerError(assistantMessage.id, setMessages);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
handleStreamError(assistantMessage.id, setMessages);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (readerRef.current === reader) {
|
|
91
|
+
reader.cancel();
|
|
92
|
+
readerRef.current = null;
|
|
93
|
+
setIsStreaming(false);
|
|
94
|
+
isStreamingRef.current = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}, [cioClient, domain, nextMessageId]);
|
|
99
|
+
useEffect(() => () => {
|
|
100
|
+
killSwitchRef.current = true;
|
|
101
|
+
if (readerRef.current) {
|
|
102
|
+
readerRef.current.cancel();
|
|
103
|
+
readerRef.current = null;
|
|
104
|
+
}
|
|
105
|
+
}, []);
|
|
106
|
+
const clearHistory = useCallback(() => {
|
|
107
|
+
killSwitchRef.current = true;
|
|
108
|
+
if (readerRef.current) {
|
|
109
|
+
readerRef.current.cancel();
|
|
110
|
+
readerRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
threadIdRef.current = null;
|
|
113
|
+
setMessages([]);
|
|
114
|
+
setIsStreaming(false);
|
|
115
|
+
isStreamingRef.current = false;
|
|
116
|
+
}, []);
|
|
117
|
+
return { messages, sendMessage, isStreaming, clearHistory };
|
|
118
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const AsaContext = createContext(null);
|
|
3
|
+
AsaContext.displayName = 'AsaContext';
|
|
4
|
+
/**
|
|
5
|
+
* React Hook to access state provided by CioAsa provider.
|
|
6
|
+
* Note: Should only be used by components nested under a CioAsa provider
|
|
7
|
+
*/
|
|
8
|
+
export function useCioAsaContext() {
|
|
9
|
+
return useContext(AsaContext);
|
|
10
|
+
}
|
|
11
|
+
export default useCioAsaContext;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ConstructorIOClient from '@constructor-io/constructorio-client-javascript';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import version from '../version';
|
|
4
|
+
const useCioClient = ({ apiKey, cioClient, cioClientOptions } = {}) => {
|
|
5
|
+
if (!apiKey && !cioClient) {
|
|
6
|
+
throw new Error('Api Key or Constructor Client required');
|
|
7
|
+
}
|
|
8
|
+
const memoizedCioClient = useMemo(() => {
|
|
9
|
+
if (cioClient)
|
|
10
|
+
return cioClient;
|
|
11
|
+
if (apiKey && typeof window !== 'undefined') {
|
|
12
|
+
return new ConstructorIOClient({
|
|
13
|
+
apiKey,
|
|
14
|
+
sendTrackingEvents: true,
|
|
15
|
+
version: `cio-ui-asa-${version}`,
|
|
16
|
+
...cioClientOptions,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}, [apiKey, cioClient, cioClientOptions]);
|
|
21
|
+
return memoizedCioClient;
|
|
22
|
+
};
|
|
23
|
+
export default useCioClient;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
3
|
+
/**
|
|
4
|
+
* Traps Tab/Shift+Tab focus within the referenced container and invokes
|
|
5
|
+
* onEscape when Escape is pressed.
|
|
6
|
+
*/
|
|
7
|
+
export default function useFocusTrap(containerRef, { onEscape } = {}) {
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const container = containerRef.current;
|
|
10
|
+
if (!container)
|
|
11
|
+
return undefined;
|
|
12
|
+
const handleKeyDown = (e) => {
|
|
13
|
+
if (e.key === 'Escape' && onEscape) {
|
|
14
|
+
onEscape();
|
|
15
|
+
}
|
|
16
|
+
if (e.key === 'Tab') {
|
|
17
|
+
const focusableElements = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
18
|
+
if (focusableElements.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const first = focusableElements[0];
|
|
21
|
+
const last = focusableElements[focusableElements.length - 1];
|
|
22
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
last.focus();
|
|
25
|
+
}
|
|
26
|
+
else if (!e.shiftKey && document.activeElement === last) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
first.focus();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
container.addEventListener('keydown', handleKeyDown);
|
|
33
|
+
return () => container.removeEventListener('keydown', handleKeyDown);
|
|
34
|
+
}, [containerRef, onEscape]);
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
export default function useMessageInput({ onSend, isDisabled = false, submitOnEnterOnly = false, }) {
|
|
3
|
+
const [value, setValue] = useState('');
|
|
4
|
+
const handleSubmit = () => {
|
|
5
|
+
if (isDisabled || !value.trim())
|
|
6
|
+
return;
|
|
7
|
+
onSend(value.trim());
|
|
8
|
+
setValue('');
|
|
9
|
+
};
|
|
10
|
+
const handleKeyDown = (e) => {
|
|
11
|
+
if (e.key !== 'Enter')
|
|
12
|
+
return;
|
|
13
|
+
if (submitOnEnterOnly && e.shiftKey)
|
|
14
|
+
return;
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
handleSubmit();
|
|
17
|
+
};
|
|
18
|
+
return { value, setValue, handleSubmit, handleKeyDown };
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCioAsaContext } from './useCioAsaContext';
|
|
2
|
+
export default function useRequestConfigs() {
|
|
3
|
+
const context = useCioAsaContext();
|
|
4
|
+
if (!context) {
|
|
5
|
+
throw new Error('This Hook needs to be called within the C.io ASA Context Provider.');
|
|
6
|
+
}
|
|
7
|
+
const { urlHelpers, staticRequestConfigs } = context;
|
|
8
|
+
const { getUrl, setUrl, getStateFromUrl, getUrlFromState } = urlHelpers;
|
|
9
|
+
const url = getUrl();
|
|
10
|
+
const urlRequestConfigs = url ? getStateFromUrl(url) : {};
|
|
11
|
+
const requestConfigs = { ...staticRequestConfigs, ...urlRequestConfigs };
|
|
12
|
+
const setRequestConfigs = (configsToUpdate) => {
|
|
13
|
+
const oldUrl = getUrl();
|
|
14
|
+
if (!oldUrl) {
|
|
15
|
+
throw new Error('getUrl returns undefined when attempting to call setRequestConfigs');
|
|
16
|
+
}
|
|
17
|
+
const urlObj = new URL(oldUrl);
|
|
18
|
+
const oldRequestConfigs = getStateFromUrl(oldUrl);
|
|
19
|
+
const newRequestConfigs = { ...oldRequestConfigs, ...configsToUpdate };
|
|
20
|
+
const newUrl = getUrlFromState(newRequestConfigs, urlObj);
|
|
21
|
+
setUrl(newUrl);
|
|
22
|
+
};
|
|
23
|
+
return { requestConfigs, setRequestConfigs };
|
|
24
|
+
}
|
package/lib/mjs/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export { default as CioAsaProvider } from './components/CioAsaProvider/CioAsaProvider';
|
|
3
|
+
export { default as Chat } from './components/Chat/Chat';
|
|
4
|
+
export { default as ResultsBlock } from './components/ResultsBlock/ResultsBlock';
|
|
5
|
+
export { default as Button } from './components/Button/Button';
|
|
6
|
+
// Hooks
|
|
7
|
+
export { default as useAsaResults } from './hooks/useAsaResults';
|
|
8
|
+
// Utils
|
|
9
|
+
export { normalizeItemToProduct } from './utils/productNormalizer';
|
|
10
|
+
// Re-exported client value from the JS client (used to construct/type a cioClient)
|
|
11
|
+
export { ConstructorIOClient } from './types';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@import '@constructor-io/constructorio-ui-components/styles.css';
|
|
2
|
+
@import './components/Button/Button.css';
|
|
3
|
+
@import './components/Chat/Chat.css';
|
|
4
|
+
@import './components/Chat/ChatHeader.css';
|
|
5
|
+
@import './components/Chat/WelcomeScreen.css';
|
|
6
|
+
@import './components/Chat/ChatMessageList.css';
|
|
7
|
+
@import './components/Chat/UserMessage.css';
|
|
8
|
+
@import './components/Chat/AiMessage.css';
|
|
9
|
+
@import './components/Chat/TypingIndicator.css';
|
|
10
|
+
@import './components/Chat/ChatInput.css';
|
|
11
|
+
@import './components/ResultsBlock/ResultsBlock.css';
|
|
12
|
+
|
|
13
|
+
.cio-asa,
|
|
14
|
+
.cio-asa * {
|
|
15
|
+
font-family: var(--cio-asa-font-family, 'Inter', sans-serif);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cio-sr-only {
|
|
19
|
+
position: absolute;
|
|
20
|
+
width: 1px;
|
|
21
|
+
height: 1px;
|
|
22
|
+
padding: 0;
|
|
23
|
+
margin: -1px;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
clip: rect(0, 0, 0, 0);
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
border: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cio-asa button:focus-visible,
|
|
31
|
+
.cio-asa [role='button']:focus-visible {
|
|
32
|
+
outline: 2px solid #A980FF;
|
|
33
|
+
outline-offset: 2px;
|
|
34
|
+
}
|
package/lib/mjs/types.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeItemToProduct(item, options) {
|
|
2
|
+
const data = item.data || {};
|
|
3
|
+
return {
|
|
4
|
+
id: String(data.id ?? data.result_id ?? item.value ?? ''),
|
|
5
|
+
name: String(item.value ?? data.item_name ?? ''),
|
|
6
|
+
url: data.url,
|
|
7
|
+
imageUrl: data.image_url ?? data.imageUrl,
|
|
8
|
+
price: data.price,
|
|
9
|
+
salePrice: data.sale_price ?? data.salePrice,
|
|
10
|
+
description: data.description,
|
|
11
|
+
badge: data.sale_price != null || data.salePrice != null
|
|
12
|
+
? options?.saleBadgeText ?? 'Sale'
|
|
13
|
+
: undefined,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const defaultTranslations = {
|
|
2
|
+
'CioAsa.header.title': 'Shopping Assistant',
|
|
3
|
+
'CioAsa.header.close': 'Close',
|
|
4
|
+
'CioAsa.input.placeholder': 'Ask a question about this product',
|
|
5
|
+
'CioAsa.input.ariaLabel': 'Type your message',
|
|
6
|
+
'CioAsa.input.sendAriaLabel': 'Send message',
|
|
7
|
+
'CioAsa.welcome.title': 'Shopping Assistant',
|
|
8
|
+
'CioAsa.welcome.placeholder': 'Ask anything',
|
|
9
|
+
'CioAsa.welcome.sendButton': 'Chat',
|
|
10
|
+
'CioAsa.welcome.inputAriaLabel': 'Type your question',
|
|
11
|
+
'CioAsa.welcome.sendAriaLabel': 'Send message',
|
|
12
|
+
'CioAsa.welcome.suggestionsAriaLabel': 'Suggested questions',
|
|
13
|
+
'CioAsa.messageList.ariaLabel': 'Chat messages',
|
|
14
|
+
'CioAsa.typingIndicator.ariaLabel': 'Assistant is typing',
|
|
15
|
+
'CioAsa.userMessage.ariaLabel': 'You said',
|
|
16
|
+
'CioAsa.results.viewMore': 'View more products',
|
|
17
|
+
'CioAsa.results.addToCart': 'Add to cart',
|
|
18
|
+
'CioAsa.results.saleBadge': 'Sale',
|
|
19
|
+
'CioAsa.error.message': "I can't assist you with that request.",
|
|
20
|
+
};
|
|
21
|
+
export default function translate(key, translations) {
|
|
22
|
+
if (translations?.[key] !== undefined) {
|
|
23
|
+
return translations[key];
|
|
24
|
+
}
|
|
25
|
+
return defaultTranslations[key] ?? key;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export const defaultQueryStringMap = Object.freeze({
|
|
2
|
+
intent: 'q',
|
|
3
|
+
page: 'page',
|
|
4
|
+
numResultsPerPage: 'resultsPerPod',
|
|
5
|
+
domain: 'domain',
|
|
6
|
+
filters: 'filters', // do special encoding/decoding
|
|
7
|
+
});
|
|
8
|
+
export function getUrl() {
|
|
9
|
+
if (typeof window === 'undefined')
|
|
10
|
+
return undefined;
|
|
11
|
+
return window.location.href;
|
|
12
|
+
}
|
|
13
|
+
export function setUrl(newUrlWithEncodedState) {
|
|
14
|
+
if (typeof window === 'undefined')
|
|
15
|
+
return;
|
|
16
|
+
window.location.href = newUrlWithEncodedState;
|
|
17
|
+
}
|
|
18
|
+
export function extractFiltersFromUrl(urlParams) {
|
|
19
|
+
const filters = {};
|
|
20
|
+
const filterRegex = new RegExp(`^${defaultQueryStringMap.filters}\\[(.*)\\]`);
|
|
21
|
+
urlParams?.forEach((value, key) => {
|
|
22
|
+
const filterKey = key?.match(filterRegex)?.[1];
|
|
23
|
+
if (filterKey) {
|
|
24
|
+
if (filters[filterKey]) {
|
|
25
|
+
filters[filterKey].push(value);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
filters[filterKey] = [value];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return Object.keys(filters).length ? filters : undefined;
|
|
33
|
+
}
|
|
34
|
+
export function getFilterParamsFromState(urlParams, filters) {
|
|
35
|
+
if (filters) {
|
|
36
|
+
Object.entries(filters)?.forEach(([filterName, filterValues]) => {
|
|
37
|
+
if (Array.isArray(filterValues)) {
|
|
38
|
+
filterValues.forEach((filterValue) => {
|
|
39
|
+
urlParams.append(`filters[${filterName}]`, String(filterValue));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
urlParams.append(`filters[${filterName}]`, String(filterValues));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function getStateFromUrl(url) {
|
|
49
|
+
const urlObject = new URL(url);
|
|
50
|
+
const urlParams = urlObject.searchParams;
|
|
51
|
+
const rawState = {};
|
|
52
|
+
Object.entries(defaultQueryStringMap).forEach(([key, val]) => {
|
|
53
|
+
const storedVal = urlParams.get(val);
|
|
54
|
+
if (storedVal != null) {
|
|
55
|
+
rawState[key] = storedVal;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const filters = extractFiltersFromUrl(urlParams);
|
|
59
|
+
const { page, numResultsPerPage, ...rest } = rawState;
|
|
60
|
+
const state = { ...rest };
|
|
61
|
+
if (numResultsPerPage)
|
|
62
|
+
state.numResultsPerPage = Number(numResultsPerPage);
|
|
63
|
+
if (filters)
|
|
64
|
+
state.filters = filters;
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
export function getUrlFromState(state, options = {}) {
|
|
68
|
+
const { baseUrl: url, origin, pathname } = options;
|
|
69
|
+
const baseUrl = url || `${origin}${pathname}`;
|
|
70
|
+
const params = new URLSearchParams();
|
|
71
|
+
Object.entries(state).forEach(([key, val]) => {
|
|
72
|
+
if (defaultQueryStringMap[key] === undefined) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let encodedVal = '';
|
|
76
|
+
if (key === 'filters' && state.filters) {
|
|
77
|
+
getFilterParamsFromState(params, state.filters);
|
|
78
|
+
}
|
|
79
|
+
else if (typeof val !== 'string') {
|
|
80
|
+
encodedVal = JSON.stringify(val);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
encodedVal = val;
|
|
84
|
+
}
|
|
85
|
+
if (encodedVal) {
|
|
86
|
+
params.set(defaultQueryStringMap[key], encodedVal);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return `${baseUrl}?${params.toString()}`;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default '1.0.0';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ButtonProps {
|
|
3
|
+
/** Color scheme: dark (for light backgrounds) or light (for dark backgrounds) */
|
|
4
|
+
theme?: 'dark' | 'light';
|
|
5
|
+
/** Button size: sm (small) or lg (large) */
|
|
6
|
+
size?: 'sm' | 'lg';
|
|
7
|
+
/** Click handler */
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
/** Button label text */
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
12
|
+
export default function Button({ theme, size, onClick, label, }: ButtonProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AiMessageOverrides, ChatMessage, Translations } from '../../types';
|
|
3
|
+
export interface AiMessageProps {
|
|
4
|
+
message: ChatMessage;
|
|
5
|
+
componentOverrides?: AiMessageOverrides;
|
|
6
|
+
translations?: Translations;
|
|
7
|
+
}
|
|
8
|
+
declare function AiMessage({ message, componentOverrides, translations }: AiMessageProps): React.JSX.Element;
|
|
9
|
+
declare namespace AiMessage {
|
|
10
|
+
var displayName: string;
|
|
11
|
+
}
|
|
12
|
+
export default AiMessage;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatComponentOverrides, ResultGroupMeta, Translations } from '../../types';
|
|
3
|
+
import { Product, NormalizeOptions } from '../../utils/productNormalizer';
|
|
4
|
+
import { AspectRatio } from '../ResultsBlock/ResultsBlock';
|
|
5
|
+
export interface ChatHandle {
|
|
6
|
+
clearHistory: () => void;
|
|
7
|
+
}
|
|
8
|
+
interface ChatProps {
|
|
9
|
+
/** Called when the close button (✕) is clicked. The consumer controls visibility. */
|
|
10
|
+
onClose?: () => void;
|
|
11
|
+
/** Additional CSS class name for the root container */
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Suggestion chips shown on the welcome screen */
|
|
14
|
+
initialSuggestions?: string[];
|
|
15
|
+
/** Legal/terms text displayed below the welcome screen input */
|
|
16
|
+
termsText?: React.ReactNode;
|
|
17
|
+
/** Called when a product card is clicked */
|
|
18
|
+
onProductClick?: (product: Product) => void;
|
|
19
|
+
/** Called when "View more" link is clicked in a results group */
|
|
20
|
+
onViewMore?: (group: ResultGroupMeta) => void;
|
|
21
|
+
/** Called when "Add to cart" button is clicked. If not provided, the button is hidden. */
|
|
22
|
+
onAddToCart?: (product: Product) => void;
|
|
23
|
+
/** Image aspect ratio for product cards */
|
|
24
|
+
aspectRatio?: AspectRatio;
|
|
25
|
+
/** Currency symbol for product prices */
|
|
26
|
+
currency?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Map a raw search-result item to the `Product` shape rendered by result cards.
|
|
29
|
+
* Override this when your index metadata uses non-default field names.
|
|
30
|
+
*/
|
|
31
|
+
normalizeItem?: (item: any, options?: NormalizeOptions) => Product;
|
|
32
|
+
/** Override any sub-component with custom render props or React nodes */
|
|
33
|
+
componentOverrides?: ChatComponentOverrides;
|
|
34
|
+
/** Translation overrides for internationalizing UI strings */
|
|
35
|
+
translations?: Translations;
|
|
36
|
+
}
|
|
37
|
+
declare const Chat: React.ForwardRefExoticComponent<ChatProps & React.RefAttributes<ChatHandle>>;
|
|
38
|
+
export default Chat;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatHeaderRenderProps, ComponentOverrideProps, Translations } from '../../types';
|
|
3
|
+
interface ChatHeaderProps {
|
|
4
|
+
onClose?: () => void;
|
|
5
|
+
translations?: Translations;
|
|
6
|
+
componentOverrides?: ComponentOverrideProps<ChatHeaderRenderProps>;
|
|
7
|
+
}
|
|
8
|
+
declare function ChatHeader({ onClose, translations, componentOverrides }: ChatHeaderProps): React.JSX.Element;
|
|
9
|
+
declare namespace ChatHeader {
|
|
10
|
+
var displayName: string;
|
|
11
|
+
}
|
|
12
|
+
export default ChatHeader;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ChatInputRenderProps, ComponentOverrideProps, Translations } from '../../types';
|
|
3
|
+
interface ChatInputProps {
|
|
4
|
+
onSubmit: (text: string) => void;
|
|
5
|
+
isDisabled?: boolean;
|
|
6
|
+
translations?: Translations;
|
|
7
|
+
componentOverrides?: ComponentOverrideProps<ChatInputRenderProps>;
|
|
8
|
+
}
|
|
9
|
+
declare function ChatInput({ onSubmit, isDisabled, translations, componentOverrides, }: ChatInputProps): React.JSX.Element;
|
|
10
|
+
declare namespace ChatInput {
|
|
11
|
+
var displayName: string;
|
|
12
|
+
}
|
|
13
|
+
export default ChatInput;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AiMessageOverrides, ChatMessage, ComponentOverrideProps, ResultGroupMeta, ResultsBlockOverrides, Translations, UserMessageRenderProps } from '../../types';
|
|
3
|
+
import { Product, NormalizeOptions } from '../../utils/productNormalizer';
|
|
4
|
+
import { AspectRatio } from '../ResultsBlock/ResultsBlock';
|
|
5
|
+
interface ChatMessageListProps {
|
|
6
|
+
messages: ChatMessage[];
|
|
7
|
+
onProductClick?: (product: Product) => void;
|
|
8
|
+
onViewMore?: (group: ResultGroupMeta) => void;
|
|
9
|
+
onAddToCart?: (product: Product) => void;
|
|
10
|
+
aspectRatio?: AspectRatio;
|
|
11
|
+
currency?: string;
|
|
12
|
+
normalizeItem?: (item: any, options?: NormalizeOptions) => Product;
|
|
13
|
+
addToCartText?: string;
|
|
14
|
+
viewMoreText?: string;
|
|
15
|
+
aiMessageOverrides?: AiMessageOverrides;
|
|
16
|
+
userMessageOverrides?: ComponentOverrideProps<UserMessageRenderProps>;
|
|
17
|
+
resultsBlockOverrides?: ResultsBlockOverrides;
|
|
18
|
+
translations?: Translations;
|
|
19
|
+
}
|
|
20
|
+
declare function ChatMessageList({ messages, onProductClick, onViewMore, onAddToCart, aspectRatio, currency, normalizeItem, addToCartText, viewMoreText, aiMessageOverrides, userMessageOverrides, resultsBlockOverrides, translations, }: ChatMessageListProps): React.JSX.Element;
|
|
21
|
+
declare namespace ChatMessageList {
|
|
22
|
+
var displayName: string;
|
|
23
|
+
}
|
|
24
|
+
export default ChatMessageList;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Translations } from '../../types';
|
|
3
|
+
interface TypingIndicatorProps {
|
|
4
|
+
translations?: Translations;
|
|
5
|
+
}
|
|
6
|
+
declare function TypingIndicator({ translations }: TypingIndicatorProps): React.JSX.Element;
|
|
7
|
+
declare namespace TypingIndicator {
|
|
8
|
+
var displayName: string;
|
|
9
|
+
}
|
|
10
|
+
export default TypingIndicator;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ComponentOverrideProps, Translations, UserMessageRenderProps } from '../../types';
|
|
3
|
+
interface UserMessageProps {
|
|
4
|
+
text: string;
|
|
5
|
+
translations?: Translations;
|
|
6
|
+
componentOverrides?: ComponentOverrideProps<UserMessageRenderProps>;
|
|
7
|
+
}
|
|
8
|
+
declare function UserMessage({ text, translations, componentOverrides }: UserMessageProps): React.JSX.Element;
|
|
9
|
+
declare namespace UserMessage {
|
|
10
|
+
var displayName: string;
|
|
11
|
+
}
|
|
12
|
+
export default UserMessage;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Translations, WelcomeScreenOverrides } from '../../types';
|
|
3
|
+
interface WelcomeScreenProps {
|
|
4
|
+
suggestions?: string[];
|
|
5
|
+
onSend: (text: string) => void;
|
|
6
|
+
onClose?: () => void;
|
|
7
|
+
termsText?: React.ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
translations?: Translations;
|
|
10
|
+
componentOverrides?: WelcomeScreenOverrides;
|
|
11
|
+
}
|
|
12
|
+
declare function WelcomeScreen({ suggestions, onSend, onClose, termsText, disabled, translations, componentOverrides, }: WelcomeScreenProps): React.JSX.Element;
|
|
13
|
+
declare namespace WelcomeScreen {
|
|
14
|
+
var displayName: string;
|
|
15
|
+
}
|
|
16
|
+
export default WelcomeScreen;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ResultGroup, ResultGroupMeta, ResultsBlockOverrides } from '../../types';
|
|
3
|
+
import { Product, NormalizeOptions } from '../../utils/productNormalizer';
|
|
4
|
+
import { AspectRatio } from './constants';
|
|
5
|
+
import './ResultsBlock.css';
|
|
6
|
+
export type { AspectRatio } from './constants';
|
|
7
|
+
interface ResultsBlockProps {
|
|
8
|
+
groups: ResultGroup[];
|
|
9
|
+
aspectRatio?: AspectRatio;
|
|
10
|
+
minCardWidth?: number;
|
|
11
|
+
gap?: number;
|
|
12
|
+
showTitle?: boolean;
|
|
13
|
+
viewMoreText?: string;
|
|
14
|
+
addToCartText?: string;
|
|
15
|
+
saleBadgeText?: string;
|
|
16
|
+
currency?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Map a raw search-result item to the `Product` shape rendered by the card.
|
|
19
|
+
* Override this when your index metadata uses non-default field names
|
|
20
|
+
* (e.g. `imageUrl` instead of `image_url`). Defaults to `normalizeItemToProduct`.
|
|
21
|
+
*/
|
|
22
|
+
normalizeItem?: (item: any, options?: NormalizeOptions) => Product;
|
|
23
|
+
onProductClick?: (product: Product) => void;
|
|
24
|
+
onAddToCart?: (product: Product) => void;
|
|
25
|
+
onViewMore?: (group: ResultGroupMeta) => void;
|
|
26
|
+
componentOverrides?: ResultsBlockOverrides;
|
|
27
|
+
}
|
|
28
|
+
declare function ResultsBlock({ groups, aspectRatio, minCardWidth, gap, showTitle, viewMoreText, addToCartText, saleBadgeText, currency, normalizeItem, onProductClick, onAddToCart, onViewMore, componentOverrides, }: ResultsBlockProps): React.JSX.Element | null;
|
|
29
|
+
declare namespace ResultsBlock {
|
|
30
|
+
var displayName: string;
|
|
31
|
+
}
|
|
32
|
+
export default ResultsBlock;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ProductCardProps, ProductCardOverrides } from '@constructor-io/constructorio-ui-components';
|
|
3
|
+
import { Product } from '../../utils/productNormalizer';
|
|
4
|
+
interface ProductCardRendererOptions {
|
|
5
|
+
currency?: string;
|
|
6
|
+
addToCartText?: string;
|
|
7
|
+
onProductClick?: (product: Product) => void;
|
|
8
|
+
onAddToCart?: (product: Product) => void;
|
|
9
|
+
componentOverrides?: ProductCardOverrides;
|
|
10
|
+
}
|
|
11
|
+
export default function createProductCardRenderer({ currency, addToCartText, onProductClick, onAddToCart, componentOverrides, }: ProductCardRendererOptions): (renderProps: ProductCardProps) => React.JSX.Element;
|
|
12
|
+
export {};
|