@nyris/nyris-webapp 0.3.94 → 0.3.96

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/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nyris/nyris-webapp",
3
- "version": "0.3.94",
3
+ "version": "0.3.96",
4
4
  "dependencies": {
5
5
  "@auth0/auth0-react": "^2.2.4",
6
6
  "@emailjs/browser": "^4.3.3",
7
7
  "@google/genai": "^1.46.0",
8
- "@nyris/nyris-api": "^0.3.94",
9
- "@nyris/nyris-react-components": "^0.3.94",
8
+ "@nyris/nyris-api": "^0.3.96",
9
+ "@nyris/nyris-react-components": "^0.3.96",
10
10
  "@radix-ui/react-accordion": "^1.2.2",
11
11
  "@radix-ui/react-dialog": "^1.1.4",
12
12
  "@radix-ui/react-popover": "^1.1.4",
@@ -4,13 +4,10 @@ import { twMerge } from 'tailwind-merge';
4
4
  import Tooltip from './Tooltip/TooltipComponent';
5
5
  import { useAuth0 } from '@auth0/auth0-react';
6
6
  import { useMemo, useState } from 'react';
7
- import { useLocation, useNavigate } from 'react-router';
7
+ import { useLocation } from 'react-router';
8
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
9
  import UploadDisclaimer from './UploadDisclaimer';
10
+ import { useUploadSearch } from 'hooks/useUploadSearch';
14
11
 
15
12
  function ImageUpload({
16
13
  onCameraClick,
@@ -25,27 +22,10 @@ function ImageUpload({
25
22
  const { t } = useTranslation();
26
23
 
27
24
  const location = useLocation();
28
- const navigate = useNavigate();
25
+ const { uploadFile } = useUploadSearch();
29
26
 
30
- const preFilterList = useRequestStore(state => state.preFilterList);
31
27
  const requestImages = useRequestStore(state => state.requestImages);
32
- const setQuery = useRequestStore(state => state.setQuery);
33
- const setValueInput = useRequestStore(state => state.setValueInput);
34
28
  const setMetaFilter = useRequestStore(state => state.setMetaFilter);
35
- const specifications = useRequestStore(state => state.specifications);
36
-
37
- const setRequestImages = useRequestStore(state => state.setRequestImages);
38
- const setSpecifications = useRequestStore(state => state.setSpecifications);
39
- const setShowLoading = useRequestStore(state => state.setShowLoading);
40
- const setNameplateNotificationText = useRequestStore(
41
- state => state.setNameplateNotificationText,
42
- );
43
- const setShowNotMatchedError = useRequestStore(
44
- state => state.setShowNotMatchedError,
45
- );
46
- const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
47
- const setPreFilter = useRequestStore(state => state.setPreFilter);
48
- const setNameplateImage = useRequestStore(state => state.setNameplateImage);
49
29
 
50
30
  const [showDisclaimer, setShowDisclaimer] = useState(false);
51
31
 
@@ -80,97 +60,13 @@ function ImageUpload({
80
60
  // eslint-disable-next-line react-hooks/exhaustive-deps
81
61
  }, [showDisclaimer, requestImages, disableDisclaimer]);
82
62
 
83
- const { singleImageSearch } = useImageSearch();
84
- const { cadSearch } = useCadSearch();
85
-
86
63
  const handleUpload = (files: File[]) => {
87
- setValueInput('');
88
- setQuery('');
89
-
90
- if (isCadFile(files[0])) {
91
- cadSearch({ file: files[0], settings, newSearch: true }).then(() => {});
92
-
64
+ const file = files[0];
65
+ if (!file) {
93
66
  return;
94
67
  }
95
68
 
96
- setShowLoading(true);
97
-
98
- singleImageSearch({
99
- image: files[0],
100
- settings: window.settings,
101
- showFeedback: true,
102
- newSearch: true,
103
- clearPostFilter: true,
104
- }).then(singleImageResp => {
105
- const specificationPrefilter =
106
- singleImageResp?.image_analysis?.specification?.prefilter_value || null;
107
- const hasPrefilter = preFilterList.filter((filter: any) =>
108
- filter.values.includes(specificationPrefilter),
109
- );
110
- if (specificationPrefilter) {
111
- setRequestImages([]);
112
- setShowNotMatchedError(false);
113
- if (hasPrefilter.length) {
114
- setSpecifications(
115
- clone(singleImageResp?.image_analysis?.specification),
116
- );
117
- setNameplateImage(files[0]);
118
- setPreFilter({
119
- [singleImageResp?.image_analysis?.specification?.prefilter_value]:
120
- true,
121
- });
122
- setAlgoliaFilter(
123
- `${settings.alogoliaFilterField}:'${singleImageResp?.image_analysis?.specification?.prefilter_value}'`,
124
- );
125
-
126
- setShowLoading(false);
127
- navigate('/result');
128
-
129
- setTimeout(() => {
130
- setNameplateNotificationText(
131
- t('We have successfully defined the search criteria', {
132
- prefilter_value: specificationPrefilter,
133
- preFilterTitle:
134
- window.settings.preFilterTitle?.toLocaleLowerCase(),
135
- }),
136
- );
137
- }, 1000);
138
- setTimeout(() => {
139
- setNameplateNotificationText('');
140
- }, 10000);
141
- }
142
- if (!hasPrefilter.length && showPreFilter) {
143
- setSpecifications(
144
- clone({
145
- ...singleImageResp?.image_analysis?.specification,
146
- specificationPrefilter,
147
- }),
148
- );
149
- navigate('/result');
150
- setPreFilter({});
151
- setAlgoliaFilter('');
152
- setShowLoading(false);
153
- setShowNotMatchedError(true);
154
- setTimeout(() => {
155
- setNameplateNotificationText(
156
- t('Extracted details from the nameplate could not be matched', {
157
- preFilterTitle:
158
- window.settings.preFilterTitle?.toLocaleLowerCase(),
159
- }),
160
- );
161
- }, 1000);
162
- setTimeout(() => {
163
- setNameplateNotificationText('');
164
- }, 15000);
165
- }
166
- } else {
167
- if (!specifications?.is_nameplate) {
168
- setSpecifications({ ...specifications, is_nameplate: false });
169
- }
170
- setShowLoading(false);
171
- navigate('/result');
172
- }
173
- });
69
+ uploadFile(file, { showPreFilter });
174
70
  };
175
71
 
176
72
  return (
@@ -6,9 +6,17 @@ import {
6
6
  } from 'components/Accordion';
7
7
  import { Icon } from '@nyris/nyris-react-components';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { isEmpty } from 'lodash';
9
10
  import useResultStore from 'stores/result/resultStore';
10
11
  import useRequestStore from 'stores/request/requestStore';
11
- import { filterProductsByText } from 'utils/textSearchFilter';
12
+ import useUiStore from 'stores/ui/uiStore';
13
+ import { find } from 'services/visualSearch';
14
+
15
+ let postFilterRequestCounter = 0;
16
+
17
+ export function invalidatePostFilterRequests() {
18
+ postFilterRequestCounter += 1;
19
+ }
12
20
 
13
21
  function PostFilterFindApi({
14
22
  attribute,
@@ -26,84 +34,53 @@ function PostFilterFindApi({
26
34
  const [itemsLimit, setItemsLimit] = useState(10);
27
35
  const [searchInput, setSearchInput] = useState<string>('');
28
36
 
29
- const productsFromFindApi = useResultStore(
30
- state => state.productsFromFindApi,
37
+ const postFilterFacets = useResultStore(state => state.postFilterFacets);
38
+ const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
39
+ const setPostFilterSelections = useRequestStore(
40
+ state => state.setPostFilterSelections,
31
41
  );
32
- const groundingFilterResult = useResultStore(
33
- state => state.groundingFilterResult,
34
- );
35
- const showingGroundingFilterResult = useResultStore(
36
- state => state.showingGroundingFilterResult,
37
- );
38
- const query = useRequestStore(state => state.query);
39
- const requestImages = useRequestStore(state => state.requestImages);
40
42
  const postFilterSelections = useRequestStore(
41
43
  state => state.postFilterSelections,
42
44
  );
43
- const togglePostFilterSelection = useRequestStore(
44
- state => state.togglePostFilterSelection,
45
- );
45
+ const query = useRequestStore(state => state.query);
46
+ const requestImages = useRequestStore(state => state.requestImages);
47
+ const regions = useRequestStore(state => state.regions);
48
+ const preFilter = useRequestStore(state => state.preFilter);
49
+ const preFilterList = useRequestStore(state => state.preFilterList);
50
+ const metaFilter = useRequestStore(state => state.metaFilter);
51
+ const setIsFindApiLoading = useUiStore(state => state.setIsFindApiLoading);
46
52
  const { t } = useTranslation();
53
+ const settings = window.settings;
47
54
 
48
- const productsForFilters = useMemo(() => {
49
- let baseProducts = productsFromFindApi;
50
-
51
- if (requestImages?.length && query?.trim()) {
52
- baseProducts = filterProductsByText(query, productsFromFindApi);
53
- }
54
-
55
- if (showingGroundingFilterResult && groundingFilterResult?.length > 0) {
56
- const grounded = baseProducts.filter((product: any) =>
57
- groundingFilterResult.some((result: any) => product.sku === result.sku),
58
- );
59
- if (grounded.length > 0) {
60
- return grounded;
61
- }
62
- }
63
-
64
- return baseProducts;
65
- }, [
66
- productsFromFindApi,
67
- query,
68
- requestImages,
69
- showingGroundingFilterResult,
70
- groundingFilterResult,
71
- ]);
72
-
73
- const filteredProductsForCounts = useMemo(() => {
74
- const selections = postFilterSelections || {};
75
- const activeSelections = Object.entries(selections).filter(
76
- ([attr, values]) => attr !== attribute && Array.isArray(values),
55
+ const matchedFacet = useMemo(() => {
56
+ const facets = Array.isArray(postFilterFacets) ? postFilterFacets : [];
57
+ return facets.find(
58
+ (entry: { name?: string }) =>
59
+ entry.name?.toLocaleLowerCase() === attribute.toLocaleLowerCase(),
77
60
  );
61
+ }, [attribute, postFilterFacets]);
78
62
 
79
- if (!activeSelections.length) {
80
- return productsForFilters;
81
- }
82
-
83
- return productsForFilters.filter((product: any) =>
84
- activeSelections.every(([attr, values]) => {
85
- const filterValues = product.filters?.[attr];
86
- if (!Array.isArray(filterValues) || filterValues.length === 0) {
87
- return false;
88
- }
89
- return values.some(value => filterValues.includes(value));
90
- }),
91
- );
92
- }, [attribute, postFilterSelections, productsForFilters]);
63
+ const preFilterApiKey =
64
+ preFilterList.find(
65
+ filter =>
66
+ filter.key?.toLocaleLowerCase() ===
67
+ settings.visualSearchFilterKey?.toLocaleLowerCase(),
68
+ )?.key ?? settings.visualSearchFilterKey;
93
69
 
94
70
  const valueCounts = useMemo(() => {
95
71
  const counts: Record<string, number> = {};
96
- filteredProductsForCounts?.forEach((product: any) => {
97
- const filterValues = product.filters?.[attribute];
98
- if (Array.isArray(filterValues)) {
99
- filterValues.forEach((val: string) => {
100
- if (val) counts[val] = (counts[val] || 0) + 1;
101
- });
102
- }
103
- });
72
+
73
+ (matchedFacet?.values || []).forEach(
74
+ (entry: { value?: string; count?: number }) => {
75
+ const value = entry?.value;
76
+ if (value) {
77
+ counts[value] = entry.count ?? 0;
78
+ }
79
+ },
80
+ );
104
81
 
105
82
  return counts;
106
- }, [attribute, filteredProductsForCounts]);
83
+ }, [matchedFacet]);
107
84
 
108
85
  const items = useMemo(() => {
109
86
  const selectedValues = postFilterSelections?.[attribute] || [];
@@ -138,6 +115,85 @@ function PostFilterFindApi({
138
115
  onVisibilityChange?.(attribute, isVisible);
139
116
  }, [attribute, isVisible, onVisibilityChange]);
140
117
 
118
+ const handleFilterClick = (value: string, isRefined: boolean) => {
119
+ const currentSelections =
120
+ useRequestStore.getState().postFilterSelections || {};
121
+ const currentAttributeValues = currentSelections[attribute] || [];
122
+ const nextAttributeValues = isRefined
123
+ ? currentAttributeValues.filter(item => item !== value)
124
+ : [...currentAttributeValues, value];
125
+
126
+ const nextSelections = { ...currentSelections };
127
+ if (nextAttributeValues.length > 0) {
128
+ nextSelections[attribute] = nextAttributeValues;
129
+ } else {
130
+ delete nextSelections[attribute];
131
+ }
132
+
133
+ setPostFilterSelections(nextSelections);
134
+
135
+ const hasAnySelection = Object.values(nextSelections).some(
136
+ values => values.length > 0,
137
+ );
138
+
139
+ if (!hasAnySelection) {
140
+ const snapshot = useResultStore.getState().initialFindSearchSnapshot;
141
+ if (snapshot) {
142
+ setFindApiProducts(snapshot.results);
143
+ }
144
+ return;
145
+ }
146
+
147
+ const facets = useResultStore.getState().postFilterFacets || [];
148
+ const filters = [];
149
+
150
+ if (!isEmpty(preFilter)) {
151
+ filters.push({
152
+ key: preFilterApiKey,
153
+ values: Object.keys(preFilter).filter(key => preFilter[key]),
154
+ });
155
+ } else if (metaFilter) {
156
+ filters.push({
157
+ key: preFilterApiKey,
158
+ values: [metaFilter],
159
+ });
160
+ }
161
+
162
+ Object.entries(nextSelections).forEach(([attr, values]) => {
163
+ if (values.length > 0) {
164
+ const facet = facets.find(
165
+ (entry: { name?: string }) =>
166
+ entry.name?.toLocaleLowerCase() === attr.toLocaleLowerCase(),
167
+ );
168
+ filters.push({
169
+ key: facet?.name ?? attr,
170
+ values,
171
+ });
172
+ }
173
+ });
174
+
175
+ const requestId = ++postFilterRequestCounter;
176
+ setIsFindApiLoading(true);
177
+ void find({
178
+ image: requestImages[0],
179
+ settings,
180
+ region: regions[0],
181
+ filters,
182
+ text: !requestImages[0] ? query : undefined,
183
+ })
184
+ .then(response => {
185
+ if (requestId !== postFilterRequestCounter) {
186
+ return;
187
+ }
188
+ setFindApiProducts(response?.results || []);
189
+ })
190
+ .finally(() => {
191
+ if (requestId === postFilterRequestCounter) {
192
+ setIsFindApiLoading(false);
193
+ }
194
+ });
195
+ };
196
+
141
197
  if (!isVisible) {
142
198
  return null;
143
199
  }
@@ -208,9 +264,7 @@ function PostFilterFindApi({
208
264
  <input
209
265
  type="checkbox"
210
266
  checked={item.isRefined}
211
- onChange={() =>
212
- togglePostFilterSelection(attribute, item.value)
213
- }
267
+ onChange={() => handleFilterClick(item.value, item.isRefined)}
214
268
  className="cursor-pointer"
215
269
  />
216
270
  <span className="text-xs font-normal pl-2 pr-1">
@@ -76,10 +76,16 @@ const PreFilterComponent = (props: Props) => {
76
76
  };
77
77
  }, []);
78
78
 
79
+ const matchedPreFilter = preFilterList.find(
80
+ value =>
81
+ value.key?.toLocaleLowerCase() ===
82
+ settings.visualSearchFilterKey?.toLocaleLowerCase(),
83
+ );
84
+ const preFilterApiKey =
85
+ matchedPreFilter?.key ?? settings.visualSearchFilterKey;
86
+
79
87
  const getDataFilterDesktop = () => {
80
- const arrResult =
81
- preFilterList.find(value => value.key === settings.visualSearchFilterKey)
82
- ?.values || [];
88
+ const arrResult = matchedPreFilter?.values || [];
83
89
 
84
90
  const newResult = arrResult.sort().reduce((a: any, c: any) => {
85
91
  if (!c[0]) return a;
@@ -100,7 +106,7 @@ const PreFilterComponent = (props: Props) => {
100
106
  }
101
107
  setLoading(true);
102
108
  const data = await searchFilters(
103
- settings.visualSearchFilterKey,
109
+ preFilterApiKey,
104
110
  encodeURIComponent(value),
105
111
  settings,
106
112
  )
@@ -63,6 +63,10 @@ function ProductList({ sendFeedBackAction }: Props): JSX.Element {
63
63
  }, [productsFromFindApi, query, requestImages]);
64
64
 
65
65
  const postFilteredProducts = useMemo(() => {
66
+ if (!window.settings.algolia?.enabled && window.settings.postFilterOption) {
67
+ return textFilteredProducts;
68
+ }
69
+
66
70
  const entries = Object.entries(postFilterSelections || {}).filter(
67
71
  ([, values]) => values?.length,
68
72
  );
@@ -15,8 +15,9 @@ export const Toaster = () => {
15
15
  <ReactHotToaster
16
16
  position={isMobile ? 'bottom-center' : 'top-right'}
17
17
  containerStyle={{
18
- top: 80,
19
- bottom: isMobile ? 80 : 'auto',
18
+ top: 100,
19
+ bottom: isMobile ? 90 : 'auto',
20
+ right: isMobile ? 15 : 25,
20
21
  }}
21
22
  >
22
23
  {t => (
@@ -26,7 +26,12 @@ export function useEffectiveGroundingResults() {
26
26
  const postEntries = Object.entries(postFilterSelections || {}).filter(
27
27
  ([, values]) => values?.length,
28
28
  );
29
- if (postEntries.length > 0) {
29
+ if (
30
+ postEntries.length > 0 &&
31
+ !(
32
+ !window.settings.algolia?.enabled && window.settings.postFilterOption
33
+ )
34
+ ) {
30
35
  filteredProducts = filteredProducts.filter((product: any) => {
31
36
  const productFilters = product?.filters || {};
32
37
  return postEntries.every(([attribute, selectedValues]) => {
@@ -19,6 +19,7 @@ import useConversationStore from 'stores/chat/conversationStore';
19
19
  import { useGroundedSearch } from './useGroundedSearch';
20
20
  import { generateSmartFilters } from '../services/vizo';
21
21
  import { cropImage } from 'utils/cropImageToBase64';
22
+ import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
22
23
 
23
24
  export const useImageSearch = () => {
24
25
  const setRegions = useRequestStore(state => state.setRegions);
@@ -68,6 +69,10 @@ export const useImageSearch = () => {
68
69
  state => state.setGroundingFilterResult,
69
70
  );
70
71
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
72
+ const setPostFilterFacets = useResultStore(state => state.setPostFilterFacets);
73
+ const setInitialFindSearchSnapshot = useResultStore(
74
+ state => state.setInitialFindSearchSnapshot,
75
+ );
71
76
 
72
77
  const setGroundingError = useResultStore(state => state.setGroundingError);
73
78
 
@@ -110,6 +115,8 @@ export const useImageSearch = () => {
110
115
  setGroundingFilterResult([]);
111
116
  setGoogleGroundingResponse(null);
112
117
  setGroundingError(null);
118
+ setPostFilterFacets([]);
119
+ setInitialFindSearchSnapshot(null);
113
120
  if (isSmartFiltersEnabled) {
114
121
  setSmartFilters({
115
122
  status: 'idle',
@@ -185,9 +192,17 @@ export const useImageSearch = () => {
185
192
  }
186
193
  }
187
194
 
195
+ const preFilterList = useRequestStore.getState().preFilterList;
196
+ const preFilterApiKey =
197
+ preFilterList.find(
198
+ filter =>
199
+ filter.key?.toLocaleLowerCase() ===
200
+ settings.visualSearchFilterKey?.toLocaleLowerCase(),
201
+ )?.key ?? settings.visualSearchFilterKey;
202
+
188
203
  const preFilterValues = [
189
204
  {
190
- key: settings.visualSearchFilterKey,
205
+ key: preFilterApiKey,
191
206
  values: Object.keys(preFilterParams || preFilter),
192
207
  },
193
208
  ];
@@ -272,6 +287,22 @@ export const useImageSearch = () => {
272
287
  });
273
288
  }
274
289
 
290
+ const shouldResetPostFilters =
291
+ clearPostFilter ||
292
+ (!settings.algolia?.enabled &&
293
+ !requestImage &&
294
+ settings.postFilterOption);
295
+
296
+ if (shouldResetPostFilters) {
297
+ invalidatePostFilterRequests();
298
+ refine();
299
+ clearPostFilterSelections();
300
+ if (clearPostFilter) {
301
+ setPostFilterFacets([]);
302
+ setInitialFindSearchSnapshot(null);
303
+ }
304
+ }
305
+
275
306
  try {
276
307
  res = await find({
277
308
  image: requestImage,
@@ -281,7 +312,7 @@ export const useImageSearch = () => {
281
312
  : metaFilter
282
313
  ? [
283
314
  {
284
- key: settings.visualSearchFilterKey,
315
+ key: preFilterApiKey,
285
316
  values: [metaFilter],
286
317
  },
287
318
  ]
@@ -295,12 +326,14 @@ export const useImageSearch = () => {
295
326
  : undefined,
296
327
  });
297
328
  useResultStore.getState().resetGrounding();
298
- if (clearPostFilter) {
299
- refine();
300
- clearPostFilterSelections();
301
- }
302
329
 
303
330
  setFindApiProducts(res?.results);
331
+ const initialFacets = res?.facets || [];
332
+ setPostFilterFacets(initialFacets);
333
+ setInitialFindSearchSnapshot({
334
+ results: res?.results || [],
335
+ postFilterFacets: initialFacets,
336
+ });
304
337
  setImageAnalysis(res?.image_analysis);
305
338
  setSessionId(res?.session);
306
339
  setRequestId(res?.id);
@@ -386,6 +419,8 @@ export const useImageSearch = () => {
386
419
  setFirstSearchPreFilter,
387
420
  setFirstRequestImageAnalysis,
388
421
  setSmartFilters,
422
+ setPostFilterFacets,
423
+ setInitialFindSearchSnapshot,
389
424
  ],
390
425
  );
391
426