@nyris/nyris-webapp 0.3.89 → 0.3.91

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 (70) hide show
  1. package/build/_headers +2 -0
  2. package/build/asset-manifest.json +6 -6
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +17 -0
  5. package/build/static/css/main.734b52e1.css +4 -0
  6. package/build/static/css/main.734b52e1.css.map +1 -0
  7. package/build/static/js/main.f2255597.js +3 -0
  8. package/build/static/js/{main.ca8b95bc.js.map → main.f2255597.js.map} +1 -1
  9. package/package.json +3 -3
  10. package/public/_headers +2 -0
  11. package/public/index.html +1 -1
  12. package/public/js/settings.example.js +17 -0
  13. package/src/App.tsx +5 -3
  14. package/src/components/Cart.tsx +321 -0
  15. package/src/components/CustomCameraDrawer.tsx +4 -22
  16. package/src/components/DragDropFile.tsx +57 -38
  17. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
  18. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
  19. package/src/components/GroundingSpecs.tsx +47 -0
  20. package/src/components/Header.tsx +94 -93
  21. package/src/components/HitsPerPage.tsx +4 -2
  22. package/src/components/ImagePreview.tsx +64 -31
  23. package/src/components/ImageUpload.tsx +247 -0
  24. package/src/components/ItemSpecification.tsx +164 -0
  25. package/src/components/MatchNotificationBanner.tsx +165 -0
  26. package/src/components/PostFilter/PostFilter.tsx +22 -1
  27. package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
  28. package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
  29. package/src/components/PoweredBy.tsx +16 -0
  30. package/src/components/PreFilter/PreFilter.tsx +77 -54
  31. package/src/components/Product/Product.tsx +186 -28
  32. package/src/components/Product/ProductAttribute.tsx +2 -2
  33. package/src/components/Product/ProductDetailView.tsx +123 -18
  34. package/src/components/Product/ProductDetailViewModal.tsx +3 -0
  35. package/src/components/Product/ProductList.tsx +78 -8
  36. package/src/components/SidePanel.tsx +212 -120
  37. package/src/components/TextSearch.tsx +82 -203
  38. package/src/components/Toaster.tsx +34 -15
  39. package/src/helpers/ToastHelper.ts +6 -2
  40. package/src/hooks/useCadSearch.ts +5 -0
  41. package/src/hooks/useImageSearch.ts +102 -13
  42. package/src/index.css +59 -0
  43. package/src/layouts/AppLayout.tsx +16 -14
  44. package/src/pages/Home.tsx +61 -13
  45. package/src/pages/Result.tsx +287 -295
  46. package/src/services/vizo.ts +161 -0
  47. package/src/stores/request/Misc/misc.initialstate.ts +1 -0
  48. package/src/stores/request/Misc/misc.slice.ts +1 -0
  49. package/src/stores/request/filter/filter.initialState.ts +3 -0
  50. package/src/stores/request/filter/filter.slice.ts +23 -0
  51. package/src/stores/result/prodcuts/products.initialState.ts +4 -0
  52. package/src/stores/result/prodcuts/products.slice.ts +15 -0
  53. package/src/stores/types.ts +27 -1
  54. package/src/stores/ui/loading/loading.initialState.ts +1 -0
  55. package/src/stores/ui/loading/loading.slice.ts +4 -0
  56. package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
  57. package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
  58. package/src/stores/ui/uiStore.ts +4 -1
  59. package/src/styles/Cart.scss +210 -0
  60. package/src/styles/common.scss +10 -0
  61. package/src/translations.ts +4 -4
  62. package/src/types.ts +11 -3
  63. package/src/utils/prepareImageList.ts +6 -5
  64. package/src/utils/textSearchFilter.ts +203 -0
  65. package/tailwind.config.js +1 -0
  66. package/build/static/css/main.ba1c7479.css +0 -4
  67. package/build/static/css/main.ba1c7479.css.map +0 -1
  68. package/build/static/js/main.ca8b95bc.js +0 -3
  69. package/src/components/Footer.tsx +0 -21
  70. /package/build/static/js/{main.ca8b95bc.js.LICENSE.txt → main.f2255597.js.LICENSE.txt} +0 -0
@@ -0,0 +1,247 @@
1
+ import { Icon } from '@nyris/nyris-react-components';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import Tooltip from './Tooltip/TooltipComponent';
5
+ import { useAuth0 } from '@auth0/auth0-react';
6
+ import { useMemo, useState } from 'react';
7
+ import { useLocation, useNavigate } from 'react-router';
8
+ import useRequestStore from 'stores/request/requestStore';
9
+ import { useImageSearch } from 'hooks/useImageSearch';
10
+ import { clone } from 'lodash';
11
+ import { useCadSearch } from 'hooks/useCadSearch';
12
+ import { isCadFile } from '@nyris/nyris-api';
13
+ import UploadDisclaimer from './UploadDisclaimer';
14
+
15
+ function ImageUpload({ onCameraClick }: { onCameraClick?: () => void }) {
16
+ const settings = window.settings;
17
+ const user = useAuth0().user;
18
+
19
+ const { t } = useTranslation();
20
+
21
+ const location = useLocation();
22
+ const navigate = useNavigate();
23
+
24
+ const preFilterList = useRequestStore(state => state.preFilterList);
25
+ const requestImages = useRequestStore(state => state.requestImages);
26
+ const setQuery = useRequestStore(state => state.setQuery);
27
+ const setValueInput = useRequestStore(state => state.setValueInput);
28
+ const setMetaFilter = useRequestStore(state => state.setMetaFilter);
29
+ const specifications = useRequestStore(state => state.specifications);
30
+
31
+ const setRequestImages = useRequestStore(state => state.setRequestImages);
32
+ const setSpecifications = useRequestStore(state => state.setSpecifications);
33
+ const setShowLoading = useRequestStore(state => state.setShowLoading);
34
+ const setNameplateNotificationText = useRequestStore(
35
+ state => state.setNameplateNotificationText,
36
+ );
37
+ const setShowNotMatchedError = useRequestStore(
38
+ state => state.setShowNotMatchedError,
39
+ );
40
+ const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
41
+ const setPreFilter = useRequestStore(state => state.setPreFilter);
42
+ const setNameplateImage = useRequestStore(state => state.setNameplateImage);
43
+
44
+ const [showDisclaimer, setShowDisclaimer] = useState(false);
45
+
46
+ const showPreFilter = useMemo(() => {
47
+ if (settings.shouldUseUserMetadata && user) {
48
+ if (user['/user_metadata'].value) {
49
+ setMetaFilter(user['/user_metadata'].value);
50
+ }
51
+ }
52
+
53
+ if (settings.shouldUseUserMetadata && user) {
54
+ if (settings.preFilterOption && !user['/user_metadata'].value) {
55
+ return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ return settings.preFilterOption;
61
+ }, [
62
+ setMetaFilter,
63
+ settings.preFilterOption,
64
+ settings.shouldUseUserMetadata,
65
+ user,
66
+ ]);
67
+
68
+ const showDisclaimerDisabled = useMemo(() => {
69
+ const disclaimer = localStorage.getItem('upload-disclaimer-suite');
70
+ if (requestImages.length === 0) return true;
71
+ if (!disclaimer) return false;
72
+ return disclaimer === 'dont-show';
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, [showDisclaimer, requestImages]);
75
+
76
+ const { singleImageSearch } = useImageSearch();
77
+ const { cadSearch } = useCadSearch();
78
+
79
+ const handleUpload = (files: File[]) => {
80
+ setValueInput('');
81
+ setQuery('');
82
+
83
+ if (isCadFile(files[0])) {
84
+ cadSearch({ file: files[0], settings, newSearch: true }).then(res => {});
85
+
86
+ return;
87
+ }
88
+
89
+ setShowLoading(true);
90
+
91
+ singleImageSearch({
92
+ image: files[0],
93
+ settings: window.settings,
94
+ showFeedback: true,
95
+ newSearch: true,
96
+ clearPostFilter: true,
97
+ }).then(singleImageResp => {
98
+ const specificationPrefilter =
99
+ singleImageResp.image_analysis?.specification?.prefilter_value || null;
100
+ const hasPrefilter = preFilterList.filter((filter: any) =>
101
+ filter.values.includes(specificationPrefilter),
102
+ );
103
+ if (specificationPrefilter) {
104
+ setRequestImages([]);
105
+ setShowNotMatchedError(false);
106
+ if (hasPrefilter.length) {
107
+ setSpecifications(
108
+ clone(singleImageResp.image_analysis.specification),
109
+ );
110
+ setNameplateImage(files[0]);
111
+ setPreFilter({
112
+ [singleImageResp.image_analysis?.specification?.prefilter_value]:
113
+ true,
114
+ });
115
+ setAlgoliaFilter(
116
+ `${settings.alogoliaFilterField}:'${singleImageResp.image_analysis?.specification?.prefilter_value}'`,
117
+ );
118
+
119
+ setShowLoading(false);
120
+ navigate('/result');
121
+
122
+ setTimeout(() => {
123
+ setNameplateNotificationText(
124
+ t('We have successfully defined the search criteria', {
125
+ prefilter_value: specificationPrefilter,
126
+ preFilterTitle:
127
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
128
+ }),
129
+ );
130
+ }, 1000);
131
+ setTimeout(() => {
132
+ setNameplateNotificationText('');
133
+ }, 10000);
134
+ }
135
+ if (!hasPrefilter.length && showPreFilter) {
136
+ setSpecifications(
137
+ clone({
138
+ ...singleImageResp.image_analysis.specification,
139
+ specificationPrefilter,
140
+ }),
141
+ );
142
+ navigate('/result');
143
+ setPreFilter({});
144
+ setAlgoliaFilter('');
145
+ setShowLoading(false);
146
+ setShowNotMatchedError(true);
147
+ setTimeout(() => {
148
+ setNameplateNotificationText(
149
+ t('Extracted details from the nameplate could not be matched', {
150
+ preFilterTitle:
151
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
152
+ }),
153
+ );
154
+ }, 1000);
155
+ setTimeout(() => {
156
+ setNameplateNotificationText('');
157
+ }, 15000);
158
+ }
159
+ } else {
160
+ if (!specifications?.is_nameplate) {
161
+ setSpecifications({ ...specifications, is_nameplate: false });
162
+ }
163
+ setShowLoading(false);
164
+ navigate('/result');
165
+ }
166
+ });
167
+ };
168
+
169
+ return (
170
+ <>
171
+ <div
172
+ className={twMerge([
173
+ location.pathname !== '/result' && 'hidden desktop:flex',
174
+ ])}
175
+ >
176
+ <input
177
+ accept={'.stp,.step,.stl,.obj,.glb,.gltf,.heic,.heif,.pdf,image/*'}
178
+ id="icon-button-file"
179
+ type="file"
180
+ style={{ display: 'none' }}
181
+ onClick={e => {
182
+ e.stopPropagation();
183
+ }}
184
+ onChange={e => {
185
+ if (e?.target?.files) {
186
+ handleUpload(Array.from(e.target.files));
187
+ }
188
+ e.target.value = '';
189
+ }}
190
+ />
191
+ <Tooltip content={t('Search with an image')}>
192
+ <label
193
+ className={twMerge(
194
+ 'mr-2 desktop:mr-1',
195
+ 'w-10 h-10 flex justify-center items-center cursor-pointer rounded-full bg-gray-100 desktop:bg-transparent hover:bg-gray-100',
196
+ location.pathname === '/result' && 'desktop:w-8 desktop:h-8',
197
+ )}
198
+ htmlFor={
199
+ showDisclaimerDisabled && !onCameraClick ? 'icon-button-file' : ''
200
+ }
201
+ onClick={e => {
202
+ if (!showDisclaimerDisabled) {
203
+ // disclaimer
204
+ setShowDisclaimer(true);
205
+ } else if (onCameraClick) {
206
+ onCameraClick();
207
+ }
208
+ }}
209
+ >
210
+ <Icon name="camera_simple" width={16} height={16} fill="#2B2C46" />
211
+ </label>
212
+ </Tooltip>
213
+ </div>
214
+
215
+ {showDisclaimer && (
216
+ <UploadDisclaimer
217
+ onClose={() => {
218
+ setShowDisclaimer(false);
219
+ }}
220
+ onContinue={({
221
+ file,
222
+ dontShowAgain,
223
+ }: {
224
+ file: any;
225
+ dontShowAgain: any;
226
+ }) => {
227
+ if (dontShowAgain) {
228
+ localStorage.setItem('upload-disclaimer-suite', 'dont-show');
229
+ }
230
+
231
+ if (onCameraClick) {
232
+ onCameraClick();
233
+ } else {
234
+ if (file) {
235
+ handleUpload(Array(file));
236
+ }
237
+ }
238
+
239
+ setShowDisclaimer(false);
240
+ }}
241
+ />
242
+ )}
243
+ </>
244
+ );
245
+ }
246
+
247
+ export default ImageUpload;
@@ -0,0 +1,164 @@
1
+ import { useRefinementList } from 'react-instantsearch';
2
+ import Tooltip from './Tooltip/TooltipComponent';
3
+ import { twMerge } from 'tailwind-merge';
4
+ import useRequestStore from '../stores/request/requestStore';
5
+ import useResultStore from '../stores/result/resultStore';
6
+ import { Icon } from '@nyris/nyris-react-components';
7
+ import { useMediaQuery } from 'react-responsive';
8
+
9
+ const ItemSpecification = ({
10
+ attr,
11
+ value,
12
+ specificationFilter,
13
+ imageAnalysis,
14
+ }: any) => {
15
+ const refinement = window.settings.refinements.filter((ref: any) => {
16
+ return ref.header.toLocaleLowerCase() === attr.toLocaleLowerCase();
17
+ });
18
+ const attribute = refinement?.length ? refinement[0].attribute : 'none';
19
+ const isMobile = useMediaQuery({ query: '(max-width: 776px)' });
20
+ const { refine } = useRefinementList({
21
+ attribute,
22
+ operator: 'or',
23
+ showMore: false,
24
+ limit: 1,
25
+ });
26
+
27
+ return isMobile ? (
28
+ <>
29
+ <div
30
+ className="inline-flex flex-col justify-center items-start "
31
+ key={attr}
32
+ >
33
+ <div className="pl-1 inline-flex justify-center items-center gap-2.5">
34
+ <div className="justify-start text-[#3B3E5F] text-sm font-semibold">
35
+ {attr}
36
+ </div>
37
+ </div>
38
+ <div
39
+ className={twMerge(
40
+ `p-3 bg-[#e4e3ff] rounded-lg inline-flex justify-center items-center gap-1.5`,
41
+ 'text-[#3e36dc]',
42
+ specificationFilter[attr]
43
+ ? 'border-[#3E36DC] bg-[#3E36DC] text-white'
44
+ : '',
45
+ )}
46
+ onClick={() => {
47
+ if (!value) {
48
+ return;
49
+ }
50
+ if (attribute !== 'none') {
51
+ refine(value);
52
+ } else {
53
+ const setSpecificationFilter =
54
+ useRequestStore.getState().setSpecificationFilter;
55
+
56
+ const setSpecificationFilteredProducts =
57
+ useResultStore.getState().setSpecificationFilteredProducts;
58
+
59
+ if (specificationFilter[attr]) {
60
+ setSpecificationFilter({});
61
+ setSpecificationFilteredProducts([]);
62
+ // setProducts(results);
63
+ } else {
64
+ setSpecificationFilter({
65
+ [attr]: value,
66
+ });
67
+ }
68
+ }
69
+ }}
70
+ >
71
+ <div className="justify-start text-sm font-medium leading-none flex gap-2">
72
+ {imageAnalysis?.specification[attr] || 'N/A'}
73
+ <div>
74
+ <Icon
75
+ name="close"
76
+ className={twMerge(
77
+ 'w-3 h-3 text-white',
78
+ specificationFilter[attr] ? 'block' : 'hidden',
79
+ )}
80
+ />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </>
86
+ ) : (
87
+ <div key={attr} className="flex justify-between w-full gap-2 items-center">
88
+ <div className="self-stretch inline-flex justify-start items-center gap-1.5">
89
+ <div className="justify-start text-[#3B3E5F] text-xs font-semibold">
90
+ {attr}:
91
+ </div>
92
+ <Tooltip
93
+ content={
94
+ specificationFilter[attr]
95
+ ? 'Filter applied. Clear to choose a different value.'
96
+ : 'Click to apply as a search filter.'
97
+ }
98
+ delayDuration={1000}
99
+ disabled={!value}
100
+ >
101
+ <div
102
+ className={twMerge(
103
+ `px-1 py-1 bg-[#e4e3ff] rounded-[1px] flex justify-center items-center gap-1.5`,
104
+ 'border border-solid border-transparent hover:border-[#3E36DC]',
105
+ 'cursor-pointer',
106
+ specificationFilter[attr] ? 'border-[#3E36DC] bg-[#3E36DC] ' : '',
107
+ )}
108
+ onClick={() => {
109
+ if (!value) {
110
+ return;
111
+ }
112
+ if (attribute !== 'none') {
113
+ refine(value);
114
+ } else {
115
+ const setSpecificationFilter =
116
+ useRequestStore.getState().setSpecificationFilter;
117
+
118
+ const setSpecificationFilteredProducts =
119
+ useResultStore.getState().setSpecificationFilteredProducts;
120
+
121
+ if (specificationFilter[attr]) {
122
+ setSpecificationFilter({});
123
+ setSpecificationFilteredProducts([]);
124
+ // setProducts(results);
125
+ } else {
126
+ setSpecificationFilter({
127
+ [attr]: value,
128
+ });
129
+ }
130
+ }
131
+ }}
132
+ >
133
+ <div
134
+ className={twMerge(
135
+ 'justify-start text-[#3e36dc] text-[10px] leading-none px-0.5',
136
+ 'font-normal hover:font-bold hover:px-0',
137
+ specificationFilter[attr]
138
+ ? 'font-bold text-white hover:px-0.5'
139
+ : '',
140
+ 'max-line-1',
141
+ )}
142
+ >
143
+ {imageAnalysis?.specification[attr] || 'N/A'}
144
+ </div>
145
+ </div>
146
+ </Tooltip>
147
+ </div>
148
+ <div
149
+ onClick={() => {
150
+ navigator.clipboard.writeText(
151
+ imageAnalysis?.specification[attr] || '',
152
+ );
153
+ }}
154
+ >
155
+ <Icon
156
+ name="copy"
157
+ className="text-[#AAABB5] w-[12px] h-[12px] hover:text-[#3E36DC] cursor-pointer"
158
+ />
159
+ </div>
160
+ </div>
161
+ );
162
+ };
163
+
164
+ export default ItemSpecification;
@@ -0,0 +1,165 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { find } from 'services/visualSearch';
3
+ import useRequestStore from 'stores/request/requestStore';
4
+ import useResultStore from 'stores/result/resultStore';
5
+ import { twMerge } from 'tailwind-merge';
6
+
7
+ const MatchNotificationBanner = ({ className }: { className?: string }) => {
8
+ const [goBack, setGoBack] = useState(false);
9
+ const [textSearch, setTextSearch] = useState<
10
+ 'loading' | 'resolved' | 'pending'
11
+ >('pending');
12
+
13
+ const [prevResult, setPrevResult] = useState<any[]>([]);
14
+
15
+ const productsFromFindApi = useResultStore(
16
+ state => state.productsFromFindApi,
17
+ );
18
+
19
+ const groundingFilterResult = useResultStore(
20
+ state => state.groundingFilterResult,
21
+ );
22
+
23
+ const showBanner = useMemo(() => {
24
+ if (groundingFilterResult.length !== productsFromFindApi.length) {
25
+ return true;
26
+ }
27
+
28
+ // Compare the order of SKUs (or fallback to id)
29
+ const getSku = (item: any) => item?.sku ?? '';
30
+ for (let i = 0; i < productsFromFindApi.length; i++) {
31
+ if (getSku(productsFromFindApi[i]) !== getSku(groundingFilterResult[i])) {
32
+ return true;
33
+ }
34
+ }
35
+ // If all SKUs match in order
36
+ return false;
37
+ }, [groundingFilterResult, productsFromFindApi]);
38
+
39
+ useEffect(() => {
40
+ setGoBack(false);
41
+ setTextSearch('pending');
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ if (textSearch === 'pending') {
46
+ setPrevResult(productsFromFindApi || []);
47
+ }
48
+
49
+ return () => {};
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [productsFromFindApi]);
52
+
53
+ const showingGroundingFilterResult = useResultStore(
54
+ state => state.showingGroundingFilterResult,
55
+ );
56
+
57
+ const groundingQuery = useRequestStore(state => state.groundingQuery);
58
+ if (goBack) {
59
+ return <></>;
60
+ }
61
+ if (
62
+ (!groundingFilterResult || groundingFilterResult?.length === 0) &&
63
+ !groundingQuery
64
+ ) {
65
+ return <></>;
66
+ }
67
+
68
+ if (groundingQuery && groundingFilterResult?.length === 0) {
69
+ return (
70
+ <div
71
+ className={twMerge([
72
+ 'flex items-center justify-between w-full bg-[#F2FBF3] rounded-[16px] p-2 pl-2 shadow-sm',
73
+ 'h-[56px]',
74
+ className,
75
+ ])}
76
+ >
77
+ {/* Left Section: Icon and Text */}
78
+ <div className="flex items-center gap-3">
79
+ <div className="h-2.5 w-2.5 rounded-full bg-[#10b981]" />
80
+ {textSearch === 'resolved' && (
81
+ <p className="text-[#4a5568] text-sm md:text-base">
82
+ Didn't find the correct product?
83
+ </p>
84
+ )}
85
+ {textSearch !== 'resolved' && (
86
+ <p className="text-[#4a5568] text-sm md:text-base">
87
+ Are you looking for ?{' '}
88
+ <span className="font-bold">{groundingQuery}</span>
89
+ </p>
90
+ )}
91
+ </div>
92
+
93
+ {/* Right Section: Action Button */}
94
+ {textSearch === 'resolved' && (
95
+ <button
96
+ className="bg-[#373b53] hover:bg-[#2d3145] text-white px-6 py-3 rounded-lg text-xs font-semibold transition-colors duration-200"
97
+ onClick={() => {
98
+ setGoBack(true);
99
+ useResultStore.getState().setFindApiProducts(prevResult);
100
+ }}
101
+ >
102
+ Show all results
103
+ </button>
104
+ )}
105
+
106
+ {textSearch !== 'resolved' && (
107
+ <button
108
+ className="bg-[#373b53] hover:bg-[#2d3145] text-white px-6 py-3 rounded-lg text-xs font-semibold transition-colors duration-200"
109
+ onClick={() => {
110
+ setTextSearch('loading');
111
+ find({
112
+ settings: window.settings,
113
+ text: groundingQuery,
114
+ }).then(res => {
115
+ useResultStore.getState().setFindApiProducts(res?.results);
116
+ setTextSearch('resolved');
117
+ });
118
+ }}
119
+ >
120
+ Search now
121
+ </button>
122
+ )}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ if (!showBanner) {
128
+ return <></>;
129
+ }
130
+
131
+ return (
132
+ <div
133
+ className={twMerge([
134
+ 'flex items-center justify-between w-full bg-[#F2FBF3] rounded-[16px] p-2 pl-2 shadow-sm',
135
+ className,
136
+ ])}
137
+ >
138
+ {/* Left Section: Icon and Text */}
139
+ <div className="flex items-center gap-3">
140
+ <div className="h-2.5 w-2.5 rounded-full bg-[#10b981]" />
141
+ <p className="text-[#4a5568] text-sm md:text-base">
142
+ {showingGroundingFilterResult ? 'Showing' : 'Found'}{' '}
143
+ <span className="font-bold text-slate-900 pr-1">
144
+ {groundingFilterResult?.length}
145
+ </span>
146
+ matching product in results
147
+ </p>
148
+ </div>
149
+
150
+ {/* Right Section: Action Button */}
151
+ <button
152
+ className="bg-[#373b53] hover:bg-[#2d3145] text-white px-6 py-3 rounded-lg text-xs font-semibold transition-colors duration-200"
153
+ onClick={() => {
154
+ useResultStore.getState().setShowingGroundingFilterResult();
155
+ }}
156
+ >
157
+ {showingGroundingFilterResult
158
+ ? 'Show all results'
159
+ : 'Show only matching results'}
160
+ </button>
161
+ </div>
162
+ );
163
+ };
164
+
165
+ export default MatchNotificationBanner;
@@ -12,10 +12,14 @@ function PostFilter({
12
12
  attribute,
13
13
  label,
14
14
  searchable,
15
+ onVisibilityChange,
16
+ isLastVisible,
15
17
  }: {
16
18
  attribute: string;
17
19
  label: string;
18
20
  searchable: boolean;
21
+ onVisibilityChange?: (attribute: string, isVisible: boolean) => void;
22
+ isLastVisible?: boolean;
19
23
  }) {
20
24
  const [itemsLimit, setItemsLimit] = useState(10);
21
25
  const [searchInput, setSearchInput] = useState<string>('');
@@ -33,13 +37,30 @@ function PostFilter({
33
37
  // eslint-disable-next-line react-hooks/exhaustive-deps
34
38
  }, [searchInput]);
35
39
 
40
+ const hasSelection = items.some(item => item.isRefined);
41
+ const hasAvailableValues = items.length > 0 || searchInput.length > 0;
42
+ const isVisible = hasAvailableValues || hasSelection;
43
+
44
+ useEffect(() => {
45
+ onVisibilityChange?.(attribute, isVisible);
46
+ }, [attribute, isVisible, onVisibilityChange]);
47
+
48
+ if (!isVisible) {
49
+ return null;
50
+ }
51
+
36
52
  const onShowMore = () => {
37
53
  setItemsLimit(prev => prev + 10);
38
54
  };
39
55
 
40
56
  return (
41
57
  <>
42
- <AccordionItem value={attribute}>
58
+ <AccordionItem
59
+ value={attribute}
60
+ className={
61
+ isLastVisible ? 'py-4' : 'border-b border-solid border-[#e0e0e0] py-4'
62
+ }
63
+ >
43
64
  <AccordionTrigger className="text-sm font-semibold w-full h-8 items-center">
44
65
  {label}
45
66
  </AccordionTrigger>