@nyris/nyris-webapp 0.3.97 → 0.3.99

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 (94) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +41 -0
  4. package/build/static/css/main.5cb72c09.css +4 -0
  5. package/build/static/css/main.5cb72c09.css.map +1 -0
  6. package/build/static/js/main.2339271b.js +3 -0
  7. package/build/static/js/{main.9e7fb38a.js.map → main.2339271b.js.map} +1 -1
  8. package/build/static/media/plate_info.6beaea36d35053be3a45.png +0 -0
  9. package/package.json +3 -3
  10. package/public/js/settings.example.js +41 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/AuthApiKeySync.tsx +34 -0
  13. package/src/components/Cart.tsx +41 -0
  14. package/src/components/ChatAssistant/useChatAssistantLogic.ts +3 -2
  15. package/src/components/GoBackButton.tsx +29 -2
  16. package/src/components/Header.tsx +3 -0
  17. package/src/components/HitsPerPage.tsx +55 -18
  18. package/src/components/ImagePreview.tsx +31 -1
  19. package/src/components/ImageUpload.tsx +14 -3
  20. package/src/components/Inquiry/InquiryModal.tsx +3 -0
  21. package/src/components/LogoutModal.tsx +2 -0
  22. package/src/components/Pagination.tsx +33 -6
  23. package/src/components/PostFilter/PostFilter.tsx +8 -1
  24. package/src/components/PostFilter/PostFilterComponent.tsx +8 -22
  25. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  26. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  27. package/src/components/PreFilter/PreFilter.tsx +10 -2
  28. package/src/components/Product/Product.tsx +60 -10
  29. package/src/components/Product/ProductDetailView.tsx +23 -1
  30. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  31. package/src/components/Product/ProductList.tsx +51 -16
  32. package/src/components/Rfq/RfqModal.tsx +3 -0
  33. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  34. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  35. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  36. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  37. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  38. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  39. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  41. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  43. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  44. package/src/components/SidePanel.tsx +43 -0
  45. package/src/components/TextSearch.tsx +80 -14
  46. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  47. package/src/components/TypePlateUpload.tsx +132 -0
  48. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  49. package/src/hooks/useGoodResultsChat.ts +8 -22
  50. package/src/hooks/useImageSearch.ts +325 -20
  51. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  52. package/src/hooks/useResultEvaluator.ts +43 -19
  53. package/src/index.css +1 -1
  54. package/src/index.tsx +4 -0
  55. package/src/layouts/AppLayout.tsx +95 -9
  56. package/src/pages/Home.tsx +60 -14
  57. package/src/pages/Logout.tsx +2 -0
  58. package/src/pages/Result.tsx +206 -20
  59. package/src/providers/AuthProvider.tsx +5 -0
  60. package/src/services/Feedback.ts +224 -35
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +4 -3
  63. package/src/services/initialMachinePartsText.payload.test.ts +61 -0
  64. package/src/services/initialMachinePartsText.test.ts +33 -0
  65. package/src/services/initialMachinePartsText.ts +11 -0
  66. package/src/services/machinePartsPage.test.ts +74 -0
  67. package/src/services/machinePartsPage.ts +120 -0
  68. package/src/services/normalizeChatResponse.ts +26 -0
  69. package/src/services/scopeLookup.test.ts +67 -0
  70. package/src/services/scopeLookup.ts +39 -0
  71. package/src/services/visualSearch.listingOptions.test.ts +79 -0
  72. package/src/services/visualSearch.ts +76 -7
  73. package/src/services/vizo.ts +17 -5
  74. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  75. package/src/stores/request/filter/filter.initialState.ts +1 -0
  76. package/src/stores/request/filter/filter.slice.ts +2 -0
  77. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  78. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  79. package/src/stores/scope/scopeStore.ts +67 -4
  80. package/src/stores/types.ts +19 -14
  81. package/src/translations.ts +213 -0
  82. package/src/types.ts +40 -0
  83. package/src/utils/derivePostFilterFacets.ts +68 -0
  84. package/src/utils/fetchProductImage.ts +1 -0
  85. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  86. package/src/utils/nyrisApiKey.ts +115 -0
  87. package/src/utils/productCardImageUrl.test.ts +41 -0
  88. package/src/utils/productCardImageUrl.ts +49 -0
  89. package/src/utils/textSearchFilter.test.ts +41 -0
  90. package/src/utils/textSearchFilter.ts +13 -6
  91. package/build/static/css/main.7c7f5447.css +0 -4
  92. package/build/static/css/main.7c7f5447.css.map +0 -1
  93. package/build/static/js/main.9e7fb38a.js +0 -3
  94. /package/build/static/js/{main.9e7fb38a.js.LICENSE.txt → main.2339271b.js.LICENSE.txt} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { useCallback } from 'react';
2
+ import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
2
3
  import { RectCoords } from '@nyris/nyris-api';
3
4
  import { isEmpty, isUndefined } from 'lodash';
4
5
 
@@ -17,16 +18,131 @@ import { isHEIC } from 'utils/misc';
17
18
  import useResultEvaluator from './useResultEvaluator';
18
19
  import useConversationStore from 'stores/chat/conversationStore';
19
20
  import { useGroundedSearch } from './useGroundedSearch';
20
- import { generateSmartFilters } from '../services/vizo';
21
- import { cropImage } from 'utils/cropImageToBase64';
22
- import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
21
+ import { chatAnalysis, generateSmartFilters } from '../services/vizo';
22
+ import { parseScopeIdChatResponse } from 'components/ScopeSelect/parseScopeIdResponse';
23
+ import { lookupScopeById } from 'services/scopeLookup';
24
+ import { canvasToBase64, cropImage } from 'utils/cropImageToBase64';
25
+ import useScopeStore, { hasActiveMachineScope } from 'stores/scope/scopeStore';
26
+ import { derivePostFilterFacetsFromResults } from 'utils/derivePostFilterFacets';
23
27
  import { useAuth0 } from '@auth0/auth0-react';
28
+ import { isNyrisApiKeyReady } from 'utils/nyrisApiKey';
29
+ import { trackRegion } from 'services/Feedback';
30
+ import { REGION_SUBTYPE } from 'services/feedbackSubtypes';
31
+
32
+ export type TypePlateScopeResult =
33
+ | { status: 'applied'; itemKeys: string[] }
34
+ | { status: 'empty' }
35
+ | { status: 'not_found' }
36
+ | { status: 'unavailable' };
37
+
38
+ export async function applyScopeById(
39
+ scopeId: string,
40
+ ): Promise<TypePlateScopeResult> {
41
+ const lookup = await lookupScopeById(scopeId);
42
+ if (lookup.status !== 'found') {
43
+ return { status: lookup.status };
44
+ }
45
+
46
+ useScopeStore
47
+ .getState()
48
+ .setScope(lookup.itemKeys, lookup.filterAttributes, scopeId);
49
+ return { status: 'applied', itemKeys: lookup.itemKeys };
50
+ }
51
+
52
+ /**
53
+ * Extract scopeId from a type-plate image. Does not call GET /v1/scopes.
54
+ */
55
+ export async function extractScopeIdFromTypePlate(file: File): Promise<{
56
+ extractedScopeId: string;
57
+ status: 'extracted' | 'not_found' | 'unavailable';
58
+ }> {
59
+ const settings = window.settings;
60
+ if (!settings.scopes?.scopeID) {
61
+ return { extractedScopeId: '', status: 'unavailable' };
62
+ }
63
+
64
+ if (!isNyrisApiKeyReady()) {
65
+ return { extractedScopeId: '', status: 'unavailable' };
66
+ }
67
+
68
+ const requestImage = await createImage(file);
69
+ const croppedCanvas = cropImage(requestImage);
70
+ const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
71
+ const parts: {
72
+ text?: string;
73
+ inlineData?: { mimeType: string; data: string };
74
+ }[] = [
75
+ {
76
+ text: 'request_image',
77
+ inlineData: { mimeType: 'image/jpeg', data: base64Image },
78
+ },
79
+ ];
80
+
81
+ const response = await chatAnalysis({
82
+ promptKey: 'scope_id',
83
+ multimodalInputs: parts.map(p =>
84
+ p.inlineData
85
+ ? {
86
+ metadata: p.text || '',
87
+ mimeType: p.inlineData.mimeType,
88
+ image: p.inlineData.data,
89
+ }
90
+ : {
91
+ metadata: p.text || '',
92
+ },
93
+ ),
94
+ });
95
+
96
+ const parsed = parseScopeIdChatResponse(response);
97
+ if (parsed.status !== 'extracted') {
98
+ return { extractedScopeId: '', status: 'not_found' };
99
+ }
100
+ return { extractedScopeId: parsed.serialNumber, status: 'extracted' };
101
+ }
102
+
103
+ /**
104
+ * Nameplate upload only resolves scope context (itemKeys).
105
+ * Does not run /find or navigate — normal searches happen afterwards.
106
+ */
107
+ export async function resolveTypePlateScope(
108
+ file: File,
109
+ ): Promise<TypePlateScopeResult & { extractedScopeId: string }> {
110
+ const extract = await extractScopeIdFromTypePlate(file);
111
+ if (extract.status !== 'extracted') {
112
+ return {
113
+ status: extract.status === 'unavailable' ? 'unavailable' : 'not_found',
114
+ extractedScopeId: extract.extractedScopeId,
115
+ };
116
+ }
117
+ const result = await applyScopeById(extract.extractedScopeId);
118
+ return { ...result, extractedScopeId: extract.extractedScopeId };
119
+ }
120
+
121
+ const RECT_TOLERANCE = 1e-3;
122
+
123
+ const isSameRect = (a: RectCoords, b: RectCoords) =>
124
+ Math.abs(a.x1 - b.x1) < RECT_TOLERANCE &&
125
+ Math.abs(a.y1 - b.y1) < RECT_TOLERANCE &&
126
+ Math.abs(a.x2 - b.x2) < RECT_TOLERANCE &&
127
+ Math.abs(a.y2 - b.y2) < RECT_TOLERANCE;
128
+
129
+ /**
130
+ * A user-selected region that matches one of the detected object regions
131
+ * came from clicking a detection dot; anything else is a manual crop.
132
+ */
133
+ const getRegionSubtype = (region: RectCoords) => {
134
+ const detected = useResultStore.getState().detectedRegions?.[0] || [];
135
+ return detected.some(entry => isSameRect(entry.normalizedRect, region))
136
+ ? REGION_SUBTYPE.OBJECT_DETECTION_CLICK
137
+ : REGION_SUBTYPE.CROP;
138
+ };
139
+
140
+ let findSearchGeneration = 0;
24
141
 
25
142
  export const useImageSearch = () => {
26
143
  const { user } = useAuth0();
27
144
 
28
145
  const setRegions = useRequestStore(state => state.setRegions);
29
- const query = useRequestStore(state => state.query);
30
146
 
31
147
  const setRequestImages = useRequestStore(state => state.setRequestImages);
32
148
  const setSpecificationFilter = useRequestStore(
@@ -72,6 +188,7 @@ export const useImageSearch = () => {
72
188
  state => state.setGroundingFilterResult,
73
189
  );
74
190
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
191
+ const setRequestQuality = useResultStore(state => state.setRequestQuality);
75
192
  const setPostFilterFacets = useResultStore(
76
193
  state => state.setPostFilterFacets,
77
194
  );
@@ -97,6 +214,7 @@ export const useImageSearch = () => {
97
214
  preFilterParams,
98
215
  clearPostFilter,
99
216
  text,
217
+ regionSelectedByUser = false,
100
218
  }: {
101
219
  image?: any;
102
220
  settings: AppSettings;
@@ -107,8 +225,13 @@ export const useImageSearch = () => {
107
225
  preFilterParams?: Record<string, boolean>;
108
226
  clearPostFilter?: boolean;
109
227
  text?: string;
228
+ /** True when the user just chose `imageRegion` (crop / detection dot). */
229
+ regionSelectedByUser?: boolean;
110
230
  }) => {
231
+ const searchGeneration = ++findSearchGeneration;
232
+ const isCurrentSearch = () => searchGeneration === findSearchGeneration;
111
233
  const isSmartFiltersEnabled = !!settings.useSmartFilters;
234
+ const isRequestQualityEnabled = !!settings.requestQuality;
112
235
  setIsFindApiLoading(true);
113
236
  // setAlgoliaProducts([]);
114
237
  useRequestStore.getState().setGroundingQuery('');
@@ -133,6 +256,7 @@ export const useImageSearch = () => {
133
256
  errorMessage: null,
134
257
  });
135
258
  }
259
+ setRequestQuality(null);
136
260
  useRequestStore.getState().setGroundingQuery('');
137
261
  useResultStore.getState().setShowingGroundingFilterResult(false);
138
262
  startNewSession();
@@ -181,6 +305,7 @@ export const useImageSearch = () => {
181
305
  canvasImage = await createImage(compressedBase64 || blob);
182
306
 
183
307
  requestImage = await createImage(blob);
308
+ if (!isCurrentSearch()) return;
184
309
 
185
310
  if (!imageRegion) {
186
311
  setRequestImages([canvasImage]);
@@ -190,10 +315,12 @@ export const useImageSearch = () => {
190
315
  if (!imageRegion) {
191
316
  try {
192
317
  let res = await findRegions(requestImage, settings);
318
+ if (!isCurrentSearch()) return;
193
319
  setDetectedRegions(res.regions, 0);
194
320
  region = res.selectedRegion;
195
321
  setRegions([region]);
196
322
  } catch (error) {}
323
+ if (!isCurrentSearch()) return;
197
324
  }
198
325
  }
199
326
 
@@ -213,6 +340,7 @@ export const useImageSearch = () => {
213
340
  ];
214
341
 
215
342
  let smartFilterPromise: Promise<void> | null = null;
343
+ let requestQualityPromise: Promise<string | null> | null = null;
216
344
 
217
345
  if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
218
346
  setSmartFilters({
@@ -228,6 +356,7 @@ export const useImageSearch = () => {
228
356
  const smartFilterImage = cropImage(requestImage, region);
229
357
  smartFilterPromise = generateSmartFilters({ image: smartFilterImage })
230
358
  .then(response => {
359
+ if (!isCurrentSearch()) return;
231
360
  const rawFilters = response?.filters;
232
361
  if (!rawFilters || typeof rawFilters !== 'object') {
233
362
  setSmartFilters({
@@ -276,6 +405,7 @@ export const useImageSearch = () => {
276
405
  });
277
406
  })
278
407
  .catch(error => {
408
+ if (!isCurrentSearch()) return;
279
409
  const timeout = String(error?.message || '').includes(
280
410
  'SMART_FILTER_TIMEOUT',
281
411
  );
@@ -292,23 +422,82 @@ export const useImageSearch = () => {
292
422
  });
293
423
  });
294
424
  }
425
+ if (
426
+ isRequestQualityEnabled &&
427
+ requestImage &&
428
+ (newSearch || imageRegion)
429
+ ) {
430
+ const requestQualityImage = cropImage(requestImage, region);
431
+ const requestQualityBase64 = canvasToBase64(
432
+ requestQualityImage,
433
+ 'image/jpeg',
434
+ 1,
435
+ );
436
+ const parts: Array<{
437
+ text?: string;
438
+ inlineData?: { mimeType: string; data: string };
439
+ }> = [
440
+ {
441
+ text: 'data',
442
+ inlineData: {
443
+ mimeType: 'image/jpeg',
444
+ data: requestQualityBase64,
445
+ },
446
+ },
447
+ ];
448
+
449
+ requestQualityPromise = chatAnalysis({
450
+ promptKey: 'request_quality',
451
+ multimodalInputs: parts.map(p =>
452
+ p.inlineData
453
+ ? {
454
+ metadata: p.text || '',
455
+ mimeType: p.inlineData.mimeType,
456
+ image: p.inlineData.data,
457
+ }
458
+ : {
459
+ metadata: p.text || '',
460
+ },
461
+ ),
462
+ })
463
+ .then(response => {
464
+ const description =
465
+ typeof response === 'string' ? response.trim() : '';
466
+ return description.length > 0 ? description : null;
467
+ })
468
+ .catch(() => null);
469
+ }
470
+ if (!requestImage) {
471
+ setRequestQuality(null);
472
+ }
295
473
 
474
+ const usesFindApiPostFilters =
475
+ !settings.algolia?.enabled && settings.postFilterOption;
476
+ // Crop/region finds replace results without sending postFilterSelections;
477
+ // clear stale selections so checkboxes match the new unfiltered grid.
296
478
  const shouldResetPostFilters =
297
479
  clearPostFilter ||
298
- (!settings.algolia?.enabled &&
299
- !requestImage &&
300
- settings.postFilterOption);
480
+ (Boolean(imageRegion) && usesFindApiPostFilters) ||
481
+ (!requestImage && settings.postFilterOption);
301
482
 
302
483
  if (shouldResetPostFilters) {
303
484
  invalidatePostFilterRequests();
304
485
  refine();
305
486
  clearPostFilterSelections();
306
- if (clearPostFilter) {
487
+ if (clearPostFilter || !requestImage) {
307
488
  setPostFilterFacets([]);
308
489
  setInitialFindSearchSnapshot(null);
309
490
  }
310
491
  }
311
492
 
493
+ const requestedText = isUndefined(text)
494
+ ? useRequestStore.getState().query
495
+ : text;
496
+ const keyword =
497
+ typeof requestedText === 'string' && requestedText.length > 0
498
+ ? requestedText
499
+ : undefined;
500
+
312
501
  try {
313
502
  res = await find({
314
503
  image: requestImage,
@@ -325,26 +514,56 @@ export const useImageSearch = () => {
325
514
  : undefined,
326
515
  region,
327
516
  text:
328
- !window.settings.algolia.enabled && !requestImage
329
- ? isUndefined(text)
330
- ? query
331
- : text
517
+ !requestImage &&
518
+ (hasActiveMachineScope() || !window.settings.algolia.enabled)
519
+ ? keyword
332
520
  : undefined,
333
521
  scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
334
522
  });
523
+ if (!isCurrentSearch()) return;
335
524
  useResultStore.getState().resetGrounding();
336
525
 
337
526
  setFindApiProducts(res?.results);
338
- const initialFacets = res?.facets || [];
339
- setPostFilterFacets(initialFacets);
527
+
528
+ // Image: API facets if present, else derive from result.filters.
529
+ // Text: API facets only.
530
+ // Empty results with no facet payload may keep prior facet UI for unselect,
531
+ // but the search snapshot always updates to this response.
532
+ const isImageSearch = Boolean(requestImage);
533
+ const results = res?.results || [];
534
+ const apiFacets = Array.isArray(res?.facets) ? res.facets : [];
535
+ const nextFacets = isImageSearch
536
+ ? apiFacets.length > 0
537
+ ? apiFacets
538
+ : derivePostFilterFacetsFromResults(results)
539
+ : apiFacets;
540
+ const previousFacets = useResultStore.getState().postFilterFacets;
541
+ const shouldPreserveFacets =
542
+ results.length === 0 &&
543
+ nextFacets.length === 0 &&
544
+ Array.isArray(previousFacets) &&
545
+ previousFacets.length > 0;
546
+
547
+ // Facet UI may stay for recovery; snapshot always matches this search.
548
+ if (!shouldPreserveFacets) {
549
+ setPostFilterFacets(nextFacets);
550
+ }
340
551
  setInitialFindSearchSnapshot({
341
- results: res?.results || [],
342
- postFilterFacets: initialFacets,
552
+ results,
553
+ postFilterFacets: nextFacets,
343
554
  });
555
+
344
556
  setImageAnalysis(res?.image_analysis);
345
557
  setSessionId(res?.session);
346
558
  setRequestId(res?.id);
347
559
 
560
+ if (regionSelectedByUser && imageRegion && res?.id) {
561
+ trackRegion(imageRegion, getRegionSubtype(imageRegion), {
562
+ requestId: res.id,
563
+ sessionId: res.session,
564
+ });
565
+ }
566
+
348
567
  const specification = res?.image_analysis?.specification || {};
349
568
  const hasOcr = Object.keys(specification).length > 0;
350
569
 
@@ -363,6 +582,7 @@ export const useImageSearch = () => {
363
582
  evaluateResults({
364
583
  activeProducts: res?.results || [],
365
584
  imageAnalysis: res?.image_analysis,
585
+ query: !requestImage ? keyword || '' : '',
366
586
  });
367
587
  }
368
588
  const nonEmptyFilter: any[] = ['sku:DOES_NOT_EXIST<score=1> '];
@@ -374,7 +594,9 @@ export const useImageSearch = () => {
374
594
  : '';
375
595
  const filterSkusString = [...nonEmptyFilter, ...filterSkus].join('OR ');
376
596
 
377
- setAlgoliaFilter(filterSkusString);
597
+ if (!hasActiveMachineScope()) {
598
+ setAlgoliaFilter(filterSkusString);
599
+ }
378
600
  setIsFindApiLoading(false);
379
601
 
380
602
  if (showFeedback) {
@@ -390,12 +612,21 @@ export const useImageSearch = () => {
390
612
  setFirstRequestImageAnalysis(res?.image_analysis);
391
613
  }
392
614
  } catch (error) {
393
- setIsFindApiLoading(false);
615
+ if (isCurrentSearch()) {
616
+ setIsFindApiLoading(false);
617
+ }
394
618
  }
395
619
 
396
620
  if (smartFilterPromise) {
397
621
  void smartFilterPromise;
398
622
  }
623
+ if (requestQualityPromise) {
624
+ void requestQualityPromise.then(quality => {
625
+ if (isCurrentSearch()) {
626
+ setRequestQuality(quality);
627
+ }
628
+ });
629
+ }
399
630
 
400
631
  return res;
401
632
  },
@@ -409,7 +640,6 @@ export const useImageSearch = () => {
409
640
  setDetectedRegions,
410
641
  setRegions,
411
642
  metaFilter,
412
- query,
413
643
  setFindApiProducts,
414
644
  setImageAnalysis,
415
645
  setSessionId,
@@ -426,11 +656,86 @@ export const useImageSearch = () => {
426
656
  setFirstSearchPreFilter,
427
657
  setFirstRequestImageAnalysis,
428
658
  setSmartFilters,
659
+ setRequestQuality,
429
660
  setPostFilterFacets,
430
661
  setInitialFindSearchSnapshot,
431
662
  user,
432
663
  ],
433
664
  );
434
665
 
435
- return { singleImageSearch };
666
+ /**
667
+ * Scope-only itemKeys page. Reuses find(), but does not start a search:
668
+ * no Vizo session, no evaluateResults, and no first-search snapshot.
669
+ */
670
+ const searchMachinePartsListing = useCallback(
671
+ async ({
672
+ text,
673
+ showFeedback = false,
674
+ clearPostFilters = false,
675
+ }: {
676
+ text: string;
677
+ showFeedback?: boolean;
678
+ clearPostFilters?: boolean;
679
+ }) => {
680
+ if (!text) {
681
+ return;
682
+ }
683
+ const searchGeneration = ++findSearchGeneration;
684
+ const isCurrentSearch = () => searchGeneration === findSearchGeneration;
685
+ setIsFindApiLoading(true);
686
+
687
+ if (clearPostFilters) {
688
+ invalidatePostFilterRequests();
689
+ refine();
690
+ clearPostFilterSelections();
691
+ setPostFilterFacets([]);
692
+ setInitialFindSearchSnapshot(null);
693
+ }
694
+
695
+ try {
696
+ const res: any = await find({
697
+ settings: window.settings,
698
+ text,
699
+ });
700
+ if (!isCurrentSearch()) {
701
+ return;
702
+ }
703
+ const results = res?.results || [];
704
+ const facets = Array.isArray(res?.facets) ? res.facets : [];
705
+ setFindApiProducts(results);
706
+ setImageAnalysis(res?.image_analysis);
707
+ setSessionId(res?.session);
708
+ setRequestId(res?.id);
709
+ setPostFilterFacets(facets);
710
+ setInitialFindSearchSnapshot({
711
+ results,
712
+ postFilterFacets: facets,
713
+ });
714
+ useResultStore.getState().setShowingGroundingFilterResult(false);
715
+ setIsFindApiLoading(false);
716
+ if (showFeedback) {
717
+ setShowFeedback(true);
718
+ }
719
+ return res;
720
+ } catch {
721
+ if (isCurrentSearch()) {
722
+ setIsFindApiLoading(false);
723
+ }
724
+ }
725
+ },
726
+ [
727
+ clearPostFilterSelections,
728
+ refine,
729
+ setFindApiProducts,
730
+ setImageAnalysis,
731
+ setInitialFindSearchSnapshot,
732
+ setIsFindApiLoading,
733
+ setPostFilterFacets,
734
+ setRequestId,
735
+ setSessionId,
736
+ setShowFeedback,
737
+ ],
738
+ );
739
+
740
+ return { singleImageSearch, searchMachinePartsListing };
436
741
  };
@@ -0,0 +1,95 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useImageSearch } from 'hooks/useImageSearch';
3
+ import {
4
+ hasActivePostFilters,
5
+ isScopeOnlyMachinePartsListing,
6
+ machinePartsPageCount,
7
+ machinePartsPageText,
8
+ } from 'services/machinePartsPage';
9
+ import useMachinePartsListingStore from 'stores/machinePartsListing/machinePartsListingStore';
10
+ import useRequestStore from 'stores/request/requestStore';
11
+ import useScopeStore from 'stores/scope/scopeStore';
12
+
13
+ export function useMachinePartsListingPagination() {
14
+ const pageSize = useMachinePartsListingStore(state => state.pageSize);
15
+ const pageIndex = useMachinePartsListingStore(state => state.pageIndex);
16
+ const setPageSize = useMachinePartsListingStore(state => state.setPageSize);
17
+ const setPageIndex = useMachinePartsListingStore(state => state.setPageIndex);
18
+ const scopeId = useScopeStore(state => state.scopeId);
19
+ const itemKeys = useScopeStore(state => state.itemKeys);
20
+ const query = useRequestStore(state => state.query);
21
+ const requestImages = useRequestStore(state => state.requestImages);
22
+ const postFilterSelections = useRequestStore(
23
+ state => state.postFilterSelections,
24
+ );
25
+ const { searchMachinePartsListing } = useImageSearch();
26
+
27
+ const isListing = useMemo(
28
+ () =>
29
+ isScopeOnlyMachinePartsListing({
30
+ scopeEnabled: Boolean(window.settings.scopes?.scopeID),
31
+ scopeId,
32
+ itemKeyCount: itemKeys.length,
33
+ keyword: query || '',
34
+ hasSearchImage: requestImages.length > 0,
35
+ hasPostFilters: hasActivePostFilters(postFilterSelections),
36
+ }),
37
+ [itemKeys.length, postFilterSelections, query, requestImages.length, scopeId],
38
+ );
39
+
40
+ const pageCount = isListing
41
+ ? machinePartsPageCount(itemKeys.length, pageSize)
42
+ : 0;
43
+
44
+ const searchPage = useCallback(
45
+ (nextPageIndex: number, nextPageSize: number) => {
46
+ const text = machinePartsPageText(
47
+ useScopeStore.getState().itemKeys || [],
48
+ nextPageIndex,
49
+ nextPageSize,
50
+ );
51
+ if (!text) {
52
+ return;
53
+ }
54
+ void searchMachinePartsListing({ text });
55
+ },
56
+ [searchMachinePartsListing],
57
+ );
58
+
59
+ const changePage = useCallback(
60
+ (nextPageIndex: number) => {
61
+ if (!isListing || nextPageIndex === pageIndex) {
62
+ return;
63
+ }
64
+ if (nextPageIndex < 0 || nextPageIndex >= pageCount) {
65
+ return;
66
+ }
67
+ setPageIndex(nextPageIndex);
68
+ searchPage(nextPageIndex, pageSize);
69
+ },
70
+ [isListing, pageCount, pageIndex, pageSize, searchPage, setPageIndex],
71
+ );
72
+
73
+ const changePageSize = useCallback(
74
+ (nextPageSize: number) => {
75
+ if (nextPageSize === pageSize && pageIndex === 0) {
76
+ return;
77
+ }
78
+ setPageSize(nextPageSize);
79
+ if (!isListing) {
80
+ return;
81
+ }
82
+ searchPage(0, nextPageSize);
83
+ },
84
+ [isListing, pageIndex, pageSize, searchPage, setPageSize],
85
+ );
86
+
87
+ return {
88
+ isListing,
89
+ pageCount,
90
+ pageIndex,
91
+ pageSize,
92
+ changePage,
93
+ changePageSize,
94
+ };
95
+ }