@nyris/nyris-webapp 0.3.89 → 0.3.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/build/_headers +2 -0
  2. package/build/asset-manifest.json +6 -6
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +17 -0
  5. package/build/static/css/main.734b52e1.css +4 -0
  6. package/build/static/css/main.734b52e1.css.map +1 -0
  7. package/build/static/js/main.f2255597.js +3 -0
  8. package/build/static/js/{main.ca8b95bc.js.map → main.f2255597.js.map} +1 -1
  9. package/package.json +3 -3
  10. package/public/_headers +2 -0
  11. package/public/index.html +1 -1
  12. package/public/js/settings.example.js +17 -0
  13. package/src/App.tsx +5 -3
  14. package/src/components/Cart.tsx +321 -0
  15. package/src/components/CustomCameraDrawer.tsx +4 -22
  16. package/src/components/DragDropFile.tsx +57 -38
  17. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +6 -1
  18. package/src/components/ExperienceVisualSearch/ExperienceVisualSearchTrigger.tsx +2 -2
  19. package/src/components/GroundingSpecs.tsx +47 -0
  20. package/src/components/Header.tsx +94 -93
  21. package/src/components/HitsPerPage.tsx +4 -2
  22. package/src/components/ImagePreview.tsx +64 -31
  23. package/src/components/ImageUpload.tsx +247 -0
  24. package/src/components/ItemSpecification.tsx +164 -0
  25. package/src/components/MatchNotificationBanner.tsx +165 -0
  26. package/src/components/PostFilter/PostFilter.tsx +22 -1
  27. package/src/components/PostFilter/PostFilterComponent.tsx +59 -26
  28. package/src/components/PostFilter/PostFilterFindApi.tsx +242 -0
  29. package/src/components/PoweredBy.tsx +16 -0
  30. package/src/components/PreFilter/PreFilter.tsx +77 -54
  31. package/src/components/Product/Product.tsx +186 -28
  32. package/src/components/Product/ProductAttribute.tsx +2 -2
  33. package/src/components/Product/ProductDetailView.tsx +123 -18
  34. package/src/components/Product/ProductDetailViewModal.tsx +3 -0
  35. package/src/components/Product/ProductList.tsx +78 -8
  36. package/src/components/SidePanel.tsx +212 -120
  37. package/src/components/TextSearch.tsx +82 -203
  38. package/src/components/Toaster.tsx +34 -15
  39. package/src/helpers/ToastHelper.ts +6 -2
  40. package/src/hooks/useCadSearch.ts +5 -0
  41. package/src/hooks/useImageSearch.ts +102 -13
  42. package/src/index.css +59 -0
  43. package/src/layouts/AppLayout.tsx +16 -14
  44. package/src/pages/Home.tsx +61 -13
  45. package/src/pages/Result.tsx +287 -295
  46. package/src/services/vizo.ts +161 -0
  47. package/src/stores/request/Misc/misc.initialstate.ts +1 -0
  48. package/src/stores/request/Misc/misc.slice.ts +1 -0
  49. package/src/stores/request/filter/filter.initialState.ts +3 -0
  50. package/src/stores/request/filter/filter.slice.ts +23 -0
  51. package/src/stores/result/prodcuts/products.initialState.ts +4 -0
  52. package/src/stores/result/prodcuts/products.slice.ts +15 -0
  53. package/src/stores/types.ts +27 -1
  54. package/src/stores/ui/loading/loading.initialState.ts +1 -0
  55. package/src/stores/ui/loading/loading.slice.ts +4 -0
  56. package/src/stores/ui/sidePanel/sidePanel.initialState.ts +5 -0
  57. package/src/stores/ui/sidePanel/sidePanel.slice.ts +11 -0
  58. package/src/stores/ui/uiStore.ts +4 -1
  59. package/src/styles/Cart.scss +210 -0
  60. package/src/styles/common.scss +10 -0
  61. package/src/translations.ts +4 -4
  62. package/src/types.ts +11 -3
  63. package/src/utils/prepareImageList.ts +6 -5
  64. package/src/utils/textSearchFilter.ts +203 -0
  65. package/tailwind.config.js +1 -0
  66. package/build/static/css/main.ba1c7479.css +0 -4
  67. package/build/static/css/main.ba1c7479.css.map +0 -1
  68. package/build/static/js/main.ca8b95bc.js +0 -3
  69. package/src/components/Footer.tsx +0 -21
  70. /package/build/static/js/{main.ca8b95bc.js.LICENSE.txt → main.f2255597.js.LICENSE.txt} +0 -0
@@ -1,21 +1,18 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import { isEmpty, debounce, clone } from 'lodash';
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import { isEmpty, debounce } from 'lodash';
3
3
  import { twMerge } from 'tailwind-merge';
4
4
  import { useAuth0 } from '@auth0/auth0-react';
5
5
  import { useLocation, useNavigate } from 'react-router';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { Icon } from '@nyris/nyris-react-components';
9
- import { isCadFile } from '@nyris/nyris-api';
10
9
 
11
- import { useCadSearch } from 'hooks/useCadSearch';
12
10
  import { useImageSearch } from 'hooks/useImageSearch';
13
11
  import PreFilterModal from './PreFilter/PreFilterModal';
14
12
  import useRequestStore from 'stores/request/requestStore';
15
13
  import Tooltip from './Tooltip/TooltipComponent';
16
- import UploadDisclaimer from './UploadDisclaimer';
17
- import { getFilters } from '../services/filter';
18
14
  import { useMediaQuery } from 'react-responsive';
15
+ import ImageUpload from './ImageUpload';
19
16
 
20
17
  function TextSearch({
21
18
  className,
@@ -35,8 +32,6 @@ function TextSearch({
35
32
  const location = useLocation();
36
33
  const navigate = useNavigate();
37
34
 
38
- const [resultFilter, setResultFilter] = useState<any>([]);
39
-
40
35
  const preFilter = useRequestStore(state => state.preFilter);
41
36
  const requestImages = useRequestStore(state => state.requestImages);
42
37
  const setQuery = useRequestStore(state => state.setQuery);
@@ -45,22 +40,16 @@ function TextSearch({
45
40
  const setValueInput = useRequestStore(state => state.setValueInput);
46
41
  const setMetaFilter = useRequestStore(state => state.setMetaFilter);
47
42
  const specifications = useRequestStore(state => state.specifications);
48
- const nameplateNotificationText = useRequestStore(state => state.nameplateNotificationText);
43
+ const nameplateNotificationText = useRequestStore(
44
+ state => state.nameplateNotificationText,
45
+ );
49
46
  const isMobile = useMediaQuery({ query: '(max-width: 776px)' });
50
47
 
51
48
  const regions = useRequestStore(state => state.regions);
52
- const setRequestImages = useRequestStore(state => state.setRequestImages);
53
49
  const setSpecifications = useRequestStore(state => state.setSpecifications);
54
- const setShowLoading = useRequestStore(state => state.setShowLoading);
55
- const setNameplateNotificationText = useRequestStore(state => state.setNameplateNotificationText);
56
- const setShowNotMatchedError = useRequestStore(state => state.setShowNotMatchedError);
57
- const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
58
- const setPreFilter = useRequestStore(state => state.setPreFilter);
59
- const setNameplateImage = useRequestStore(state => state.setNameplateImage);
60
50
 
61
51
  const [isOpenModalFilterDesktop, setToggleModalFilterDesktop] =
62
52
  useState<boolean>(false);
63
- const [showDisclaimer, setShowDisclaimer] = useState(false);
64
53
 
65
54
  const showPreFilter = useMemo(() => {
66
55
  if (settings.shouldUseUserMetadata && user) {
@@ -84,14 +73,6 @@ function TextSearch({
84
73
  user,
85
74
  ]);
86
75
 
87
- const showDisclaimerDisabled = useMemo(() => {
88
- const disclaimer = localStorage.getItem('upload-disclaimer-suite');
89
- if (requestImages.length === 0) return true;
90
- if (!disclaimer) return false;
91
- return disclaimer === 'dont-show';
92
- // eslint-disable-next-line react-hooks/exhaustive-deps
93
- }, [showDisclaimer, requestImages]);
94
-
95
76
  const visualSearch = useMemo(() => requestImages.length > 0, [requestImages]);
96
77
  const { singleImageSearch } = useImageSearch();
97
78
 
@@ -104,7 +85,7 @@ function TextSearch({
104
85
  return;
105
86
  }
106
87
 
107
- if (!window.settings?.algolia.enabled) {
88
+ if (!window.settings?.algolia.enabled && requestImages.length === 0) {
108
89
  singleImageSearch({
109
90
  image: requestImages[0],
110
91
  imageRegion: regions[0],
@@ -130,87 +111,6 @@ function TextSearch({
130
111
  }
131
112
  };
132
113
 
133
- const { cadSearch } = useCadSearch();
134
-
135
- const getPreFilters = async () => {
136
- getFilters(1000, settings)
137
- .then(res => {
138
- setResultFilter(res);
139
- })
140
- .catch((e: any) => {
141
- console.log('err getDataFilterDesktop', e);
142
- });
143
- }
144
-
145
- useEffect(() => {
146
- getPreFilters()
147
- }, []);
148
-
149
- const handleUpload = (files: File[]) => {
150
- setValueInput('');
151
- setQuery('');
152
-
153
- if (isCadFile(files[0])) {
154
- cadSearch({ file: files[0], settings, newSearch: true }).then(res => {});
155
-
156
- return;
157
- }
158
-
159
- setShowLoading(true);
160
-
161
- singleImageSearch({
162
- image: files[0],
163
- settings: window.settings,
164
- showFeedback: true,
165
- newSearch: true,
166
- }).then((singleImageResp) => {
167
- const specificationPrefilter = singleImageResp.image_analysis?.specification?.prefilter_value || null;
168
- const hasPrefilter = resultFilter.filter((filter: any) => filter.values.includes(specificationPrefilter));
169
- if (specificationPrefilter) {
170
- setRequestImages([]);
171
- setShowNotMatchedError(false);
172
- if (hasPrefilter.length) {
173
- setSpecifications(clone(singleImageResp.image_analysis.specification));
174
- setNameplateImage(files[0]);
175
- setPreFilter({[singleImageResp.image_analysis?.specification?.prefilter_value]: true});
176
- setAlgoliaFilter(`${settings.alogoliaFilterField}:'${singleImageResp.image_analysis?.specification?.prefilter_value}'`);
177
-
178
- setShowLoading(false);
179
- navigate('/result');
180
-
181
- setTimeout(() => {
182
- setNameplateNotificationText(t('We have successfully defined the search criteria', { prefilter_value: specificationPrefilter, preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }));
183
- }, 1000);
184
- setTimeout(() => {
185
- setNameplateNotificationText('');
186
- }, 6000);
187
- }
188
- if (!hasPrefilter.length && showPreFilter) {
189
- setSpecifications(clone({...singleImageResp.image_analysis.specification, prefilter_value: '', specificationPrefilter}));
190
- navigate('/result');
191
- setPreFilter({});
192
- setAlgoliaFilter('');
193
- setShowLoading(false);
194
- setShowNotMatchedError(true);
195
- setTimeout(() => {
196
- setNameplateNotificationText(t('Extracted details from the nameplate could not be matched', { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }));
197
- }, 1000);
198
- setTimeout(() => {
199
- setNameplateNotificationText('');
200
- }, 6000);
201
- }
202
- } else {
203
- if (specifications?.is_nameplate) {
204
- setSpecifications({...specifications, prefilter_value: '', specificationPrefilter: ''});
205
- } else {
206
- setSpecifications({...specifications, is_nameplate: false});
207
- }
208
- setShowLoading(false);
209
- navigate('/result');
210
- }
211
- });
212
- };
213
-
214
114
  return (
215
115
  <div
216
116
  className={twMerge(
@@ -307,10 +207,28 @@ function TextSearch({
307
207
  style={{
308
208
  position: 'fixed',
309
209
  backgroundColor:
310
- nameplateNotificationText !== t('Extracted details from the nameplate could not be matched', { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() })
311
- ? '#E4E3FF' : '#FFDBB3',
210
+ nameplateNotificationText !==
211
+ t(
212
+ 'Extracted details from the nameplate could not be matched',
213
+ {
214
+ preFilterTitle:
215
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
216
+ },
217
+ )
218
+ ? '#E4E3FF'
219
+ : '#FFDBB3',
312
220
  border: '1px solid',
313
- borderColor: nameplateNotificationText !== t('Extracted details from the nameplate could not be matched', { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }) ? '#3E36DC' : '#FF8800',
221
+ borderColor:
222
+ nameplateNotificationText !==
223
+ t(
224
+ 'Extracted details from the nameplate could not be matched',
225
+ {
226
+ preFilterTitle:
227
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
228
+ },
229
+ )
230
+ ? '#3E36DC'
231
+ : '#FF8800',
314
232
  fontSize: 13,
315
233
  borderRadius: 24,
316
234
  color: '#545987',
@@ -321,7 +239,7 @@ function TextSearch({
321
239
  justifyContent: 'center',
322
240
  marginLeft: 8,
323
241
  zIndex: 999999,
324
- top: !isMobile ? 60 : 'unset',
242
+ top: !isMobile ? 60 : 'unset',
325
243
  bottom: isMobile ? 76 : 'unset',
326
244
  maxWidth: 510,
327
245
  width: !isMobile ? 'unset' : '90%',
@@ -341,15 +259,31 @@ function TextSearch({
341
259
  borderRight: '7px solid transparent',
342
260
  borderTop: isMobile
343
261
  ? `7px solid ${
344
- nameplateNotificationText !== t('Extracted details from the nameplate could not be matched',
345
- { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }) ? '#3E36DC' : '#FF8800'
262
+ nameplateNotificationText !==
263
+ t(
264
+ 'Extracted details from the nameplate could not be matched',
265
+ {
266
+ preFilterTitle:
267
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
268
+ },
269
+ )
270
+ ? '#3E36DC'
271
+ : '#FF8800'
346
272
  }`
347
273
  : 'unset',
348
274
  borderBottom: !isMobile
349
275
  ? `7px solid ${
350
- nameplateNotificationText !== t('Extracted details from the nameplate could not be matched',
351
- { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }) ? '#3E36DC' : '#FF8800'
352
- }`
276
+ nameplateNotificationText !==
277
+ t(
278
+ 'Extracted details from the nameplate could not be matched',
279
+ {
280
+ preFilterTitle:
281
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
282
+ },
283
+ )
284
+ ? '#3E36DC'
285
+ : '#FF8800'
286
+ }`
353
287
  : 'unset',
354
288
  }}
355
289
  />
@@ -366,12 +300,32 @@ function TextSearch({
366
300
  borderLeft: '6px solid transparent',
367
301
  borderRight: '6px solid transparent',
368
302
  borderTop: isMobile
369
- ? `6px solid ${nameplateNotificationText !== t('Extracted details from the nameplate could not be matched',
370
- { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }) ? '#E4E3FF' : '#FFDBB3'}`
303
+ ? `6px solid ${
304
+ nameplateNotificationText !==
305
+ t(
306
+ 'Extracted details from the nameplate could not be matched',
307
+ {
308
+ preFilterTitle:
309
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
310
+ },
311
+ )
312
+ ? '#E4E3FF'
313
+ : '#FFDBB3'
314
+ }`
371
315
  : 'unset',
372
316
  borderBottom: !isMobile
373
- ? `6px solid ${nameplateNotificationText !== t('Extracted details from the nameplate could not be matched',
374
- { preFilterTitle: window.settings.preFilterTitle?.toLocaleLowerCase() }) ? '#E4E3FF' : '#FFDBB3'}`
317
+ ? `6px solid ${
318
+ nameplateNotificationText !==
319
+ t(
320
+ 'Extracted details from the nameplate could not be matched',
321
+ {
322
+ preFilterTitle:
323
+ window.settings.preFilterTitle?.toLocaleLowerCase(),
324
+ },
325
+ )
326
+ ? '#E4E3FF'
327
+ : '#FFDBB3'
328
+ }`
375
329
  : 'unset',
376
330
  }}
377
331
  />
@@ -422,7 +376,7 @@ function TextSearch({
422
376
  }}
423
377
  className="peer desktop:bg-gray-200 focus:bg-white pl-1.5 outline-none"
424
378
  placeholder={t('Search')}
425
- value={valueInput || query}
379
+ value={valueInput}
426
380
  onChange={onChangeText}
427
381
  ref={focusInp}
428
382
  />
@@ -449,7 +403,10 @@ function TextSearch({
449
403
  navigate('/result');
450
404
  setValueInput('');
451
405
  setQuery('');
452
- if (!window.settings?.algolia.enabled) {
406
+ if (
407
+ !window.settings?.algolia.enabled &&
408
+ requestImages.length === 0
409
+ ) {
453
410
  singleImageSearch({
454
411
  image: requestImages[0],
455
412
  imageRegion: regions[0],
@@ -471,95 +428,17 @@ function TextSearch({
471
428
  </button>
472
429
  </Tooltip>
473
430
  )}
474
- <div
475
- className={twMerge([
476
- location.pathname !== '/result' && 'hidden desktop:flex',
477
- ])}
478
- >
479
- <input
480
- accept={
481
- '.stp,.step,.stl,.obj,.glb,.gltf,.heic,.heif,.pdf,image/*'
482
- }
483
- id="icon-button-file"
484
- type="file"
485
- style={{ display: 'none' }}
486
- onClick={e => {
487
- e.stopPropagation();
488
- }}
489
- onChange={e => {
490
- if (e?.target?.files) {
491
- handleUpload(Array.from(e.target.files));
492
- }
493
- e.target.value = '';
494
- }}
495
- />
496
- <Tooltip content={t('Search with an image')}>
497
- <label
498
- className={twMerge(
499
- 'mr-2 desktop:mr-1',
500
- 'w-10 h-10 flex justify-center items-center cursor-pointer rounded-full bg-gray-100 desktop:bg-transparent hover:bg-gray-100',
501
- location.pathname === '/result' && 'desktop:w-8 desktop:h-8',
502
- )}
503
- htmlFor={
504
- showDisclaimerDisabled && !onCameraClick
505
- ? 'icon-button-file'
506
- : ''
507
- }
508
- onClick={e => {
509
- if (!showDisclaimerDisabled) {
510
- // disclaimer
511
- setShowDisclaimer(true);
512
- } else if (onCameraClick) {
513
- onCameraClick();
514
- }
515
- }}
516
- >
517
- <Icon
518
- name="camera_simple"
519
- width={16}
520
- height={16}
521
- fill="#2B2C46"
522
- />
523
- </label>
524
- </Tooltip>
525
- </div>
431
+
432
+ <ImageUpload onCameraClick={onCameraClick} />
526
433
  </div>
527
434
  </div>
435
+
528
436
  {showPreFilter && (
529
437
  <PreFilterModal
530
438
  openModal={isOpenModalFilterDesktop}
531
439
  handleClose={() => setToggleModalFilterDesktop(false)}
532
440
  />
533
441
  )}
534
-
535
- {showDisclaimer && (
536
- <UploadDisclaimer
537
- onClose={() => {
538
- setShowDisclaimer(false);
539
- }}
540
- onContinue={({
541
- file,
542
- dontShowAgain,
543
- }: {
544
- file: any;
545
- dontShowAgain: any;
546
- }) => {
547
- if (dontShowAgain) {
548
- localStorage.setItem('upload-disclaimer-suite', 'dont-show');
549
- }
550
-
551
- if (onCameraClick) {
552
- onCameraClick();
553
- } else {
554
- if (file) {
555
- handleUpload(Array(file));
556
- }
557
- }
558
-
559
- setShowDisclaimer(false);
560
- }}
561
- />
562
- )}
563
442
  </div>
564
443
  );
565
444
  }
@@ -13,33 +13,52 @@ export const Toaster = () => {
13
13
 
14
14
  return (
15
15
  <ReactHotToaster
16
- containerClassName="bottom-[80px] desktop:bottom-auto desktop:top-[60px]"
17
16
  position={isMobile ? 'bottom-center' : 'top-right'}
17
+ containerStyle={{
18
+ top: 80,
19
+ bottom: isMobile ? 80 : 'auto',
20
+ }}
18
21
  >
19
22
  {t => (
20
- <ToastBar toast={t} style={{ padding: 0, borderRadius: 0 }}>
23
+ <ToastBar
24
+ toast={t}
25
+ style={{
26
+ padding: 0,
27
+ borderRadius: 0,
28
+ width: !isMobile ? 410 : undefined,
29
+ }}
30
+ >
21
31
  {({ icon }) => (
22
32
  <>
23
33
  <span
24
34
  style={{
25
35
  width: 5,
26
36
  height: '100%',
27
- background: t.type === 'success' ? '#61d345' : 'transparent',
37
+ background: t.type === 'success' ? '#3E36DC' : 'transparent',
28
38
  marginRight: 7,
29
39
  }}
30
40
  />
31
- <span style={{ padding: 15, display: 'inline-flex' }}>
32
- {icon}
33
- <span
34
- style={{
35
- fontSize: 15,
36
- fontWeight: 300,
37
- margin: '0 10px',
38
- }}
39
- >
40
- {' '}
41
- {resolveValue(t.message, t)}
42
- </span>
41
+ <span
42
+ style={{
43
+ padding: 15,
44
+ display: 'flex',
45
+ justifyContent: 'space-between',
46
+ width: '100%',
47
+ }}
48
+ >
49
+ <div className="flex items-center justify-center">
50
+ {icon}
51
+ <span
52
+ style={{
53
+ fontSize: 14,
54
+ fontWeight: 600,
55
+ margin: '0 10px',
56
+ }}
57
+ >
58
+ {' '}
59
+ {resolveValue(t.message, t)}
60
+ </span>
61
+ </div>
43
62
  {t.type !== 'loading' && (
44
63
  <span style={{ display: 'flex', alignItems: 'center' }}>
45
64
  <Icon
@@ -5,10 +5,14 @@ export class ToastHelper {
5
5
  toast.success(msg, {
6
6
  duration: 3000,
7
7
  style: {
8
- background: '#1E1F31',
9
- color: '#fff',
8
+ background: '#E4E3FF',
9
+ color: '#000000',
10
10
  },
11
11
  position: position,
12
+ iconTheme: {
13
+ primary: '#3E36DC',
14
+ secondary: '#fff',
15
+ },
12
16
  });
13
17
  }
14
18
 
@@ -17,6 +17,9 @@ export const useCadSearch = () => {
17
17
 
18
18
  const setRegions = useRequestStore(state => state.setRegions);
19
19
  const setRequestImages = useRequestStore(state => state.setRequestImages);
20
+ const clearPostFilterSelections = useRequestStore(
21
+ state => state.clearPostFilterSelections,
22
+ );
20
23
 
21
24
  const setIsFindApiLoading = useUiStore(state => state.setIsFindApiLoading);
22
25
  const setShowFeedback = useUiStore(state => state.setShowFeedback);
@@ -66,6 +69,7 @@ export const useCadSearch = () => {
66
69
 
67
70
  if (clearPostFilter) {
68
71
  refine();
72
+ clearPostFilterSelections();
69
73
  }
70
74
 
71
75
  const responseBody = res?.responseBody;
@@ -136,6 +140,7 @@ export const useCadSearch = () => {
136
140
  [
137
141
  preFilter,
138
142
  refine,
143
+ clearPostFilterSelections,
139
144
  setAlgoliaFilter,
140
145
  setDetectedRegions,
141
146
  setFindApiProducts,