@envive-ai/react-widgets 0.1.1 → 0.1.2-arthur-1
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/SearchResults/index-D8nrHueo.d.ts +6 -0
- package/dist/{index.cjs → SearchResults/index.cjs} +2 -24
- package/dist/SearchZeroState/index-19CiYvee.d.cts +27 -0
- package/dist/SearchZeroState/index-6EaGWYP4.d.ts +27 -0
- package/dist/SearchZeroState/index.cjs +3047 -0
- package/dist/SearchZeroState/index.js +3043 -0
- package/dist/SuggestionBar/index-DZU9kbWS.d.cts +39 -0
- package/dist/SuggestionBar/index-DyXd4-b7.d.ts +39 -0
- package/dist/SuggestionBar/index.cjs +5 -0
- package/dist/SuggestionBar/index.js +4 -0
- package/dist/SuggestionBar-BOThXJvJ.cjs +453 -0
- package/dist/SuggestionBar-DeMmAK4M.js +131 -0
- package/dist/SuggestionButtonContainer/index-B_X537jw.d.cts +20 -0
- package/dist/SuggestionButtonContainer/index-vwelzDzM.d.ts +20 -0
- package/dist/SuggestionButtonContainer/index.cjs +3 -0
- package/dist/SuggestionButtonContainer/index.js +3 -0
- package/dist/SuggestionButtonContainer-BeWPpeQk.cjs +173 -0
- package/dist/SuggestionButtonContainer-CZhOkZaJ.js +167 -0
- package/dist/chunk-DWy1uDak.cjs +39 -0
- package/package.json +18 -6
- package/src/SearchZeroState/SearchIcon.tsx +57 -0
- package/src/SearchZeroState/SearchOverlay.tsx +81 -0
- package/src/SearchZeroState/SearchZeroState.tsx +264 -0
- package/src/SearchZeroState/SearchZeroStateWidget.tsx +33 -0
- package/src/SearchZeroState/components/RecommendedProducts.tsx +118 -0
- package/src/SearchZeroState/index.ts +8 -0
- package/src/SearchZeroState/overlay/overlayHostLocator.ts +17 -0
- package/src/SearchZeroState/types.ts +9 -0
- package/src/SearchZeroState/zeroStateSearchVariants.ts +24 -0
- package/src/SuggestionBar/SuggestionBar.tsx +139 -0
- package/src/SuggestionBar/index.ts +2 -0
- package/src/SuggestionBar/types.ts +4 -0
- package/src/SuggestionButtonContainer/SuggestionButtonContainer.tsx +141 -0
- package/src/SuggestionButtonContainer/index.ts +2 -0
- package/src/SuggestionButtonContainer/types.ts +16 -0
- package/src/stories/SearchZeroState.stories.tsx +44 -0
- package/src/stories/SuggestionBar.stories.tsx +46 -0
- package/src/util/useHorizontalScrollAnimation.ts +121 -0
- package/src/util/useReducedMotionWithOverride.ts +24 -0
- package/dist/index-VWNd4lyI.d.cts +0 -6
- /package/dist/{index-BPfKr14f.d.ts → SearchResults/index-D52sX_I2.d.cts} +0 -0
- /package/dist/{index.js → SearchResults/index.js} +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
3
|
+
import { useAmplitudeTracking } from '@envive-ai/react-hooks/hooks/AmplitudeOperations';
|
|
4
|
+
import { useSearch } from '@envive-ai/react-hooks/hooks/Search';
|
|
5
|
+
import { SuggestionBarLocationForMetrics } from '@envive-ai/react-hooks/types';
|
|
6
|
+
import { Message, MessageType, SpiffyWidgets } from '@envive-ai/react-hooks/application/models';
|
|
7
|
+
import { SearchZeroStateVariant } from '@envive-ai/react-hooks/contexts/types';
|
|
8
|
+
import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
9
|
+
import { SearchInputForm } from '@envive-ai/react-toolkit/SearchInputForm';
|
|
10
|
+
import { SearchInput, searchInputVariantClasses } from '@envive-ai/react-toolkit/SearchInput';
|
|
11
|
+
import { Typography } from '@envive-ai/react-toolkit/Typography';
|
|
12
|
+
import Sparkles from '@envive-ai/react-icons/Sparkles';
|
|
13
|
+
import IconCloseVariant from '@envive-ai/react-icons/IconCloseVariant';
|
|
14
|
+
|
|
15
|
+
import { SuggestionBar } from 'src/SuggestionBar';
|
|
16
|
+
|
|
17
|
+
import { SearchOverlay } from './SearchOverlay';
|
|
18
|
+
import { SearchZeroStateProps } from './types';
|
|
19
|
+
import { searchZeroStateVariantClasses } from './zeroStateSearchVariants';
|
|
20
|
+
import { SearchIcon } from './SearchIcon';
|
|
21
|
+
import { RecommendedProducts } from './components/RecommendedProducts';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const SEARCH_ENTRYPOINT_INPUT_TESTID = 'spiffy-search-entrypoint-input';
|
|
25
|
+
|
|
26
|
+
export const SearchZeroState: React.FC<SearchZeroStateProps> = ({
|
|
27
|
+
widgetConfig,
|
|
28
|
+
initialIsOpen,
|
|
29
|
+
entryPointRef,
|
|
30
|
+
}) => {
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.log('SearchZeroState: widgetConfig', widgetConfig);
|
|
34
|
+
const {
|
|
35
|
+
searchZeroStateVariant,
|
|
36
|
+
searchInputVariant,
|
|
37
|
+
searchIconVariant,
|
|
38
|
+
searchIconSize = 24,
|
|
39
|
+
searchBoxPlaceholder,
|
|
40
|
+
layout,
|
|
41
|
+
compactLabel,
|
|
42
|
+
initialSuggestions = [],
|
|
43
|
+
animationSpeed = 'standard',
|
|
44
|
+
suggestionButtonConfig,
|
|
45
|
+
includeSubtitle,
|
|
46
|
+
usingPortal = false,
|
|
47
|
+
} = widgetConfig;
|
|
48
|
+
const {
|
|
49
|
+
variant: suggestionButtonVariant,
|
|
50
|
+
hoverVariant: suggestionButtonHoverVariant,
|
|
51
|
+
borderRadius: suggestionButtonBorderRadius,
|
|
52
|
+
} = suggestionButtonConfig;
|
|
53
|
+
const [isOpen, setIsOpen] = useState(!!initialIsOpen);
|
|
54
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
55
|
+
|
|
56
|
+
const searchInput = useSearch();
|
|
57
|
+
|
|
58
|
+
const { track } = useAmplitudeTracking();
|
|
59
|
+
|
|
60
|
+
const {
|
|
61
|
+
recommendedProductsHeading,
|
|
62
|
+
searchOverlayHeading,
|
|
63
|
+
productCardConfig,
|
|
64
|
+
merchantShortName,
|
|
65
|
+
recommendedProducts,
|
|
66
|
+
searchText,
|
|
67
|
+
autocompleteResults,
|
|
68
|
+
focusedIndex,
|
|
69
|
+
focusedOptionId,
|
|
70
|
+
shouldShowAutocomplete,
|
|
71
|
+
onSearchInputChange,
|
|
72
|
+
onSearchInputFocus,
|
|
73
|
+
onSearchInputBlur,
|
|
74
|
+
onKeyDown,
|
|
75
|
+
onAutocompleteSelect,
|
|
76
|
+
onSubmitSearch,
|
|
77
|
+
resetSearch,
|
|
78
|
+
} = searchInput;
|
|
79
|
+
|
|
80
|
+
// Ensure variant is valid, fallback to backgroundTertiary if not
|
|
81
|
+
const validVariant: SearchZeroStateVariant =
|
|
82
|
+
searchZeroStateVariant && searchZeroStateVariantClasses[searchZeroStateVariant]
|
|
83
|
+
? searchZeroStateVariant
|
|
84
|
+
: 'backgroundTertiary';
|
|
85
|
+
|
|
86
|
+
const { overlayBackgroundClasses, sparklesIconColor } =
|
|
87
|
+
searchZeroStateVariantClasses[validVariant];
|
|
88
|
+
|
|
89
|
+
const { searchInputIconColor } =
|
|
90
|
+
searchInputVariantClasses[searchInputVariant];
|
|
91
|
+
|
|
92
|
+
const handleOpen = () => setIsOpen(true);
|
|
93
|
+
const handleClose = () => setIsOpen(false);
|
|
94
|
+
|
|
95
|
+
// Track when the search overlay becomes visible
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (isOpen) {
|
|
98
|
+
track(SpiffyMetricsEventName.SearchComponentVisible, {
|
|
99
|
+
eventProps: {
|
|
100
|
+
search_component: SpiffyWidgets.SearchZeroState,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}, [isOpen, track]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (isOpen) {
|
|
108
|
+
|
|
109
|
+
resetSearch(); // Reset search state when opening
|
|
110
|
+
searchInputRef.current?.focus();
|
|
111
|
+
}
|
|
112
|
+
}, [isOpen, resetSearch]);
|
|
113
|
+
|
|
114
|
+
const submitSearchString = useCallback(
|
|
115
|
+
(query: string) => {
|
|
116
|
+
onSearchInputChange(query);
|
|
117
|
+
// Amplitude tracking is handled by useSearchInput.handleSubmitSearch
|
|
118
|
+
if (onSubmitSearch && query.trim()) {
|
|
119
|
+
onSubmitSearch();
|
|
120
|
+
setIsOpen(false);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[onSubmitSearch, onSearchInputChange],
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const searchChange = (value: string) => {
|
|
127
|
+
onSearchInputChange(value);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (!isOpen) {
|
|
131
|
+
if (layout === 'icon') {
|
|
132
|
+
return (
|
|
133
|
+
<SearchIcon
|
|
134
|
+
entryPointRef={entryPointRef}
|
|
135
|
+
size={searchIconSize}
|
|
136
|
+
variant={searchIconVariant}
|
|
137
|
+
label={compactLabel}
|
|
138
|
+
onClick={handleOpen}
|
|
139
|
+
color={searchInputIconColor}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
<SearchInput
|
|
145
|
+
value=""
|
|
146
|
+
onChange={() => {}} // No-op since this just opens the overlay
|
|
147
|
+
placeholder={searchBoxPlaceholder}
|
|
148
|
+
suggestions={[]} // No autocomplete in closed state
|
|
149
|
+
onFocus={handleOpen}
|
|
150
|
+
searchInputVariant={searchInputVariant}
|
|
151
|
+
dataTestId={SEARCH_ENTRYPOINT_INPUT_TESTID}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<AnimatePresence>
|
|
158
|
+
{isOpen && (
|
|
159
|
+
<SearchOverlay
|
|
160
|
+
role="dialog"
|
|
161
|
+
ariaModal
|
|
162
|
+
ariaLabelledby="global-search-title"
|
|
163
|
+
className={overlayBackgroundClasses}
|
|
164
|
+
usingPortal={usingPortal}
|
|
165
|
+
>
|
|
166
|
+
<>
|
|
167
|
+
<div className="spiffy-tw-relative spiffy-tw-mb-4">
|
|
168
|
+
<div className="spiffy-tw-flex spiffy-tw-items-center">
|
|
169
|
+
<Typography id="global-search-title" variant="t3">
|
|
170
|
+
{searchOverlayHeading}
|
|
171
|
+
</Typography>
|
|
172
|
+
<Sparkles
|
|
173
|
+
className="sm:spiffy-tw-w-[36px] sm:spiffy-tw-h-[45px] spiffy-tw-w-[24px] spiffy-tw-h-[36px] spiffy-tw-ml-2"
|
|
174
|
+
color={sparklesIconColor}
|
|
175
|
+
stroke="2px"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
{
|
|
179
|
+
includeSubtitle && (
|
|
180
|
+
<Typography variant="body2">Go ahead, get as specific as you like...</Typography>
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
</div>
|
|
184
|
+
<button
|
|
185
|
+
onClick={handleClose}
|
|
186
|
+
className="spiffy-tw-absolute spiffy-tw-top-4 spiffy-tw-right-4 sm:spiffy-tw-top-8 sm:spiffy-tw-right-8 "
|
|
187
|
+
aria-label="Close search"
|
|
188
|
+
type="button"
|
|
189
|
+
>
|
|
190
|
+
<IconCloseVariant
|
|
191
|
+
className="spiffy-tw-w-[20px] spiffy-tw-h-[20px] sm:spiffy-tw-w-[28px] sm:spiffy-tw-h-[28px]"
|
|
192
|
+
strokeWidth="2px"
|
|
193
|
+
/>
|
|
194
|
+
</button>
|
|
195
|
+
</>
|
|
196
|
+
<SearchInputForm
|
|
197
|
+
searchInputRef={searchInputRef}
|
|
198
|
+
searchInputVariant={searchInputVariant}
|
|
199
|
+
searchText={searchText}
|
|
200
|
+
autocompleteResults={autocompleteResults}
|
|
201
|
+
searchBoxPlaceholder={searchBoxPlaceholder}
|
|
202
|
+
focusedOptionId={focusedOptionId}
|
|
203
|
+
shouldShowAutocomplete={shouldShowAutocomplete}
|
|
204
|
+
focusedIndex={focusedIndex}
|
|
205
|
+
onKeyDown={onKeyDown}
|
|
206
|
+
onAutocompleteSelect={onAutocompleteSelect}
|
|
207
|
+
onSearchInputChange={searchChange}
|
|
208
|
+
onSearchSubmit={submitSearchString}
|
|
209
|
+
onSearchInputFocus={onSearchInputFocus}
|
|
210
|
+
onSearchInputBlur={onSearchInputBlur}
|
|
211
|
+
/>
|
|
212
|
+
<AnimatePresence>
|
|
213
|
+
{initialSuggestions &&
|
|
214
|
+
initialSuggestions.length > 0 && ( // Conditionally render suggestions
|
|
215
|
+
<motion.div
|
|
216
|
+
className="spiffy-tw-justify-center spiffy-tw-w-full spiffy-tw-overflow-hidden"
|
|
217
|
+
initial={{ opacity: 0 }}
|
|
218
|
+
animate={{ opacity: 1 }}
|
|
219
|
+
exit={{ opacity: 0 }}
|
|
220
|
+
transition={{ duration: 0.2 }}
|
|
221
|
+
>
|
|
222
|
+
<div className="spiffy-tw-mt-6">
|
|
223
|
+
<SuggestionBar
|
|
224
|
+
id="global-search-suggestions"
|
|
225
|
+
locationForMetrics={SuggestionBarLocationForMetrics.SUGGESTION_BAR_TOP}
|
|
226
|
+
buttonTexts={initialSuggestions}
|
|
227
|
+
buttonBorderRadius={suggestionButtonBorderRadius}
|
|
228
|
+
buttonVariation={suggestionButtonVariant ?? 'primary'}
|
|
229
|
+
hoverButtonVariation={suggestionButtonHoverVariant ?? 'primary'}
|
|
230
|
+
animationSpeed={animationSpeed}
|
|
231
|
+
handleReply={(message: Message) => {
|
|
232
|
+
if (message.type === MessageType.QueryTyped && message.metadata?.content) {
|
|
233
|
+
track(SpiffyMetricsEventName.SearchZeroStateSuggestionClicked, {
|
|
234
|
+
eventProps: {
|
|
235
|
+
queryText: message.metadata.content,
|
|
236
|
+
timestamp: new Date().toISOString(),
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
submitSearchString(message.metadata.content);
|
|
240
|
+
}
|
|
241
|
+
}}
|
|
242
|
+
twoRowsOnMobile
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</motion.div>
|
|
246
|
+
)}
|
|
247
|
+
{recommendedProducts.length > 0 && (
|
|
248
|
+
<div className="spiffy-tw-mt-[40px]">
|
|
249
|
+
<RecommendedProducts
|
|
250
|
+
retrievedProducts={recommendedProducts}
|
|
251
|
+
merchantShortName={merchantShortName}
|
|
252
|
+
productCardConfig={productCardConfig}
|
|
253
|
+
// TODO: Figure out where this comes from
|
|
254
|
+
productGridVariant="standard"
|
|
255
|
+
heading={recommendedProductsHeading}
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</AnimatePresence>
|
|
260
|
+
</SearchOverlay>
|
|
261
|
+
)}
|
|
262
|
+
</AnimatePresence>
|
|
263
|
+
);
|
|
264
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SearchEntryPointWidgetConfig } from "@envive-ai/react-hooks/contexts/types";
|
|
2
|
+
import { useNewOrgConfig } from "@envive-ai/react-hooks/hooks/NewOrgConfig";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { SearchZeroState } from "./SearchZeroState";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type SearchZeroStateWidgetProps = {
|
|
8
|
+
initialIsOpen: boolean;
|
|
9
|
+
widgetConfigId?: string;
|
|
10
|
+
entryPointRef: React.Ref<HTMLButtonElement>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SearchZeroStateWidget = ({ initialIsOpen, widgetConfigId = 'searchEntryPointIcon', entryPointRef }: SearchZeroStateWidgetProps) => {
|
|
14
|
+
const newConfig = useNewOrgConfig();
|
|
15
|
+
|
|
16
|
+
const widgetConfig = useMemo(() => {
|
|
17
|
+
if (newConfig && newConfig.frontendConfig?.widgetConfigs) {
|
|
18
|
+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
19
|
+
const baseWidgetConfig = (newConfig.frontendConfig?.widgetConfigs as unknown as any[]).find(
|
|
20
|
+
(widget: { key: string }) => widget.key === widgetConfigId,
|
|
21
|
+
);
|
|
22
|
+
return {
|
|
23
|
+
...baseWidgetConfig.config,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}, [newConfig, widgetConfigId]);
|
|
28
|
+
if (!widgetConfig) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return <SearchZeroState widgetConfig={widgetConfig as SearchEntryPointWidgetConfig} initialIsOpen={initialIsOpen} entryPointRef={entryPointRef} />;
|
|
33
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
3
|
+
|
|
4
|
+
import { ProductGrid } from '@envive-ai/react-toolkit/ProductGrid';
|
|
5
|
+
import { useAmplitudeTracking } from '@envive-ai/react-hooks/hooks/AmplitudeOperations';
|
|
6
|
+
import { SearchResponseProduct } from '@spiffy-ai/commerce-api-client';
|
|
7
|
+
import { Typography } from '@envive-ai/react-toolkit/Typography';
|
|
8
|
+
import { ProductCardConfig, ProductGridVariant } from '@envive-ai/react-hooks/contexts/types';
|
|
9
|
+
import { ChatElementDisplayLocation, SearchResponseProductAttributes } from '@envive-ai/react-hooks/application/models';
|
|
10
|
+
import { SpiffyMetricsEventName } from '@envive-ai/react-hooks/contexts/amplitudeContext';
|
|
11
|
+
|
|
12
|
+
interface RecommendedProductsProps {
|
|
13
|
+
retrievedProducts: SearchResponseProductAttributes['attributes'][];
|
|
14
|
+
merchantShortName: string;
|
|
15
|
+
productCardConfig?: ProductCardConfig;
|
|
16
|
+
productGridVariant?: ProductGridVariant;
|
|
17
|
+
heading?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const RecommendedProducts: React.FC<RecommendedProductsProps> = ({
|
|
21
|
+
retrievedProducts,
|
|
22
|
+
merchantShortName,
|
|
23
|
+
productCardConfig = { variant: 'minimal', hoverVariant: 'none', layoutVariant: 'square' },
|
|
24
|
+
productGridVariant = 'square',
|
|
25
|
+
heading,
|
|
26
|
+
}: RecommendedProductsProps) => {
|
|
27
|
+
const { track } = useAmplitudeTracking();
|
|
28
|
+
const containerClasses = classNames(
|
|
29
|
+
'spiffy-tw-justify-center',
|
|
30
|
+
'spiffy-tw-overflow-hidden',
|
|
31
|
+
'spiffy-tw-bg-white',
|
|
32
|
+
// Break out of parent container to fill full viewport width using CSS calc
|
|
33
|
+
'spiffy-tw-relative',
|
|
34
|
+
'spiffy-tw-px-[24px]',
|
|
35
|
+
'spiffy-tw-py-[16px]',
|
|
36
|
+
'sm:spiffy-tw-px-[41px]',
|
|
37
|
+
'sm:spiffy-tw-py-[40px]',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const titleContainerClasses = classNames(
|
|
41
|
+
'spiffy-tw-w-full',
|
|
42
|
+
'spiffy-tw-border-b',
|
|
43
|
+
'spiffy-tw-border-solid',
|
|
44
|
+
'spiffy-tw-border-b-[--spiffy-colors-text-accent]',
|
|
45
|
+
'spiffy-tw-pb-[8px]',
|
|
46
|
+
'spiffy-tw-mb-[16px]',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const productGridClasses = classNames(
|
|
50
|
+
'spiffy-tw-grid',
|
|
51
|
+
'spiffy-tw-justify-items-stretch',
|
|
52
|
+
'spiffy-tw-grid-cols-2',
|
|
53
|
+
'md:spiffy-tw-grid-cols-3',
|
|
54
|
+
'lg:spiffy-tw-grid-cols-4',
|
|
55
|
+
'spiffy-tw-gap-x-[4px]',
|
|
56
|
+
'spiffy-tw-gap-y-[24px]',
|
|
57
|
+
'spiffy-tw-h-full',
|
|
58
|
+
'spiffy-tw-w-full',
|
|
59
|
+
'spiffy-tw-items-stretch',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
if (retrievedProducts == null || retrievedProducts.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleProductClick = (product: SearchResponseProduct, index: number) => {
|
|
67
|
+
|
|
68
|
+
track(SpiffyMetricsEventName.ProductCardClicked, {
|
|
69
|
+
eventProps: {
|
|
70
|
+
url: product.url,
|
|
71
|
+
search_response_id: undefined,
|
|
72
|
+
product_response_id: product.response_id,
|
|
73
|
+
trigger_location: ChatElementDisplayLocation.SEARCH_ZERO_STATE_SUGGESTED_PRODUCTS,
|
|
74
|
+
click_position: index != null ? index + 1 : null,
|
|
75
|
+
title: product.title,
|
|
76
|
+
original_price: product.original_price,
|
|
77
|
+
sale_price: product.sale_price,
|
|
78
|
+
average_rating: product.average_rating,
|
|
79
|
+
number_reviews: product.number_reviews,
|
|
80
|
+
},
|
|
81
|
+
alsoSendToGoogleAnalytics: true,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<motion.div
|
|
87
|
+
className={containerClasses}
|
|
88
|
+
initial={{ opacity: 0 }}
|
|
89
|
+
animate={{ opacity: 1 }}
|
|
90
|
+
exit={{ opacity: 0 }}
|
|
91
|
+
transition={{ duration: 0.2 }}
|
|
92
|
+
style={{
|
|
93
|
+
// Break out of parent container padding using CSS calc
|
|
94
|
+
left: '50%',
|
|
95
|
+
right: '50%',
|
|
96
|
+
marginLeft: '-50vw',
|
|
97
|
+
marginRight: '-50vw',
|
|
98
|
+
width: '100vw',
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{heading && (
|
|
102
|
+
<div className={titleContainerClasses}>
|
|
103
|
+
<Typography variant="h1" className="spiffy-tw-text-[--spiffy-colors-text-accent]">
|
|
104
|
+
{heading}
|
|
105
|
+
</Typography>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
<ProductGrid
|
|
109
|
+
productList={retrievedProducts}
|
|
110
|
+
productGridVariant={productGridVariant}
|
|
111
|
+
productGridClasses={productGridClasses}
|
|
112
|
+
productCardConfig={productCardConfig}
|
|
113
|
+
merchantShortName={merchantShortName}
|
|
114
|
+
onProductClick={handleProductClick}
|
|
115
|
+
/>
|
|
116
|
+
</motion.div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Barrel file for GlobalSearch components
|
|
2
|
+
// This file will export all public components and types from this module.
|
|
3
|
+
|
|
4
|
+
export * from './types';
|
|
5
|
+
export * from './SearchZeroState';
|
|
6
|
+
export * from './SearchZeroStateWidget';
|
|
7
|
+
// Note: Partials like SuggestionButton are not exported here by design,
|
|
8
|
+
// as they are intended for internal use within this module.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const SEARCH_OVERLAY_ENTRYPOINT_ID = 'spiffy-ai-search-overlay-entrypoint';
|
|
2
|
+
export const SEARCH_OVERLAY_PORTAL_ID = 'spiffy-ai-search-overlay-portal'; // The inner div the overlay portal will render into
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the portal target inside the overlay host's ShadowRoot.
|
|
6
|
+
*/
|
|
7
|
+
export function getOverlayPortalTarget(
|
|
8
|
+
hostId: string = SEARCH_OVERLAY_ENTRYPOINT_ID,
|
|
9
|
+
): HTMLElement {
|
|
10
|
+
const host = document.getElementById(hostId);
|
|
11
|
+
if (!host) throw new Error(`[Overlay] Host #${hostId} not found`);
|
|
12
|
+
const sr = host.shadowRoot;
|
|
13
|
+
if (!sr) throw new Error('[Overlay] Host has no shadowRoot (was it initialized?)');
|
|
14
|
+
const mount = sr.getElementById(SEARCH_OVERLAY_PORTAL_ID) as HTMLElement | null;
|
|
15
|
+
if (!mount) throw new Error('[Overlay] Portal mount missing inside ShadowRoot');
|
|
16
|
+
return mount;
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SearchEntryPointWidgetConfig, SearchInputVariant } from "@envive-ai/react-hooks/contexts/types";
|
|
2
|
+
|
|
3
|
+
export type { SearchInputVariant, SearchEntryPointWidgetConfig };
|
|
4
|
+
|
|
5
|
+
export interface SearchZeroStateProps {
|
|
6
|
+
widgetConfig: SearchEntryPointWidgetConfig;
|
|
7
|
+
initialIsOpen?: boolean;
|
|
8
|
+
entryPointRef?: React.Ref<HTMLButtonElement>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ColorNames, colorVar, SearchZeroStateVariant } from "@envive-ai/react-hooks/contexts/types";
|
|
2
|
+
|
|
3
|
+
interface SearchZeroStateVariantClasses {
|
|
4
|
+
overlayBackgroundClasses: string;
|
|
5
|
+
sparklesIconColor: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const searchZeroStateVariantClasses: Record<SearchZeroStateVariant, SearchZeroStateVariantClasses> = {
|
|
9
|
+
backgroundTertiary: { // standard light mode
|
|
10
|
+
overlayBackgroundClasses:
|
|
11
|
+
'spiffy-tw-bg-[--spiffy-colors-background-tertiary] spiffy-tw-text-[--spiffy-colors-text-primary] spiffy-tw-bg-opacity-90 spiffy-tw-backdrop-blur-20',
|
|
12
|
+
sparklesIconColor: colorVar(ColorNames.AccentPrimary),
|
|
13
|
+
},
|
|
14
|
+
backgroundDark: { // standard dark mode
|
|
15
|
+
overlayBackgroundClasses:
|
|
16
|
+
'spiffy-tw-bg-[--spiffy-colors-background-dark] spiffy-tw-text-[--spiffy-colors-text-light] spiffy-tw-bg-opacity-90 spiffy-tw-backdrop-blur-20',
|
|
17
|
+
sparklesIconColor: colorVar(ColorNames.AccentPrimary),
|
|
18
|
+
},
|
|
19
|
+
backgroundPrimary: {
|
|
20
|
+
overlayBackgroundClasses:
|
|
21
|
+
'spiffy-tw-bg-[--spiffy-colors-background-primary] spiffy-tw-text-[--spiffy-colors-text-primary] spiffy-tw-bg-opacity-90 spiffy-tw-backdrop-blur-20',
|
|
22
|
+
sparklesIconColor: colorVar(ColorNames.AccentPrimary)
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { useScrollContainer } from 'react-indiana-drag-scroll';
|
|
3
|
+
import { v4 as uuid } from 'uuid';
|
|
4
|
+
import { useSetAtom } from 'jotai';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import { SpiffyWidgets, MessageRole, MessageType } from '@envive-ai/react-hooks/application/models';
|
|
8
|
+
import { logPerfMetricAtom, PerfMetricsEvents } from '@envive-ai/react-hooks/atoms/chat';
|
|
9
|
+
import { SUGGESTION_BAR_TESTID } from '@envive-ai/react-hooks/config';
|
|
10
|
+
import { useIsSmallScreen } from '@envive-ai/react-hooks/hooks/IsSmallScreen';
|
|
11
|
+
import { useTrackComponentVisibleEvent } from '@envive-ai/react-hooks/hooks/TrackComponentVisibleEvent';
|
|
12
|
+
import { TestProps } from '@envive-ai/react-hooks/types';
|
|
13
|
+
import { Message } from 'postcss';
|
|
14
|
+
|
|
15
|
+
import { SuggestionButtonVariant } from '@envive-ai/react-hooks/contexts/types';
|
|
16
|
+
import { SuggestionBarLocationForMetrics } from './types';
|
|
17
|
+
import { SuggestionButtonContainer } from '../SuggestionButtonContainer';
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// SuggestionBarV2 Props
|
|
21
|
+
interface SuggestionBarProps extends TestProps {
|
|
22
|
+
id: string;
|
|
23
|
+
locationForMetrics: SuggestionBarLocationForMetrics;
|
|
24
|
+
buttonTexts: string[];
|
|
25
|
+
buttonVariation: SuggestionButtonVariant;
|
|
26
|
+
hoverButtonVariation: SuggestionButtonVariant;
|
|
27
|
+
handleReply: (message: Message) => void;
|
|
28
|
+
boldFirstButton?: boolean | undefined;
|
|
29
|
+
twoRowsOnMobile?: boolean | undefined;
|
|
30
|
+
animationSpeed?: 'standard' | 'slow' | 'none';
|
|
31
|
+
buttonBorderRadius?: 'sm' | 'md' | 'lg';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// SuggestionBar functional component
|
|
35
|
+
function SuggestionBar({
|
|
36
|
+
id,
|
|
37
|
+
locationForMetrics,
|
|
38
|
+
buttonTexts,
|
|
39
|
+
buttonVariation,
|
|
40
|
+
hoverButtonVariation,
|
|
41
|
+
boldFirstButton = false,
|
|
42
|
+
twoRowsOnMobile = false,
|
|
43
|
+
animationSpeed = 'none',
|
|
44
|
+
buttonBorderRadius = 'lg',
|
|
45
|
+
handleReply,
|
|
46
|
+
dataTestId,
|
|
47
|
+
}: Readonly<SuggestionBarProps>) {
|
|
48
|
+
// Refs
|
|
49
|
+
const componentVisibleTriggerRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
51
|
+
|
|
52
|
+
// Hooks
|
|
53
|
+
const { ref } = useScrollContainer();
|
|
54
|
+
const isSmallScreen = useIsSmallScreen();
|
|
55
|
+
const logPerfMetric = useSetAtom(logPerfMetricAtom);
|
|
56
|
+
|
|
57
|
+
const isAnimated = animationSpeed !== 'none';
|
|
58
|
+
|
|
59
|
+
// Track component visibility
|
|
60
|
+
useTrackComponentVisibleEvent(
|
|
61
|
+
SpiffyWidgets.SuggestionBar,
|
|
62
|
+
componentVisibleTriggerRef,
|
|
63
|
+
{ animated: isAnimated },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Handle button click
|
|
67
|
+
const handleClickSuggestion = useCallback(
|
|
68
|
+
(buttonText: string) => {
|
|
69
|
+
const newMessage: Message = {
|
|
70
|
+
id: uuid(),
|
|
71
|
+
role: MessageRole.User,
|
|
72
|
+
type: MessageType.QueryTyped,
|
|
73
|
+
createdAt: new Date().toISOString(),
|
|
74
|
+
metadata: { content: buttonText },
|
|
75
|
+
};
|
|
76
|
+
handleReply(newMessage);
|
|
77
|
+
},
|
|
78
|
+
[handleReply],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Combine refs
|
|
82
|
+
const setRefs = useCallback(
|
|
83
|
+
(el: HTMLDivElement | null) => {
|
|
84
|
+
if (typeof ref === 'function') ref(el);
|
|
85
|
+
containerRef.current = el;
|
|
86
|
+
},
|
|
87
|
+
[ref],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const handleContainerRef = useCallback(
|
|
91
|
+
(el: HTMLDivElement | null) => {
|
|
92
|
+
if (el) {
|
|
93
|
+
logPerfMetric(
|
|
94
|
+
locationForMetrics === SuggestionBarLocationForMetrics.SUGGESTION_BAR_TOP
|
|
95
|
+
? PerfMetricsEvents.TopSuggestionsBarRendered
|
|
96
|
+
: PerfMetricsEvents.BottomSuggestionsBarRendered,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
setRefs(el);
|
|
100
|
+
},
|
|
101
|
+
[locationForMetrics, logPerfMetric, setRefs],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div id={id} className="spiffy-tw-justify-center spiffy-tw-w-full spiffy-tw-overflow-hidden">
|
|
106
|
+
<div
|
|
107
|
+
className={`
|
|
108
|
+
spiffy-tw-relative
|
|
109
|
+
spiffy-tw-w-full
|
|
110
|
+
spiffy-tw-overflow-x-scroll
|
|
111
|
+
spiffy-tw-overflow-y-hidden
|
|
112
|
+
spiffy-tw-no-scrollbar
|
|
113
|
+
${twoRowsOnMobile && isSmallScreen ? 'spiffy-tw-h-20' : 'spiffy-tw-h-9'}
|
|
114
|
+
`}
|
|
115
|
+
ref={handleContainerRef}
|
|
116
|
+
>
|
|
117
|
+
<div
|
|
118
|
+
className="spiffy-tw-relative spiffy-tw-inline-block spiffy-tw-whitespace-nowrap"
|
|
119
|
+
ref={componentVisibleTriggerRef}
|
|
120
|
+
data-testid={dataTestId || SUGGESTION_BAR_TESTID}
|
|
121
|
+
>
|
|
122
|
+
<SuggestionButtonContainer
|
|
123
|
+
buttonVariation={buttonVariation}
|
|
124
|
+
hoverButtonVariation={hoverButtonVariation}
|
|
125
|
+
buttonTexts={buttonTexts}
|
|
126
|
+
onButtonClick={handleClickSuggestion}
|
|
127
|
+
scrollContainerRef={containerRef}
|
|
128
|
+
boldFirstButton={boldFirstButton}
|
|
129
|
+
twoRowsOnMobile={twoRowsOnMobile}
|
|
130
|
+
animationSpeed={animationSpeed}
|
|
131
|
+
buttonBorderRadius={buttonBorderRadius}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { SuggestionBar };
|