@nyris/nyris-webapp 0.3.93 → 0.3.94

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 (32) hide show
  1. package/build/asset-manifest.json +6 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +1 -0
  4. package/build/static/css/main.5fff4ca0.css +4 -0
  5. package/build/static/css/main.5fff4ca0.css.map +1 -0
  6. package/build/static/js/{main.c9bbbcb9.js → main.09201b03.js} +3 -3
  7. package/build/static/js/{main.c9bbbcb9.js.map → main.09201b03.js.map} +1 -1
  8. package/package.json +3 -3
  9. package/public/js/settings.example.js +1 -0
  10. package/src/components/ChatAssistant/useChatAssistantLogic.ts +1 -1
  11. package/src/components/CustomCameraDrawer.tsx +5 -5
  12. package/src/components/DragDropFile.tsx +4 -4
  13. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchModal.tsx +1 -1
  14. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +1 -1
  15. package/src/components/ImagePreview.tsx +4 -4
  16. package/src/components/ImageUpload.tsx +5 -5
  17. package/src/components/MobileCameraCTA.tsx +1 -1
  18. package/src/components/Rfq/RfqBanner.tsx +3 -4
  19. package/src/components/SidePanel.tsx +8 -12
  20. package/src/components/SmartFilter.tsx +70 -42
  21. package/src/hooks/useGoodResultsChat.ts +1 -1
  22. package/src/hooks/useImageSearch.ts +4 -8
  23. package/src/hooks/useResultEvaluator.ts +5 -4
  24. package/src/pages/Home.tsx +2 -2
  25. package/src/pages/Result.tsx +1 -4
  26. package/src/services/vizo.ts +0 -6
  27. package/src/styles/common.scss +1 -0
  28. package/src/styles/experienceVisualSearch.scss +2 -0
  29. package/src/types.ts +1 -0
  30. package/build/static/css/main.fed38059.css +0 -4
  31. package/build/static/css/main.fed38059.css.map +0 -1
  32. /package/build/static/js/{main.c9bbbcb9.js.LICENSE.txt → main.09201b03.js.LICENSE.txt} +0 -0
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nyris/nyris-webapp",
3
- "version": "0.3.93",
3
+ "version": "0.3.94",
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.93",
9
- "@nyris/nyris-react-components": "^0.3.93",
8
+ "@nyris/nyris-api": "^0.3.94",
9
+ "@nyris/nyris-react-components": "^0.3.94",
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",
@@ -110,6 +110,7 @@ var settings = {
110
110
  geoLocation: false,
111
111
  geoLocationMessage: '',
112
112
  showImageDetails: false,
113
+ supportButtonLabel: 'Request a Quote',
113
114
  rfq: {
114
115
  enabled: '',
115
116
  emailTemplateId: '',
@@ -682,7 +682,7 @@ Each result includes a unique SKU in brackets. Use the result thumbnails and met
682
682
 
683
683
  const tableSuggestions = lookupRelatedParts(
684
684
  table,
685
- imageAnalysis.specification.product_category,
685
+ imageAnalysis?.specification?.product_category,
686
686
  );
687
687
 
688
688
  if (tableSuggestions && tableSuggestions.length > 0) {
@@ -95,7 +95,7 @@ function CustomCamera(props: Props) {
95
95
  showFeedback: true,
96
96
  }).then(singleImageResp => {
97
97
  const specificationPrefilter =
98
- singleImageResp.image_analysis?.specification?.prefilter_value || null;
98
+ singleImageResp?.image_analysis?.specification?.prefilter_value || null;
99
99
 
100
100
  const hasPrefilter = preFilterList.filter((filter: any) =>
101
101
  filter.values.includes(specificationPrefilter),
@@ -105,15 +105,15 @@ function CustomCamera(props: Props) {
105
105
  setShowNotMatchedError(false);
106
106
  if (hasPrefilter.length) {
107
107
  setSpecifications(
108
- clone(singleImageResp.image_analysis.specification),
108
+ clone(singleImageResp?.image_analysis?.specification),
109
109
  );
110
110
  setNameplateImage(image);
111
111
  setPreFilter({
112
- [singleImageResp.image_analysis?.specification?.prefilter_value]:
112
+ [singleImageResp?.image_analysis?.specification?.prefilter_value]:
113
113
  true,
114
114
  });
115
115
  setAlgoliaFilter(
116
- `${settings.alogoliaFilterField}:'${singleImageResp.image_analysis?.specification?.prefilter_value}'`,
116
+ `${settings.alogoliaFilterField}:'${singleImageResp?.image_analysis?.specification?.prefilter_value}'`,
117
117
  );
118
118
 
119
119
  setShowLoading(false);
@@ -133,7 +133,7 @@ function CustomCamera(props: Props) {
133
133
  if (!hasPrefilter.length && window.settings.preFilterOption) {
134
134
  setSpecifications(
135
135
  clone({
136
- ...singleImageResp.image_analysis.specification,
136
+ ...singleImageResp?.image_analysis?.specification,
137
137
  specificationPrefilter,
138
138
  }),
139
139
  );
@@ -70,15 +70,15 @@ function DragDropFile(props: Props) {
70
70
  setShowNotMatchedError(false);
71
71
  if (hasPrefilter.length) {
72
72
  setSpecifications(
73
- clone(singleImageResp.image_analysis.specification),
73
+ clone(singleImageResp?.image_analysis?.specification),
74
74
  );
75
75
  setNameplateImage(file);
76
76
  setPreFilter({
77
- [singleImageResp.image_analysis?.specification?.prefilter_value]:
77
+ [singleImageResp?.image_analysis?.specification?.prefilter_value]:
78
78
  true,
79
79
  });
80
80
  setAlgoliaFilter(
81
- `${window.settings.alogoliaFilterField}:'${singleImageResp.image_analysis?.specification?.prefilter_value}'`,
81
+ `${window.settings.alogoliaFilterField}:'${singleImageResp?.image_analysis?.specification?.prefilter_value}'`,
82
82
  );
83
83
 
84
84
  setShowLoading(false);
@@ -100,7 +100,7 @@ function DragDropFile(props: Props) {
100
100
  if (!hasPrefilter.length && window.settings.preFilterOption) {
101
101
  setSpecifications(
102
102
  clone({
103
- ...singleImageResp.image_analysis.specification,
103
+ ...singleImageResp?.image_analysis?.specification,
104
104
  specificationPrefilter,
105
105
  }),
106
106
  );
@@ -23,7 +23,7 @@ function ExperienceVisualSearchModal(props: Props) {
23
23
  return (
24
24
  <Dialog open={openModal} onOpenChange={handleClose}>
25
25
  <DialogContent
26
- className="bg-white p-0 max-w-[1500px] w-fit max-h-[95vh] h-fit m-auto overflow-y-hidden rounded-lg"
26
+ className="bg-white p-0 max-w-[1500px] w-fit max-h-[95vh] m-auto overflow-y-hidden rounded-lg"
27
27
  closeButton={false}
28
28
  >
29
29
  <DialogTitle className="h-0 w-0 hidden">
@@ -46,7 +46,7 @@ function ExperienceVisualSearchTrigger({
46
46
 
47
47
  return (
48
48
  <>
49
- <div className="flex flex-col items-center">
49
+ <div className={`flex flex-col items-center ${window.settings.experienceVisualSearch ? 'mb-[80px]' : ''}`}>
50
50
  <div
51
51
  className={`group bg-[#3E36DC] w-10 hover:w-[215px] hover:gap-2 ${
52
52
  expand ? 'w-[215px] gap-2' : ''
@@ -116,13 +116,13 @@ function ImagePreviewComponent({
116
116
  setRequestImages([]);
117
117
  setShowNotMatchedError(false);
118
118
  if (hasPrefilter.length) {
119
- setSpecifications(clone(res.image_analysis.specification));
119
+ setSpecifications(clone(res?.image_analysis?.specification));
120
120
  setNameplateImage(image);
121
121
  setPreFilter({
122
- [res.image_analysis?.specification?.prefilter_value]: true,
122
+ [res?.image_analysis?.specification?.prefilter_value]: true,
123
123
  });
124
124
  setAlgoliaFilter(
125
- `${settings.alogoliaFilterField}:'${res.image_analysis?.specification?.prefilter_value}'`,
125
+ `${settings.alogoliaFilterField}:'${res?.image_analysis?.specification?.prefilter_value}'`,
126
126
  );
127
127
 
128
128
  setShowLoading(false);
@@ -144,7 +144,7 @@ function ImagePreviewComponent({
144
144
  if (!hasPrefilter.length && window.settings.preFilterOption) {
145
145
  setSpecifications(
146
146
  clone({
147
- ...res.image_analysis.specification,
147
+ ...res?.image_analysis?.specification,
148
148
  specificationPrefilter,
149
149
  }),
150
150
  );
@@ -103,7 +103,7 @@ function ImageUpload({
103
103
  clearPostFilter: true,
104
104
  }).then(singleImageResp => {
105
105
  const specificationPrefilter =
106
- singleImageResp.image_analysis?.specification?.prefilter_value || null;
106
+ singleImageResp?.image_analysis?.specification?.prefilter_value || null;
107
107
  const hasPrefilter = preFilterList.filter((filter: any) =>
108
108
  filter.values.includes(specificationPrefilter),
109
109
  );
@@ -112,15 +112,15 @@ function ImageUpload({
112
112
  setShowNotMatchedError(false);
113
113
  if (hasPrefilter.length) {
114
114
  setSpecifications(
115
- clone(singleImageResp.image_analysis.specification),
115
+ clone(singleImageResp?.image_analysis?.specification),
116
116
  );
117
117
  setNameplateImage(files[0]);
118
118
  setPreFilter({
119
- [singleImageResp.image_analysis?.specification?.prefilter_value]:
119
+ [singleImageResp?.image_analysis?.specification?.prefilter_value]:
120
120
  true,
121
121
  });
122
122
  setAlgoliaFilter(
123
- `${settings.alogoliaFilterField}:'${singleImageResp.image_analysis?.specification?.prefilter_value}'`,
123
+ `${settings.alogoliaFilterField}:'${singleImageResp?.image_analysis?.specification?.prefilter_value}'`,
124
124
  );
125
125
 
126
126
  setShowLoading(false);
@@ -142,7 +142,7 @@ function ImageUpload({
142
142
  if (!hasPrefilter.length && showPreFilter) {
143
143
  setSpecifications(
144
144
  clone({
145
- ...singleImageResp.image_analysis.specification,
145
+ ...singleImageResp?.image_analysis?.specification,
146
146
  specificationPrefilter,
147
147
  }),
148
148
  );
@@ -4,7 +4,7 @@ import '../styles/mobileCameraCTA.scss';
4
4
 
5
5
  function MobileCameraCTA({ setOpenModalCamera }: { setOpenModalCamera: any }) {
6
6
  return (
7
- <div className="take-photo">
7
+ <div className={`take-photo ${window.settings.experienceVisualSearch ? 'mt-[180px]' : ''}`}>
8
8
  <div
9
9
  className="take-photo-wrapper"
10
10
  >
@@ -29,7 +29,6 @@ function RfqBanner({
29
29
  <div
30
30
  style={{
31
31
  padding: '0px 16px 0px 16px',
32
- backgroundColor: '#F6F3F1',
33
32
  width: '100%',
34
33
  alignSelf: 'end',
35
34
  height: '248px',
@@ -72,9 +71,9 @@ function RfqBanner({
72
71
  fontWeight: 'bold',
73
72
  }}
74
73
  >
75
- <p className="desktop:hidden">{`${t('No matches found')}?`}</p>
74
+ <p className="desktop:hidden">{`${t('No matches found')}`}?</p>
76
75
  <p className="hidden desktop:block">
77
- `${t('No matches found for your request')}?`
76
+ {`${t('No matches found for your request')}`}?
78
77
  </p>
79
78
  </div>
80
79
  <div
@@ -112,7 +111,7 @@ function RfqBanner({
112
111
  setIsRfqModalOpen(true);
113
112
  }}
114
113
  >
115
- Request a Quote
114
+ {window.settings.supportButtonLabel}
116
115
  </button>
117
116
  </div>
118
117
  </div>
@@ -24,7 +24,6 @@ export default function SidePanel({ className }: { className?: string }) {
24
24
  const showSidePanel = useUiStore(state => state.showSidePanel);
25
25
 
26
26
  const imageAnalysis = useResultStore(state => state.imageAnalysis);
27
- const smartFilters = useResultStore(state => state.smartFilters);
28
27
 
29
28
  const showPostFilter = window.settings?.postFilterOption;
30
29
 
@@ -105,17 +104,6 @@ export default function SidePanel({ className }: { className?: string }) {
105
104
  {requestImages[0] && <ImagePreview />}
106
105
  </div>
107
106
  <div className="pb-4 flex flex-col gap-6">
108
- {isSmartFiltersEnabled &&
109
- (imageAnalysis?.imageDescription ||
110
- Object.keys(imageAnalysis?.specification || {}).length > 0 ||
111
- smartFilters.status !== 'idle') && (
112
- <SmartFilter
113
- imageAnalysis={imageAnalysis}
114
- specificationFilter={specificationFilter}
115
- variant="sidepanel"
116
- />
117
- )}
118
-
119
107
  {window.settings.showImageDetails &&
120
108
  (imageAnalysis?.imageDescription ||
121
109
  Object.keys(imageAnalysis?.specification || {}).length > 0) && (
@@ -165,6 +153,14 @@ export default function SidePanel({ className }: { className?: string }) {
165
153
  </div>
166
154
  )}
167
155
 
156
+ {isSmartFiltersEnabled && (
157
+ <SmartFilter
158
+ imageAnalysis={imageAnalysis}
159
+ specificationFilter={specificationFilter}
160
+ variant="sidepanel"
161
+ />
162
+ )}
163
+
168
164
  {(isGoogleGroundingLoading ||
169
165
  googleGroundingResponse ||
170
166
  groundingError) && (
@@ -3,6 +3,7 @@ import ItemSpecification from './ItemSpecification';
3
3
  import { Icon } from '@nyris/nyris-react-components';
4
4
  import useRequestStore from 'stores/request/requestStore';
5
5
  import useResultStore from 'stores/result/resultStore';
6
+ import useUiStore from 'stores/ui/uiStore';
6
7
  import { rerankSmartFilters, SmartFilterRerankItem } from 'services/vizo';
7
8
 
8
9
  function SmartFilter({
@@ -25,6 +26,7 @@ function SmartFilter({
25
26
  const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
26
27
  const requestImages = useRequestStore(state => state.requestImages);
27
28
  const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
29
+ const setIsFindApiLoading = useUiStore(state => state.setIsFindApiLoading);
28
30
  const isAlgoliaEnabled = !!window.settings?.algolia?.enabled;
29
31
 
30
32
  const activeProducts =
@@ -32,10 +34,20 @@ function SmartFilter({
32
34
  ? productsFromAlgolia
33
35
  : productsFromFindApi;
34
36
 
35
- const rerankItems: SmartFilterRerankItem[] = useMemo(() => {
36
- return (activeProducts || [])
37
- .slice(0, 50)
38
- .map((product: any): SmartFilterRerankItem | null => {
37
+ // Always rerank against the original, unfiltered result set. Reranking
38
+ // mutates `productsFromFindApi`/`productsFromAlgolia`, so deriving items
39
+ // from those would shrink the payload on every subsequent rerank.
40
+ const originalResults = useMemo(
41
+ () =>
42
+ firstSearchResults && firstSearchResults.length > 0
43
+ ? firstSearchResults
44
+ : activeProducts || [],
45
+ [firstSearchResults, activeProducts],
46
+ );
47
+
48
+ const rerankItems: any[] = useMemo(() => {
49
+ return originalResults
50
+ .map((product: any): any | null => {
39
51
  const sku = product?.sku || product?.objectID;
40
52
  const imageUrl =
41
53
  product?.matches?.[0]?.url ||
@@ -49,31 +61,39 @@ function SmartFilter({
49
61
  }
50
62
 
51
63
  return {
52
- sku: String(sku),
64
+ ...product,
53
65
  imageUrl: String(imageUrl),
54
- metadata: {
55
- title: product?.title || product?.name || product?.Bezeichnung || '',
56
- description:
57
- product?.description ||
58
- product?.VK_Text_Englisch ||
59
- product?.VK_Text_Deutsch ||
60
- '',
61
- language: window.settings.language || 'en',
62
- sku: String(sku),
63
- score:
64
- typeof (product?.score ?? product?.similarity) === 'number'
65
- ? Number(product?.score ?? product?.similarity)
66
- : null,
67
- },
68
66
  };
69
67
  })
70
68
  .filter((item): item is SmartFilterRerankItem => item !== null);
71
- }, [activeProducts]);
69
+ }, [originalResults]);
70
+
71
+ const buildAlgoliaSkuFilter = (skus: string[]) => {
72
+ const nonEmptyFilter: string[] = ['sku:DOES_NOT_EXIST<score=1> '];
73
+ const rankedFilters = skus
74
+ .slice()
75
+ .reverse()
76
+ .map((sku, i) => `sku:'${sku}'<score=${i}> `);
77
+ return [...nonEmptyFilter, ...rankedFilters].join('OR ');
78
+ };
79
+
80
+ const restoreOriginalResults = () => {
81
+ if (originalResults.length === 0) {
82
+ return;
83
+ }
84
+ setFindApiProducts(originalResults);
85
+ if (isAlgoliaEnabled) {
86
+ setAlgoliaProducts(originalResults);
87
+ const skus = originalResults
88
+ .map((p: any) => String(p?.sku || p?.objectID || '').trim())
89
+ .filter(Boolean);
90
+ setAlgoliaFilter(buildAlgoliaSkuFilter(skus));
91
+ }
92
+ };
72
93
 
73
94
  const applySkuOrderingToCurrentResults = (orderedSkus: string[]) => {
74
- const sourceResults = activeProducts || [];
75
95
  const map = new Map<string, any>(
76
- sourceResults.map((item: any) => [
96
+ originalResults.map((item: any) => [
77
97
  String(item?.sku || item?.objectID || '').trim(),
78
98
  item,
79
99
  ]),
@@ -90,12 +110,7 @@ function SmartFilter({
90
110
  setFindApiProducts(reordered);
91
111
  if (isAlgoliaEnabled) {
92
112
  setAlgoliaProducts(reordered);
93
- const nonEmptyFilter: string[] = ['sku:DOES_NOT_EXIST<score=1> '];
94
- const rankedFilters = orderedSkus
95
- .slice()
96
- .reverse()
97
- .map((sku, i) => `sku:'${sku}'<score=${i}> `);
98
- setAlgoliaFilter([...nonEmptyFilter, ...rankedFilters].join('OR '));
113
+ setAlgoliaFilter(buildAlgoliaSkuFilter(orderedSkus));
99
114
  }
100
115
  return true;
101
116
  };
@@ -112,12 +127,11 @@ function SmartFilter({
112
127
  ...smartFilters,
113
128
  selectedFilterKey: null,
114
129
  selectedFilterValue: null,
130
+ rerankedSkus: [],
115
131
  rerankStatus: 'idle',
116
132
  errorMessage: null,
117
133
  });
118
- if (firstSearchResults.length > 0) {
119
- setFindApiProducts(firstSearchResults);
120
- }
134
+ restoreOriginalResults();
121
135
  return;
122
136
  }
123
137
 
@@ -139,6 +153,7 @@ function SmartFilter({
139
153
  rerankStatus: 'loading',
140
154
  errorMessage: null,
141
155
  });
156
+ setIsFindApiLoading(true);
142
157
 
143
158
  try {
144
159
  const rerankResponse = await rerankSmartFilters({
@@ -209,6 +224,8 @@ function SmartFilter({
209
224
  ? 'Smart filters are unavailable right now.'
210
225
  : 'Could not rerank results.',
211
226
  });
227
+ } finally {
228
+ setIsFindApiLoading(false);
212
229
  }
213
230
  };
214
231
 
@@ -224,7 +241,7 @@ function SmartFilter({
224
241
  </div>
225
242
  <Icon name="smart_filter_indication" className="mt-1" />
226
243
  </div>
227
- <div className="flex flex-wrap mx-4 gap-2 mt-4">
244
+ <div className={`flex flex-wrap mx-4 gap-2 ${imageAnalysis?.specification ? 'mt-4' : ''}`}>
228
245
  {Object.keys(imageAnalysis?.specification || {})
229
246
  .filter(key => key !== 'is_nameplate' && key !== 'prefilter_value')
230
247
  .map(key => {
@@ -244,23 +261,20 @@ function SmartFilter({
244
261
  })}
245
262
  </div>
246
263
  {smartFilters.status === 'loading' && (
247
- <div className="mx-4 mt-3 text-xs text-[#3E36DC]">
264
+ <div className="ml-[30px] mt-3 text-xs text-[#3E36DC]">
248
265
  Loading smart filters...
249
266
  </div>
250
267
  )}
251
268
  {smartFilters.status === 'error' && (
252
- <div className="mx-4 mt-3 text-xs text-[#D14343]">
269
+ <div className="ml-[30px] mt-3 text-xs text-[#D14343]">
253
270
  {smartFilters.errorMessage || 'Could not generate smart filters.'}
254
271
  </div>
255
272
  )}
256
273
  {smartFilters.status === 'success' && smartFilters.filters.length === 0 && (
257
- <div className="mx-4 mt-3 text-xs text-[#767A9F]">
274
+ <div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
258
275
  No smart filters available for this image.
259
276
  </div>
260
277
  )}
261
- {smartFilters.rerankStatus === 'loading' && (
262
- <div className="mx-4 mt-1 text-xs text-[#3E36DC]">Applying rerank...</div>
263
- )}
264
278
  <div
265
279
  className={
266
280
  variant === 'results'
@@ -290,24 +304,38 @@ function SmartFilter({
290
304
  const isSelected =
291
305
  smartFilters.selectedFilterKey === filter.key &&
292
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);
293
312
  return (
294
313
  <button
295
314
  key={`${filter.key}-${filter.value}-${index}`}
315
+ type="button"
316
+ disabled={isDisabled}
317
+ aria-pressed={isSelected}
296
318
  style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
297
- className={`px-3.5 py-2 rounded-lg border text-sm ${
319
+ className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
298
320
  isSelected
299
321
  ? 'bg-[#545987] text-white border-[#545987]'
300
322
  : 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
301
- }`}
323
+ } ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
302
324
  onClick={() => {
303
- if (smartFilters.rerankStatus === 'loading') return;
325
+ if (isRerankLoading) return;
304
326
  void onFilterClick({
305
327
  key: filter.key,
306
328
  value: filter.value,
307
329
  });
308
330
  }}
309
331
  >
310
- {filter.label}: {filter.value}
332
+ <span>{filter.value}</span>
333
+ {isSelected && (
334
+ <Icon
335
+ name="close"
336
+ className="w-3 h-3 text-white"
337
+ />
338
+ )}
311
339
  </button>
312
340
  );
313
341
  })}
@@ -592,7 +592,7 @@ Respond to the user's message with an appropriate action and follow-up options.`
592
592
  finalOptions = [
593
593
  ...finalOptions,
594
594
  {
595
- label: `Find parts related to ${imageAnalysis.specification.product_category || 'request image'}`,
595
+ label: `Find parts related to ${imageAnalysis?.specification?.product_category || 'request image'}`,
596
596
  action_type: 'find_related_parts' as const,
597
597
  key: 'find_related',
598
598
  value: 'find_related_parts',
@@ -18,9 +18,7 @@ import useResultEvaluator from './useResultEvaluator';
18
18
  import useConversationStore from 'stores/chat/conversationStore';
19
19
  import { useGroundedSearch } from './useGroundedSearch';
20
20
  import { generateSmartFilters } from '../services/vizo';
21
-
22
- const DEFAULT_SMART_FILTER_PROMPT =
23
- 'Extract at most 4 relevant product intent filters from the request image. Return strict JSON only.';
21
+ import { cropImage } from 'utils/cropImageToBase64';
24
22
 
25
23
  export const useImageSearch = () => {
26
24
  const setRegions = useRequestStore(state => state.setRegions);
@@ -195,7 +193,7 @@ export const useImageSearch = () => {
195
193
  ];
196
194
 
197
195
  let smartFilterPromise: Promise<void> | null = null;
198
- if (isSmartFiltersEnabled && requestImage && newSearch) {
196
+ if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
199
197
  setSmartFilters({
200
198
  status: 'loading',
201
199
  filters: [],
@@ -206,10 +204,8 @@ export const useImageSearch = () => {
206
204
  errorMessage: null,
207
205
  });
208
206
 
209
- smartFilterPromise = generateSmartFilters({
210
- image: requestImage,
211
- prompt: DEFAULT_SMART_FILTER_PROMPT,
212
- })
207
+ const smartFilterImage = cropImage(requestImage, region);
208
+ smartFilterPromise = generateSmartFilters({ image: smartFilterImage })
213
209
  .then(response => {
214
210
  const rawFilters = response?.filters;
215
211
  if (!rawFilters || typeof rawFilters !== 'object') {
@@ -219,7 +219,8 @@ ${resultsSummary
219
219
  .map(
220
220
  r =>
221
221
  `${r.rank}. ${r.title}${r.description ? ` - ${r.description}` : ''} (Score: ${r.score})${r.metadata ? `\n Metadata: ${JSON.stringify(r.metadata)}` : ''}`,
222
- ).join('\n')}
222
+ )
223
+ .join('\n')}
223
224
 
224
225
  COMMON ATTRIBUTES FOUND IN RESULTS (for filter suggestions):
225
226
  ${JSON.stringify(attributeFrequency, null, 2)}
@@ -316,7 +317,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
316
317
 
317
318
  const tableSuggestions = lookupRelatedParts(
318
319
  table,
319
- imageAnalysis.specification.product_category || '',
320
+ imageAnalysis?.specification?.product_category || '',
320
321
  );
321
322
  const hasTableSuggestions =
322
323
  !!tableSuggestions && tableSuggestions.length > 0;
@@ -329,7 +330,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
329
330
  !relatedPartsUsed
330
331
  ) {
331
332
  mappedOptions.push({
332
- label: `Find parts related to ${imageAnalysis.specification.product_category || 'request image'}`,
333
+ label: `Find parts related to ${imageAnalysis?.specification?.product_category || 'request image'}`,
333
334
  action_type: 'find_related_parts',
334
335
  key: 'find_related',
335
336
  value: 'find_related_parts',
@@ -353,7 +354,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
353
354
  !relatedPartsUsed
354
355
  ) {
355
356
  mappedOptions.push({
356
- label: `Find parts related to ${imageAnalysis.specification.product_category || 'request image'}`,
357
+ label: `Find parts related to ${imageAnalysis?.specification?.product_category || 'request image'}`,
357
358
  action_type: 'find_related_parts',
358
359
  key: 'find_related',
359
360
  value: 'find_related_parts',
@@ -84,7 +84,7 @@ function Home() {
84
84
  'items-center',
85
85
  'justify-center',
86
86
  'h-auto',
87
- 'mt-[calc(50vh-110px)]',
87
+ !window.settings.experienceVisualSearch ? 'mt-[calc(50vh-110px)]' : 'mt-[calc(50vh-156px)]',
88
88
  ])}
89
89
  >
90
90
  {geoLocation && <LocationInfoPopup />}
@@ -122,7 +122,7 @@ function Home() {
122
122
  </div>
123
123
  </div>
124
124
 
125
- <div className="flex flex-col desktop:hidden justify-center items-center w-full h-full ">
125
+ <div className="flex flex-col desktop:hidden justify-center items-center w-full h-full overflow-y-auto">
126
126
  <MobileCameraCTA setOpenModalCamera={setOpenModalCamera} />
127
127
  <div className="box-screenshot-camera">
128
128
  <CustomCamera
@@ -223,10 +223,7 @@ function Results() {
223
223
  >
224
224
  <div className="block desktop:hidden desktop:mb-0 mx-2">
225
225
  {requestImages[0] && <ImagePreview />}
226
- {isSmartFiltersEnabled &&
227
- (imageAnalysis?.imageDescription ||
228
- Object.keys(imageAnalysis?.specification || {}).length >
229
- 0) && (
226
+ {isSmartFiltersEnabled && (
230
227
  <div className="my-4">
231
228
  <SmartFilter
232
229
  imageAnalysis={imageAnalysis}
@@ -71,10 +71,6 @@ export interface SmartFilterRerankResponse {
71
71
 
72
72
  export interface SmartFilterGenerateParams {
73
73
  image: File | HTMLCanvasElement;
74
- prompt: string;
75
- model?: string;
76
- timeoutMs?: number;
77
- apiKey?: string;
78
74
  }
79
75
 
80
76
  export interface SmartFilterGenerateResponse {
@@ -135,7 +131,6 @@ export async function rerankSmartFilters({
135
131
 
136
132
  export async function generateSmartFilters({
137
133
  image,
138
- prompt,
139
134
  }: SmartFilterGenerateParams): Promise<SmartFilterGenerateResponse> {
140
135
  const settings = window.settings;
141
136
  const isSmartFiltersEnabled = !!settings.useSmartFilters;
@@ -154,7 +149,6 @@ export async function generateSmartFilters({
154
149
  } else {
155
150
  formData.append('image', image);
156
151
  }
157
- formData.append('prompt', prompt);
158
152
 
159
153
  try {
160
154
  const response = await axios.post<SmartFilterGenerateResponse>(
@@ -143,6 +143,7 @@
143
143
  justify-content: center;
144
144
  background-color: #16161680;
145
145
  &-body {
146
+ overflow-y: auto;
146
147
  padding: 32px;
147
148
  background: #F3F3F5;
148
149
  &.geolocation {
@@ -65,6 +65,7 @@ body.overflow-hidden {
65
65
  display: flex;
66
66
  flex-direction: row;
67
67
  flex-wrap: wrap;
68
+ justify-content: center;
68
69
  gap: 16px;
69
70
  width: 376px;
70
71
  .experience-visual-search-image-container {
@@ -166,6 +167,7 @@ body.overflow-hidden {
166
167
  padding: 32px 8px;
167
168
  &-content {
168
169
  &.experience-visual-search-images {
170
+ max-width: calc(100vw - 32px);
169
171
  width: 300px;
170
172
  gap: 8px;
171
173
  .experience-visual-search-image-container {
package/src/types.ts CHANGED
@@ -126,6 +126,7 @@ export interface AppSettings extends NyrisAPISettings {
126
126
  textSearchScoring?: Record<string, Record<string, number>>;
127
127
  theme: SearchSuiteSettings;
128
128
  visualSearchFilterKey?: string;
129
+ supportButtonLabel?: string;
129
130
  }
130
131
 
131
132
  export interface SearchSuiteSettings {