@nyris/nyris-webapp 0.3.96 → 0.3.98

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 (48) hide show
  1. package/build/asset-manifest.json +6 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +7 -1
  4. package/build/static/css/main.81216c5d.css +4 -0
  5. package/build/static/css/main.81216c5d.css.map +1 -0
  6. package/build/static/js/main.93fdbd6e.js +3 -0
  7. package/build/static/js/{main.fd12cce3.js.map → main.93fdbd6e.js.map} +1 -1
  8. package/package.json +3 -3
  9. package/public/js/settings.example.js +7 -1
  10. package/src/components/AuthApiKeySync.tsx +34 -0
  11. package/src/components/Cart.tsx +200 -85
  12. package/src/components/ChatAssistant/useChatAssistantLogic.ts +3 -2
  13. package/src/components/Header.tsx +1 -1
  14. package/src/components/ImageUpload.tsx +2 -2
  15. package/src/components/Inquiry/InquiryModal.tsx +2 -2
  16. package/src/components/LogoutModal.tsx +2 -0
  17. package/src/components/PostFilter/PostFilter.tsx +36 -3
  18. package/src/components/PostFilter/PostFilterComponent.tsx +8 -22
  19. package/src/components/PostFilter/PostFilterFindApi.tsx +41 -3
  20. package/src/components/PreFilter/PreFilter.tsx +30 -2
  21. package/src/components/Product/Product.tsx +2 -2
  22. package/src/components/Product/ProductDetailView.tsx +2 -2
  23. package/src/components/Product/ProductList.tsx +2 -0
  24. package/src/components/ScopeMissing.tsx +50 -0
  25. package/src/components/SidePanel.tsx +10 -0
  26. package/src/components/SmartFilter.tsx +59 -69
  27. package/src/components/TextSearch.tsx +6 -4
  28. package/src/hooks/useImageSearch.ts +44 -9
  29. package/src/hooks/useResultEvaluator.ts +8 -8
  30. package/src/index.tsx +4 -0
  31. package/src/layouts/AppLayout.tsx +77 -10
  32. package/src/pages/Logout.tsx +2 -0
  33. package/src/pages/Result.tsx +14 -7
  34. package/src/providers/AuthProvider.tsx +5 -0
  35. package/src/routes/ProctedRoute.tsx +52 -9
  36. package/src/services/Feedback.ts +2 -1
  37. package/src/services/filter.ts +16 -2
  38. package/src/services/visualSearch.ts +15 -5
  39. package/src/services/vizo.ts +13 -2
  40. package/src/stores/scope/scopeStore.ts +35 -0
  41. package/src/styles/Cart.scss +22 -0
  42. package/src/types.ts +6 -1
  43. package/src/utils/derivePostFilterFacets.ts +68 -0
  44. package/src/utils/nyrisApiKey.ts +115 -0
  45. package/build/static/css/main.5fff4ca0.css +0 -4
  46. package/build/static/css/main.5fff4ca0.css.map +0 -1
  47. package/build/static/js/main.fd12cce3.js +0 -3
  48. /package/build/static/js/{main.fd12cce3.js.LICENSE.txt → main.93fdbd6e.js.LICENSE.txt} +0 -0
@@ -10,7 +10,9 @@ import { isEmpty } from 'lodash';
10
10
  import useResultStore from 'stores/result/resultStore';
11
11
  import useRequestStore from 'stores/request/requestStore';
12
12
  import useUiStore from 'stores/ui/uiStore';
13
+ import useScopeStore from 'stores/scope/scopeStore';
13
14
  import { find } from 'services/visualSearch';
15
+ import { useAuth0 } from '@auth0/auth0-react';
14
16
 
15
17
  let postFilterRequestCounter = 0;
16
18
 
@@ -31,6 +33,8 @@ function PostFilterFindApi({
31
33
  onVisibilityChange?: (attribute: string, isVisible: boolean) => void;
32
34
  isLastVisible?: boolean;
33
35
  }) {
36
+ const { user } = useAuth0();
37
+
34
38
  const [itemsLimit, setItemsLimit] = useState(10);
35
39
  const [searchInput, setSearchInput] = useState<string>('');
36
40
 
@@ -52,6 +56,23 @@ function PostFilterFindApi({
52
56
  const { t } = useTranslation();
53
57
  const settings = window.settings;
54
58
 
59
+ const scopeFilterEnabled = settings.scopeFilter;
60
+ const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
61
+
62
+ const scopedAllowedValues = useMemo(() => {
63
+ if (!scopeFilterEnabled || !window.settings.auth0?.enabled) {
64
+ return null;
65
+ }
66
+ // todo: support multiple attributes in the future
67
+ const scopeAttribute = scopeFilterAttributes[0];
68
+ if (!scopeAttribute) {
69
+ return new Set<string>();
70
+ }
71
+ return new Set(
72
+ scopeAttribute.values.map(value => value.toLocaleLowerCase()),
73
+ );
74
+ }, [scopeFilterEnabled, scopeFilterAttributes]);
75
+
55
76
  const matchedFacet = useMemo(() => {
56
77
  const facets = Array.isArray(postFilterFacets) ? postFilterFacets : [];
57
78
  return facets.find(
@@ -96,6 +117,11 @@ function PostFilterFindApi({
96
117
  count: valueCounts[label] || 0,
97
118
  isRefined: selectedValues.includes(label),
98
119
  }))
120
+ .filter(
121
+ item =>
122
+ !scopedAllowedValues ||
123
+ scopedAllowedValues.has(item.value.toLocaleLowerCase()),
124
+ )
99
125
  .filter(
100
126
  item =>
101
127
  !searchInput ||
@@ -103,13 +129,22 @@ function PostFilterFindApi({
103
129
  )
104
130
  .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
105
131
  .slice(0, itemsLimit);
106
- }, [attribute, itemsLimit, postFilterSelections, searchInput, valueCounts]);
132
+ }, [
133
+ attribute,
134
+ itemsLimit,
135
+ postFilterSelections,
136
+ scopedAllowedValues,
137
+ searchInput,
138
+ valueCounts,
139
+ ]);
107
140
 
108
141
  const selectedValues = postFilterSelections?.[attribute] || [];
109
142
  const hasSelection = selectedValues.length > 0;
110
143
  const hasAvailableValues = Object.keys(valueCounts).length > 0;
111
144
  const hasSearch = searchable && searchInput.length > 0;
112
- const isVisible = hasAvailableValues || hasSelection || hasSearch;
145
+ const isVisible =
146
+ (!scopedAllowedValues || scopedAllowedValues.size > 0) &&
147
+ (hasAvailableValues || hasSelection || hasSearch);
113
148
 
114
149
  useEffect(() => {
115
150
  onVisibilityChange?.(attribute, isVisible);
@@ -180,6 +215,7 @@ function PostFilterFindApi({
180
215
  region: regions[0],
181
216
  filters,
182
217
  text: !requestImages[0] ? query : undefined,
218
+ scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
183
219
  })
184
220
  .then(response => {
185
221
  if (requestId !== postFilterRequestCounter) {
@@ -264,7 +300,9 @@ function PostFilterFindApi({
264
300
  <input
265
301
  type="checkbox"
266
302
  checked={item.isRefined}
267
- onChange={() => handleFilterClick(item.value, item.isRefined)}
303
+ onChange={() =>
304
+ handleFilterClick(item.value, item.isRefined)
305
+ }
268
306
  className="cursor-pointer"
269
307
  />
270
308
  <span className="text-xs font-normal pl-2 pr-1">
@@ -10,6 +10,7 @@ import { truncateString } from 'utils/truncateString';
10
10
  import { twMerge } from 'tailwind-merge';
11
11
  import Tooltip from 'components/Tooltip/TooltipComponent';
12
12
  import useResultStore from '../../stores/result/resultStore';
13
+ import useScopeStore from 'stores/scope/scopeStore';
13
14
 
14
15
  interface Props {
15
16
  handleClose?: any;
@@ -42,6 +43,9 @@ const PreFilterComponent = (props: Props) => {
42
43
  const setImageAnalysis = useResultStore(state => state.setImageAnalysis);
43
44
  const regions = useRequestStore(state => state.regions);
44
45
 
46
+ const scopeFilterEnabled = !!settings.scopeFilter;
47
+ const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
48
+
45
49
  const [keyFilter, setKeyFilter] = useState<Record<string, boolean>>(
46
50
  keyFilterState || {},
47
51
  );
@@ -66,7 +70,7 @@ const PreFilterComponent = (props: Props) => {
66
70
  setLoading(true);
67
71
  getDataFilterDesktop();
68
72
  // eslint-disable-next-line react-hooks/exhaustive-deps
69
- }, [preFilterList, searchKey]);
73
+ }, [preFilterList, scopeFilterAttributes, searchKey]);
70
74
 
71
75
  useEffect(() => {
72
76
  return () => {
@@ -76,7 +80,11 @@ const PreFilterComponent = (props: Props) => {
76
80
  };
77
81
  }, []);
78
82
 
79
- const matchedPreFilter = preFilterList.find(
83
+ const activeFilterList = scopeFilterEnabled
84
+ ? scopeFilterAttributes
85
+ : preFilterList;
86
+
87
+ const matchedPreFilter = activeFilterList.find(
80
88
  value =>
81
89
  value.key?.toLocaleLowerCase() ===
82
90
  settings.visualSearchFilterKey?.toLocaleLowerCase(),
@@ -105,6 +113,26 @@ const PreFilterComponent = (props: Props) => {
105
113
  return;
106
114
  }
107
115
  setLoading(true);
116
+
117
+ if (scopeFilterEnabled) {
118
+ const arrResult = matchedPreFilter?.values || [];
119
+ const lowerValue = String(value).toLocaleLowerCase();
120
+ const res = arrResult.filter(item =>
121
+ String(item).toLocaleLowerCase().includes(lowerValue),
122
+ );
123
+ if (res.length > 0) {
124
+ setResultFilter({ [res[0][0].toLocaleUpperCase()]: res });
125
+ if (res.length <= 20) setColumns(1);
126
+ else if (res.length <= 40) setColumns(2);
127
+ else setColumns(4);
128
+ } else {
129
+ setResultFilter({});
130
+ setColumns(4);
131
+ }
132
+ setLoading(false);
133
+ return;
134
+ }
135
+
108
136
  const data = await searchFilters(
109
137
  preFilterApiKey,
110
138
  encodeURIComponent(value),
@@ -673,7 +673,7 @@ function Product(props: Props) {
673
673
  </div>
674
674
  )}
675
675
 
676
- {settings.cart && (
676
+ {settings.cart?.enabled && (
677
677
  <div
678
678
  style={{
679
679
  boxShadow: '-2px 2px 4px rgba(170, 171, 181, 0.5)',
@@ -717,7 +717,7 @@ function Product(props: Props) {
717
717
  paddingRight: '8px',
718
718
  }}
719
719
  >
720
- Add to cart
720
+ {settings.cart?.buttonText || 'Add to cart'}
721
721
  </div>
722
722
  <div style={{ width: '16px' }}>
723
723
  <Icon
@@ -456,7 +456,7 @@ function ProductDetailView(props: Props) {
456
456
  </div>
457
457
  )}
458
458
  </div>
459
- {settings.cart && (
459
+ {settings.cart?.enabled && (
460
460
  <div
461
461
  className="bg-[#2B2C46] rounded mt-2 flex justify-between items-center h-8 desktop:h-12 w-full btn-detail-item"
462
462
  style={{
@@ -472,7 +472,7 @@ function ProductDetailView(props: Props) {
472
472
  }}
473
473
  >
474
474
  <div className="text-f16 fw-600 max-line-1 text-white pr-1.5">
475
- Add to cart
475
+ {settings.cart?.buttonText || 'Add to cart'}
476
476
  </div>
477
477
  <div style={{ width: '16px' }}>
478
478
  <Icon
@@ -63,6 +63,7 @@ function ProductList({ sendFeedBackAction }: Props): JSX.Element {
63
63
  }, [productsFromFindApi, query, requestImages]);
64
64
 
65
65
  const postFilteredProducts = useMemo(() => {
66
+ // Find-API post-filters re-query when Algolia is off; do not also client-filter.
66
67
  if (!window.settings.algolia?.enabled && window.settings.postFilterOption) {
67
68
  return textFilteredProducts;
68
69
  }
@@ -114,6 +115,7 @@ function ProductList({ sendFeedBackAction }: Props): JSX.Element {
114
115
  );
115
116
  }
116
117
 
118
+ // Product source follows search backend (Algolia vs Find API), not postFilterOption.
117
119
  if (!isAlgoliaEnabled) {
118
120
  return baseFindApiProducts;
119
121
  }
@@ -0,0 +1,50 @@
1
+ const ScopeMissing = () => {
2
+ const { supportEmail } = window.settings.auth0;
3
+
4
+ return (
5
+ <div className="pl-8 desktop:pl-16 pt-6 desktop:pt-8 flex flex-col gap-4 bg-white h-full">
6
+ <div
7
+ style={{
8
+ fontSize: '20px',
9
+ fontWeight: 'bold',
10
+ color: '#2B2C46',
11
+ }}
12
+ >
13
+ Access Denied
14
+ </div>
15
+
16
+ <div
17
+ style={{
18
+ width: '200px',
19
+ height: '4px',
20
+ borderRadius: '4px',
21
+ }}
22
+ className="bg-red-500"
23
+ />
24
+
25
+ <p style={{ color: '#2B2C46', fontSize: '13px' }}>
26
+ You do not have access to the {window.settings.visualSearchFilterKey}.
27
+ If you believe this is an error, please contact support.
28
+ </p>
29
+ <a
30
+ className="contact-support"
31
+ style={{
32
+ backgroundColor: '#2B2C46',
33
+ padding: '8px 16px 8px 16px',
34
+ color: '#fff',
35
+ border: 'none',
36
+ cursor: 'pointer',
37
+ fontSize: '14px',
38
+ width: 'fit-content',
39
+ }}
40
+ href={`mailto:${
41
+ supportEmail || 'support@nyris.io'
42
+ }?subject=Access Denied&body=`}
43
+ >
44
+ Contact Support
45
+ </a>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default ScopeMissing;
@@ -18,6 +18,9 @@ export default function SidePanel({ className }: { className?: string }) {
18
18
  const specificationFilter = useRequestStore(
19
19
  state => state.specificationFilter,
20
20
  );
21
+ const postFilterSelections = useRequestStore(
22
+ state => state.postFilterSelections,
23
+ );
21
24
 
22
25
  const findApiProducts = useResultStore(state => state.productsFromFindApi);
23
26
 
@@ -45,13 +48,20 @@ export default function SidePanel({ className }: { className?: string }) {
45
48
 
46
49
  const effectiveGroundingResults = useEffectiveGroundingResults();
47
50
 
51
+ const hasPostFilterSelections = Object.values(
52
+ postFilterSelections || {},
53
+ ).some(values => values?.length > 0);
54
+
48
55
  if (!showPostFilter && requestImages.length === 0) {
49
56
  return <></>;
50
57
  }
51
58
 
59
+ // Keep the panel when post-filters are applied so the user can unselect
60
+ // after a filtered find returns zero results.
52
61
  if (
53
62
  requestImages.length === 0 &&
54
63
  findApiProducts.length === 0 &&
64
+ !hasPostFilterSelections &&
55
65
  !window.settings.algolia.enabled
56
66
  ) {
57
67
  return <></>;
@@ -1,5 +1,4 @@
1
1
  import React, { useMemo } from 'react';
2
- import ItemSpecification from './ItemSpecification';
3
2
  import { Icon } from '@nyris/nyris-react-components';
4
3
  import useRequestStore from 'stores/request/requestStore';
5
4
  import useResultStore from 'stores/result/resultStore';
@@ -19,8 +18,12 @@ function SmartFilter({
19
18
  const isSmartFiltersEnabled = !!settings.useSmartFilters;
20
19
  const smartFilters = useResultStore(state => state.smartFilters);
21
20
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
22
- const productsFromFindApi = useResultStore(state => state.productsFromFindApi);
23
- const productsFromAlgolia = useResultStore(state => state.productsFromAlgolia);
21
+ const productsFromFindApi = useResultStore(
22
+ state => state.productsFromFindApi,
23
+ );
24
+ const productsFromAlgolia = useResultStore(
25
+ state => state.productsFromAlgolia,
26
+ );
24
27
  const firstSearchResults = useResultStore(state => state.firstSearchResults);
25
28
  const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
26
29
  const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
@@ -194,7 +197,10 @@ function SmartFilter({
194
197
  return;
195
198
  }
196
199
 
197
- if (rerankedSkus.length === 0 || !applySkuOrderingToCurrentResults(rerankedSkus)) {
200
+ if (
201
+ rerankedSkus.length === 0 ||
202
+ !applySkuOrderingToCurrentResults(rerankedSkus)
203
+ ) {
198
204
  setSmartFilters({
199
205
  ...smartFilters,
200
206
  selectedFilterKey: filter.key,
@@ -214,7 +220,9 @@ function SmartFilter({
214
220
  errorMessage: null,
215
221
  });
216
222
  } catch (error: any) {
217
- const timeout = String(error?.message || '').includes('SMART_FILTER_TIMEOUT');
223
+ const timeout = String(error?.message || '').includes(
224
+ 'SMART_FILTER_TIMEOUT',
225
+ );
218
226
  setSmartFilters({
219
227
  ...smartFilters,
220
228
  selectedFilterKey: filter.key,
@@ -241,25 +249,7 @@ function SmartFilter({
241
249
  </div>
242
250
  <Icon name="smart_filter_indication" className="mt-1" />
243
251
  </div>
244
- <div className={`flex flex-wrap mx-4 gap-2 ${imageAnalysis?.specification ? 'mt-4' : ''}`}>
245
- {Object.keys(imageAnalysis?.specification || {})
246
- .filter(key => key !== 'is_nameplate' && key !== 'prefilter_value')
247
- .map(key => {
248
- const value = imageAnalysis?.specification[key];
249
- if (!value) {
250
- return null;
251
- }
252
- return (
253
- <ItemSpecification
254
- key={key}
255
- attr={key}
256
- value={value}
257
- specificationFilter={specificationFilter}
258
- imageAnalysis={imageAnalysis}
259
- />
260
- );
261
- })}
262
- </div>
252
+
263
253
  {smartFilters.status === 'loading' && (
264
254
  <div className="ml-[30px] mt-3 text-xs text-[#3E36DC]">
265
255
  Loading smart filters...
@@ -270,11 +260,12 @@ function SmartFilter({
270
260
  {smartFilters.errorMessage || 'Could not generate smart filters.'}
271
261
  </div>
272
262
  )}
273
- {smartFilters.status === 'success' && smartFilters.filters.length === 0 && (
274
- <div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
275
- No smart filters available for this image.
276
- </div>
277
- )}
263
+ {smartFilters.status === 'success' &&
264
+ smartFilters.filters.length === 0 && (
265
+ <div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
266
+ No smart filters available for this image.
267
+ </div>
268
+ )}
278
269
  <div
279
270
  className={
280
271
  variant === 'results'
@@ -298,47 +289,46 @@ function SmartFilter({
298
289
  ? 'flex flex-row flex-nowrap gap-2 pb-1'
299
290
  : 'flex flex-row flex-wrap gap-2 pb-1'
300
291
  }
301
- style={variant === 'results' ? { width: 'max-content' } : { width: 'auto' }}
292
+ style={
293
+ variant === 'results' ? { width: 'max-content' } : { width: 'auto' }
294
+ }
302
295
  >
303
- {smartFilters.filters.map((filter, index) => {
304
- const isSelected =
305
- smartFilters.selectedFilterKey === filter.key &&
306
- smartFilters.selectedFilterValue === filter.value;
307
- const hasSelection =
308
- !!smartFilters.selectedFilterKey &&
309
- !!smartFilters.selectedFilterValue;
310
- const isRerankLoading = smartFilters.rerankStatus === 'loading';
311
- const isDisabled = !isSelected && (hasSelection || isRerankLoading);
312
- return (
313
- <button
314
- key={`${filter.key}-${filter.value}-${index}`}
315
- type="button"
316
- disabled={isDisabled}
317
- aria-pressed={isSelected}
318
- style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
319
- className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
320
- isSelected
321
- ? 'bg-[#545987] text-white border-[#545987]'
322
- : 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
323
- } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
324
- onClick={() => {
325
- if (isRerankLoading) return;
326
- void onFilterClick({
327
- key: filter.key,
328
- value: filter.value,
329
- });
330
- }}
331
- >
332
- <span>{filter.value}</span>
333
- {isSelected && (
334
- <Icon
335
- name="close"
336
- className="w-3 h-3 text-white"
337
- />
338
- )}
339
- </button>
340
- );
341
- })}
296
+ {smartFilters.filters.map((filter, index) => {
297
+ const isSelected =
298
+ smartFilters.selectedFilterKey === filter.key &&
299
+ smartFilters.selectedFilterValue === filter.value;
300
+ const hasSelection =
301
+ !!smartFilters.selectedFilterKey &&
302
+ !!smartFilters.selectedFilterValue;
303
+ const isRerankLoading = smartFilters.rerankStatus === 'loading';
304
+ const isDisabled = !isSelected && (hasSelection || isRerankLoading);
305
+ return (
306
+ <button
307
+ key={`${filter.key}-${filter.value}-${index}`}
308
+ type="button"
309
+ disabled={isDisabled}
310
+ aria-pressed={isSelected}
311
+ style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
312
+ className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
313
+ isSelected
314
+ ? 'bg-[#545987] text-white border-[#545987]'
315
+ : 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
316
+ } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
317
+ onClick={() => {
318
+ if (isRerankLoading) return;
319
+ void onFilterClick({
320
+ key: filter.key,
321
+ value: filter.value,
322
+ });
323
+ }}
324
+ >
325
+ <span>{filter.value}</span>
326
+ {isSelected && (
327
+ <Icon name="close" className="w-3 h-3 text-white" />
328
+ )}
329
+ </button>
330
+ );
331
+ })}
342
332
  </div>
343
333
  </div>
344
334
  </div>
@@ -65,13 +65,13 @@ function TextSearch({
65
65
 
66
66
  const showPreFilter = useMemo(() => {
67
67
  if (settings.shouldUseUserMetadata && user) {
68
- if (user['/user_metadata'].value) {
68
+ if (user['/user_metadata']?.value) {
69
69
  setMetaFilter(user['/user_metadata'].value);
70
70
  }
71
71
  }
72
72
 
73
73
  if (settings.shouldUseUserMetadata && user) {
74
- if (settings.preFilterOption && !user['/user_metadata'].value) {
74
+ if (settings.preFilterOption && !user['/user_metadata']?.value) {
75
75
  return true;
76
76
  }
77
77
  return false;
@@ -103,7 +103,7 @@ function TextSearch({
103
103
  imageRegion: regions[0],
104
104
  text: value,
105
105
  settings,
106
- showFeedback: false,
106
+ showFeedback: true,
107
107
  newSearch: false,
108
108
  });
109
109
  }
@@ -204,7 +204,9 @@ function TextSearch({
204
204
  !isEmpty(preFilter)
205
205
  ? 'fill-theme-primary'
206
206
  : 'fill-[#2B2C46]',
207
- `desktop:fill-[#3B3E5F]`,
207
+ !isEmpty(preFilter)
208
+ ? 'desktop:fill-white'
209
+ : 'desktop:fill-[#2B2C46]',
208
210
  )}
209
211
  />
210
212
  </div>
@@ -20,8 +20,12 @@ import { useGroundedSearch } from './useGroundedSearch';
20
20
  import { generateSmartFilters } from '../services/vizo';
21
21
  import { cropImage } from 'utils/cropImageToBase64';
22
22
  import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
23
+ import { derivePostFilterFacetsFromResults } from 'utils/derivePostFilterFacets';
24
+ import { useAuth0 } from '@auth0/auth0-react';
23
25
 
24
26
  export const useImageSearch = () => {
27
+ const { user } = useAuth0();
28
+
25
29
  const setRegions = useRequestStore(state => state.setRegions);
26
30
  const query = useRequestStore(state => state.query);
27
31
 
@@ -69,7 +73,9 @@ export const useImageSearch = () => {
69
73
  state => state.setGroundingFilterResult,
70
74
  );
71
75
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
72
- const setPostFilterFacets = useResultStore(state => state.setPostFilterFacets);
76
+ const setPostFilterFacets = useResultStore(
77
+ state => state.setPostFilterFacets,
78
+ );
73
79
  const setInitialFindSearchSnapshot = useResultStore(
74
80
  state => state.setInitialFindSearchSnapshot,
75
81
  );
@@ -208,6 +214,7 @@ export const useImageSearch = () => {
208
214
  ];
209
215
 
210
216
  let smartFilterPromise: Promise<void> | null = null;
217
+
211
218
  if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
212
219
  setSmartFilters({
213
220
  status: 'loading',
@@ -287,17 +294,20 @@ export const useImageSearch = () => {
287
294
  });
288
295
  }
289
296
 
297
+ const usesFindApiPostFilters =
298
+ !settings.algolia?.enabled && settings.postFilterOption;
299
+ // Crop/region finds replace results without sending postFilterSelections;
300
+ // clear stale selections so checkboxes match the new unfiltered grid.
290
301
  const shouldResetPostFilters =
291
302
  clearPostFilter ||
292
- (!settings.algolia?.enabled &&
293
- !requestImage &&
294
- settings.postFilterOption);
303
+ (Boolean(imageRegion) && usesFindApiPostFilters) ||
304
+ (!requestImage && settings.postFilterOption);
295
305
 
296
306
  if (shouldResetPostFilters) {
297
307
  invalidatePostFilterRequests();
298
308
  refine();
299
309
  clearPostFilterSelections();
300
- if (clearPostFilter) {
310
+ if (clearPostFilter || !requestImage) {
301
311
  setPostFilterFacets([]);
302
312
  setInitialFindSearchSnapshot(null);
303
313
  }
@@ -324,16 +334,40 @@ export const useImageSearch = () => {
324
334
  ? query
325
335
  : text
326
336
  : undefined,
337
+ scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
327
338
  });
328
339
  useResultStore.getState().resetGrounding();
329
340
 
330
341
  setFindApiProducts(res?.results);
331
- const initialFacets = res?.facets || [];
332
- setPostFilterFacets(initialFacets);
342
+
343
+ // Image: API facets if present, else derive from result.filters.
344
+ // Text: API facets only.
345
+ // Empty results with no facet payload may keep prior facet UI for unselect,
346
+ // but the search snapshot always updates to this response.
347
+ const isImageSearch = Boolean(requestImage);
348
+ const results = res?.results || [];
349
+ const apiFacets = Array.isArray(res?.facets) ? res.facets : [];
350
+ const nextFacets = isImageSearch
351
+ ? apiFacets.length > 0
352
+ ? apiFacets
353
+ : derivePostFilterFacetsFromResults(results)
354
+ : apiFacets;
355
+ const previousFacets = useResultStore.getState().postFilterFacets;
356
+ const shouldPreserveFacets =
357
+ results.length === 0 &&
358
+ nextFacets.length === 0 &&
359
+ Array.isArray(previousFacets) &&
360
+ previousFacets.length > 0;
361
+
362
+ // Facet UI may stay for recovery; snapshot always matches this search.
363
+ if (!shouldPreserveFacets) {
364
+ setPostFilterFacets(nextFacets);
365
+ }
333
366
  setInitialFindSearchSnapshot({
334
- results: res?.results || [],
335
- postFilterFacets: initialFacets,
367
+ results,
368
+ postFilterFacets: nextFacets,
336
369
  });
370
+
337
371
  setImageAnalysis(res?.image_analysis);
338
372
  setSessionId(res?.session);
339
373
  setRequestId(res?.id);
@@ -421,6 +455,7 @@ export const useImageSearch = () => {
421
455
  setSmartFilters,
422
456
  setPostFilterFacets,
423
457
  setInitialFindSearchSnapshot,
458
+ user,
424
459
  ],
425
460
  );
426
461
 
@@ -339,14 +339,14 @@ Evaluate if these results match the image. If they do (same type of product), ro
339
339
 
340
340
  // Enforce ≤5 rule: if initial results are ≤5, strip filter options, keep only text_search_prompt
341
341
  if (activeProducts.length <= 5 && activeProducts.length > 0) {
342
- mappedOptions = [
343
- {
344
- label: "Describe what you're looking for",
345
- action_type: 'text_search_prompt' as const,
346
- key: 'text_search',
347
- value: 'text_search',
348
- },
349
- ];
342
+ // mappedOptions = [
343
+ // {
344
+ // label: "Describe what you're looking for",
345
+ // action_type: 'text_search_prompt' as const,
346
+ // key: 'text_search',
347
+ // value: 'text_search',
348
+ // },
349
+ // ];
350
350
 
351
351
  if (
352
352
  window.settings.vizo?.findRelatedParts &&
package/src/index.tsx CHANGED
@@ -1,6 +1,10 @@
1
1
  import ReactDOM from 'react-dom/client';
2
2
  import './index.css';
3
3
  import App from './App';
4
+ import { bootstrapAuth0ApiKeyGuard } from 'utils/nyrisApiKey';
5
+
6
+ // Clear settings.js apiKey before any React tree can issue Nyris requests (Auth0 mode).
7
+ bootstrapAuth0ApiKeyGuard();
4
8
 
5
9
  const root = ReactDOM.createRoot(
6
10
  document.getElementById('root') as HTMLElement,