@nyris/nyris-webapp 0.3.88 → 0.3.90
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/build/_headers +2 -0
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +17 -0
- package/build/static/css/main.734b52e1.css +4 -0
- package/build/static/css/main.734b52e1.css.map +1 -0
- package/build/static/js/main.cede3ae1.js +3 -0
- package/build/static/js/{main.e861b336.js.map → main.cede3ae1.js.map} +1 -1
- package/package.json +3 -3
- package/public/_headers +2 -0
- package/public/index.html +1 -1
- package/public/js/settings.example.js +17 -0
- package/src/App.tsx +5 -3
- package/src/components/Cart.tsx +321 -0
- package/src/components/CustomCameraDrawer.tsx +4 -22
- package/src/components/DragDropFile.tsx +57 -38
- package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
- package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
- package/src/components/GroundingSpecs.tsx +47 -0
- package/src/components/Header.tsx +94 -93
- package/src/components/HitsPerPage.tsx +4 -2
- package/src/components/ImagePreview.tsx +64 -31
- package/src/components/ImageUpload.tsx +247 -0
- package/src/components/ItemSpecification.tsx +164 -0
- package/src/components/MatchNotificationBanner.tsx +165 -0
- package/src/components/PostFilter/PostFilter.tsx +22 -1
- package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
- package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
- package/src/components/PoweredBy.tsx +16 -0
- package/src/components/PreFilter/PreFilter.tsx +77 -54
- package/src/components/Product/Product.tsx +186 -28
- package/src/components/Product/ProductAttribute.tsx +2 -2
- package/src/components/Product/ProductDetailView.tsx +123 -18
- package/src/components/Product/ProductDetailViewModal.tsx +3 -0
- package/src/components/Product/ProductList.tsx +78 -8
- package/src/components/SidePanel.tsx +212 -120
- package/src/components/TextSearch.tsx +82 -203
- package/src/components/Toaster.tsx +34 -15
- package/src/helpers/ToastHelper.ts +6 -2
- package/src/hooks/useCadSearch.ts +5 -0
- package/src/hooks/useImageSearch.ts +102 -13
- package/src/index.css +59 -0
- package/src/layouts/AppLayout.tsx +16 -14
- package/src/pages/Home.tsx +61 -13
- package/src/pages/Result.tsx +287 -295
- package/src/services/vizo.ts +161 -0
- package/src/stores/request/Misc/misc.initialstate.ts +1 -0
- package/src/stores/request/Misc/misc.slice.ts +1 -0
- package/src/stores/request/filter/filter.initialState.ts +3 -0
- package/src/stores/request/filter/filter.slice.ts +23 -0
- package/src/stores/result/prodcuts/products.initialState.ts +4 -0
- package/src/stores/result/prodcuts/products.slice.ts +15 -0
- package/src/stores/types.ts +27 -1
- package/src/stores/ui/loading/loading.initialState.ts +1 -0
- package/src/stores/ui/loading/loading.slice.ts +4 -0
- package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
- package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
- package/src/stores/ui/uiStore.ts +4 -1
- package/src/styles/Cart.scss +210 -0
- package/src/styles/common.scss +10 -0
- package/src/translations.ts +4 -4
- package/src/types.ts +11 -3
- package/src/utils/prepareImageList.ts +6 -5
- package/src/utils/textSearchFilter.ts +203 -0
- package/tailwind.config.js +1 -0
- package/build/static/css/main.ba1c7479.css +0 -4
- package/build/static/css/main.ba1c7479.css.map +0 -1
- package/build/static/js/main.e861b336.js +0 -3
- package/src/components/Footer.tsx +0 -21
- /package/build/static/js/{main.e861b336.js.LICENSE.txt → main.cede3ae1.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
|
|
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>
|