@envive-ai/react-widgets 0.1.1 → 0.1.2-arthur-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/SearchResults/index-DCTxvwmv.d.cts +6 -0
  2. package/dist/{index.cjs → SearchResults/index.cjs} +2 -24
  3. package/dist/SearchZeroState/index-DSFtalZR.d.ts +27 -0
  4. package/dist/SearchZeroState/index-bEcxYOSF.d.cts +27 -0
  5. package/dist/SearchZeroState/index.cjs +3049 -0
  6. package/dist/SearchZeroState/index.js +3045 -0
  7. package/dist/SuggestionBar/index-DZU9kbWS.d.cts +39 -0
  8. package/dist/SuggestionBar/index-DyXd4-b7.d.ts +39 -0
  9. package/dist/SuggestionBar/index.cjs +5 -0
  10. package/dist/SuggestionBar/index.js +4 -0
  11. package/dist/SuggestionBar-BOThXJvJ.cjs +453 -0
  12. package/dist/SuggestionBar-DeMmAK4M.js +131 -0
  13. package/dist/SuggestionButtonContainer/index-B_X537jw.d.cts +20 -0
  14. package/dist/SuggestionButtonContainer/index-vwelzDzM.d.ts +20 -0
  15. package/dist/SuggestionButtonContainer/index.cjs +3 -0
  16. package/dist/SuggestionButtonContainer/index.js +3 -0
  17. package/dist/SuggestionButtonContainer-BeWPpeQk.cjs +173 -0
  18. package/dist/SuggestionButtonContainer-CZhOkZaJ.js +167 -0
  19. package/dist/chunk-DWy1uDak.cjs +39 -0
  20. package/package.json +18 -6
  21. package/src/SearchZeroState/SearchIcon.tsx +57 -0
  22. package/src/SearchZeroState/SearchOverlay.tsx +81 -0
  23. package/src/SearchZeroState/SearchZeroState.tsx +264 -0
  24. package/src/SearchZeroState/SearchZeroStateWidget.tsx +33 -0
  25. package/src/SearchZeroState/components/RecommendedProducts.tsx +118 -0
  26. package/src/SearchZeroState/index.ts +8 -0
  27. package/src/SearchZeroState/overlay/overlayHostLocator.ts +19 -0
  28. package/src/SearchZeroState/types.ts +9 -0
  29. package/src/SearchZeroState/zeroStateSearchVariants.ts +24 -0
  30. package/src/SuggestionBar/SuggestionBar.tsx +139 -0
  31. package/src/SuggestionBar/index.ts +2 -0
  32. package/src/SuggestionBar/types.ts +4 -0
  33. package/src/SuggestionButtonContainer/SuggestionButtonContainer.tsx +141 -0
  34. package/src/SuggestionButtonContainer/index.ts +2 -0
  35. package/src/SuggestionButtonContainer/types.ts +16 -0
  36. package/src/stories/SearchZeroState.stories.tsx +44 -0
  37. package/src/stories/SuggestionBar.stories.tsx +46 -0
  38. package/src/util/useHorizontalScrollAnimation.ts +121 -0
  39. package/src/util/useReducedMotionWithOverride.ts +24 -0
  40. package/dist/index-VWNd4lyI.d.cts +0 -6
  41. /package/dist/{index-BPfKr14f.d.ts → SearchResults/index-CYPV3XE0.d.ts} +0 -0
  42. /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,19 @@
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 bareMount = document.getElementById(SEARCH_OVERLAY_PORTAL_ID) as HTMLElement | null;
11
+ if (bareMount) return bareMount;
12
+ const host = document.getElementById(hostId);
13
+ if (!host) throw new Error(`[Overlay] Host #${hostId} not found`);
14
+ const sr = host.shadowRoot;
15
+ if (!sr) throw new Error('[Overlay] Host has no shadowRoot (was it initialized?)');
16
+ const mount = sr.getElementById(SEARCH_OVERLAY_PORTAL_ID) as HTMLElement | null;
17
+ if (!mount) throw new Error('[Overlay] Portal mount missing inside ShadowRoot');
18
+ return mount;
19
+ }
@@ -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 };
@@ -0,0 +1,2 @@
1
+ export * from './SuggestionBar';
2
+ export * from './types';
@@ -0,0 +1,4 @@
1
+ export enum SuggestionBarLocationForMetrics {
2
+ SUGGESTION_BAR_TOP = 'top',
3
+ SUGGESTION_BAR_BOTTOM = 'bottom'
4
+ }