@nyris/nyris-webapp 0.3.98 → 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 (85) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +40 -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.93fdbd6e.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 +40 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/Cart.tsx +41 -0
  13. package/src/components/GoBackButton.tsx +29 -2
  14. package/src/components/Header.tsx +3 -0
  15. package/src/components/HitsPerPage.tsx +55 -18
  16. package/src/components/ImagePreview.tsx +31 -1
  17. package/src/components/ImageUpload.tsx +12 -1
  18. package/src/components/Inquiry/InquiryModal.tsx +3 -0
  19. package/src/components/Pagination.tsx +33 -6
  20. package/src/components/PostFilter/PostFilter.tsx +8 -1
  21. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  22. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  23. package/src/components/PreFilter/PreFilter.tsx +10 -2
  24. package/src/components/Product/Product.tsx +60 -10
  25. package/src/components/Product/ProductDetailView.tsx +23 -1
  26. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  27. package/src/components/Product/ProductList.tsx +51 -18
  28. package/src/components/Rfq/RfqModal.tsx +3 -0
  29. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  30. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  31. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  32. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  33. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  34. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  35. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  36. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  37. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  38. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  39. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  40. package/src/components/SidePanel.tsx +33 -0
  41. package/src/components/TextSearch.tsx +77 -11
  42. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  43. package/src/components/TypePlateUpload.tsx +132 -0
  44. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  45. package/src/hooks/useGoodResultsChat.ts +8 -22
  46. package/src/hooks/useImageSearch.ts +290 -12
  47. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  48. package/src/hooks/useResultEvaluator.ts +43 -19
  49. package/src/index.css +1 -1
  50. package/src/layouts/AppLayout.tsx +43 -3
  51. package/src/pages/Home.tsx +60 -14
  52. package/src/pages/Result.tsx +194 -14
  53. package/src/services/Feedback.ts +223 -35
  54. package/src/services/feedbackSubtypes.ts +63 -0
  55. package/src/services/filter.ts +1 -1
  56. package/src/services/initialMachinePartsText.payload.test.ts +61 -0
  57. package/src/services/initialMachinePartsText.test.ts +33 -0
  58. package/src/services/initialMachinePartsText.ts +11 -0
  59. package/src/services/machinePartsPage.test.ts +74 -0
  60. package/src/services/machinePartsPage.ts +120 -0
  61. package/src/services/normalizeChatResponse.ts +26 -0
  62. package/src/services/scopeLookup.test.ts +67 -0
  63. package/src/services/scopeLookup.ts +39 -0
  64. package/src/services/visualSearch.listingOptions.test.ts +79 -0
  65. package/src/services/visualSearch.ts +71 -3
  66. package/src/services/vizo.ts +4 -3
  67. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  68. package/src/stores/request/filter/filter.initialState.ts +1 -0
  69. package/src/stores/request/filter/filter.slice.ts +2 -0
  70. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  71. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  72. package/src/stores/scope/scopeStore.ts +67 -4
  73. package/src/stores/types.ts +19 -14
  74. package/src/translations.ts +213 -0
  75. package/src/types.ts +40 -0
  76. package/src/utils/fetchProductImage.ts +1 -0
  77. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  78. package/src/utils/productCardImageUrl.test.ts +41 -0
  79. package/src/utils/productCardImageUrl.ts +49 -0
  80. package/src/utils/textSearchFilter.test.ts +41 -0
  81. package/src/utils/textSearchFilter.ts +13 -6
  82. package/build/static/css/main.81216c5d.css +0 -4
  83. package/build/static/css/main.81216c5d.css.map +0 -1
  84. package/build/static/js/main.93fdbd6e.js +0 -3
  85. /package/build/static/js/{main.93fdbd6e.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,17 +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';
23
26
  import { derivePostFilterFacetsFromResults } from 'utils/derivePostFilterFacets';
24
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;
25
141
 
26
142
  export const useImageSearch = () => {
27
143
  const { user } = useAuth0();
28
144
 
29
145
  const setRegions = useRequestStore(state => state.setRegions);
30
- const query = useRequestStore(state => state.query);
31
146
 
32
147
  const setRequestImages = useRequestStore(state => state.setRequestImages);
33
148
  const setSpecificationFilter = useRequestStore(
@@ -73,6 +188,7 @@ export const useImageSearch = () => {
73
188
  state => state.setGroundingFilterResult,
74
189
  );
75
190
  const setSmartFilters = useResultStore(state => state.setSmartFilters);
191
+ const setRequestQuality = useResultStore(state => state.setRequestQuality);
76
192
  const setPostFilterFacets = useResultStore(
77
193
  state => state.setPostFilterFacets,
78
194
  );
@@ -98,6 +214,7 @@ export const useImageSearch = () => {
98
214
  preFilterParams,
99
215
  clearPostFilter,
100
216
  text,
217
+ regionSelectedByUser = false,
101
218
  }: {
102
219
  image?: any;
103
220
  settings: AppSettings;
@@ -108,8 +225,13 @@ export const useImageSearch = () => {
108
225
  preFilterParams?: Record<string, boolean>;
109
226
  clearPostFilter?: boolean;
110
227
  text?: string;
228
+ /** True when the user just chose `imageRegion` (crop / detection dot). */
229
+ regionSelectedByUser?: boolean;
111
230
  }) => {
231
+ const searchGeneration = ++findSearchGeneration;
232
+ const isCurrentSearch = () => searchGeneration === findSearchGeneration;
112
233
  const isSmartFiltersEnabled = !!settings.useSmartFilters;
234
+ const isRequestQualityEnabled = !!settings.requestQuality;
113
235
  setIsFindApiLoading(true);
114
236
  // setAlgoliaProducts([]);
115
237
  useRequestStore.getState().setGroundingQuery('');
@@ -134,6 +256,7 @@ export const useImageSearch = () => {
134
256
  errorMessage: null,
135
257
  });
136
258
  }
259
+ setRequestQuality(null);
137
260
  useRequestStore.getState().setGroundingQuery('');
138
261
  useResultStore.getState().setShowingGroundingFilterResult(false);
139
262
  startNewSession();
@@ -182,6 +305,7 @@ export const useImageSearch = () => {
182
305
  canvasImage = await createImage(compressedBase64 || blob);
183
306
 
184
307
  requestImage = await createImage(blob);
308
+ if (!isCurrentSearch()) return;
185
309
 
186
310
  if (!imageRegion) {
187
311
  setRequestImages([canvasImage]);
@@ -191,10 +315,12 @@ export const useImageSearch = () => {
191
315
  if (!imageRegion) {
192
316
  try {
193
317
  let res = await findRegions(requestImage, settings);
318
+ if (!isCurrentSearch()) return;
194
319
  setDetectedRegions(res.regions, 0);
195
320
  region = res.selectedRegion;
196
321
  setRegions([region]);
197
322
  } catch (error) {}
323
+ if (!isCurrentSearch()) return;
198
324
  }
199
325
  }
200
326
 
@@ -214,6 +340,7 @@ export const useImageSearch = () => {
214
340
  ];
215
341
 
216
342
  let smartFilterPromise: Promise<void> | null = null;
343
+ let requestQualityPromise: Promise<string | null> | null = null;
217
344
 
218
345
  if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
219
346
  setSmartFilters({
@@ -229,6 +356,7 @@ export const useImageSearch = () => {
229
356
  const smartFilterImage = cropImage(requestImage, region);
230
357
  smartFilterPromise = generateSmartFilters({ image: smartFilterImage })
231
358
  .then(response => {
359
+ if (!isCurrentSearch()) return;
232
360
  const rawFilters = response?.filters;
233
361
  if (!rawFilters || typeof rawFilters !== 'object') {
234
362
  setSmartFilters({
@@ -277,6 +405,7 @@ export const useImageSearch = () => {
277
405
  });
278
406
  })
279
407
  .catch(error => {
408
+ if (!isCurrentSearch()) return;
280
409
  const timeout = String(error?.message || '').includes(
281
410
  'SMART_FILTER_TIMEOUT',
282
411
  );
@@ -293,6 +422,54 @@ export const useImageSearch = () => {
293
422
  });
294
423
  });
295
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
+ }
296
473
 
297
474
  const usesFindApiPostFilters =
298
475
  !settings.algolia?.enabled && settings.postFilterOption;
@@ -313,6 +490,14 @@ export const useImageSearch = () => {
313
490
  }
314
491
  }
315
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
+
316
501
  try {
317
502
  res = await find({
318
503
  image: requestImage,
@@ -329,13 +514,13 @@ export const useImageSearch = () => {
329
514
  : undefined,
330
515
  region,
331
516
  text:
332
- !window.settings.algolia.enabled && !requestImage
333
- ? isUndefined(text)
334
- ? query
335
- : text
517
+ !requestImage &&
518
+ (hasActiveMachineScope() || !window.settings.algolia.enabled)
519
+ ? keyword
336
520
  : undefined,
337
521
  scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
338
522
  });
523
+ if (!isCurrentSearch()) return;
339
524
  useResultStore.getState().resetGrounding();
340
525
 
341
526
  setFindApiProducts(res?.results);
@@ -372,6 +557,13 @@ export const useImageSearch = () => {
372
557
  setSessionId(res?.session);
373
558
  setRequestId(res?.id);
374
559
 
560
+ if (regionSelectedByUser && imageRegion && res?.id) {
561
+ trackRegion(imageRegion, getRegionSubtype(imageRegion), {
562
+ requestId: res.id,
563
+ sessionId: res.session,
564
+ });
565
+ }
566
+
375
567
  const specification = res?.image_analysis?.specification || {};
376
568
  const hasOcr = Object.keys(specification).length > 0;
377
569
 
@@ -390,6 +582,7 @@ export const useImageSearch = () => {
390
582
  evaluateResults({
391
583
  activeProducts: res?.results || [],
392
584
  imageAnalysis: res?.image_analysis,
585
+ query: !requestImage ? keyword || '' : '',
393
586
  });
394
587
  }
395
588
  const nonEmptyFilter: any[] = ['sku:DOES_NOT_EXIST<score=1> '];
@@ -401,7 +594,9 @@ export const useImageSearch = () => {
401
594
  : '';
402
595
  const filterSkusString = [...nonEmptyFilter, ...filterSkus].join('OR ');
403
596
 
404
- setAlgoliaFilter(filterSkusString);
597
+ if (!hasActiveMachineScope()) {
598
+ setAlgoliaFilter(filterSkusString);
599
+ }
405
600
  setIsFindApiLoading(false);
406
601
 
407
602
  if (showFeedback) {
@@ -417,12 +612,21 @@ export const useImageSearch = () => {
417
612
  setFirstRequestImageAnalysis(res?.image_analysis);
418
613
  }
419
614
  } catch (error) {
420
- setIsFindApiLoading(false);
615
+ if (isCurrentSearch()) {
616
+ setIsFindApiLoading(false);
617
+ }
421
618
  }
422
619
 
423
620
  if (smartFilterPromise) {
424
621
  void smartFilterPromise;
425
622
  }
623
+ if (requestQualityPromise) {
624
+ void requestQualityPromise.then(quality => {
625
+ if (isCurrentSearch()) {
626
+ setRequestQuality(quality);
627
+ }
628
+ });
629
+ }
426
630
 
427
631
  return res;
428
632
  },
@@ -436,7 +640,6 @@ export const useImageSearch = () => {
436
640
  setDetectedRegions,
437
641
  setRegions,
438
642
  metaFilter,
439
- query,
440
643
  setFindApiProducts,
441
644
  setImageAnalysis,
442
645
  setSessionId,
@@ -453,11 +656,86 @@ export const useImageSearch = () => {
453
656
  setFirstSearchPreFilter,
454
657
  setFirstRequestImageAnalysis,
455
658
  setSmartFilters,
659
+ setRequestQuality,
456
660
  setPostFilterFacets,
457
661
  setInitialFindSearchSnapshot,
458
662
  user,
459
663
  ],
460
664
  );
461
665
 
462
- 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 };
463
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
+ }
@@ -47,10 +47,18 @@ export function useResultEvaluator() {
47
47
  async ({
48
48
  imageAnalysis,
49
49
  activeProducts,
50
+ query,
50
51
  }: {
51
52
  imageAnalysis: any;
52
53
  activeProducts: any[];
54
+ query: string;
53
55
  }) => {
56
+ // Bind this run to the current session; a newer search calls
57
+ // startNewSession, which makes any in-flight evaluation stale.
58
+ const runSessionId = useConversationStore.getState().sessionId;
59
+ const isStale = () =>
60
+ useConversationStore.getState().sessionId !== runSessionId;
61
+
54
62
  setIsLoading(true);
55
63
  setError(null);
56
64
  setRelatedPartsTableSuggestions([]);
@@ -207,12 +215,22 @@ export function useResultEvaluator() {
207
215
  }
208
216
  });
209
217
 
218
+ const requestImage = useRequestStore.getState().requestImages;
219
+ const regions = useRequestStore.getState().regions;
220
+ const hasRequestImage = requestImage && requestImage[0];
221
+
222
+ const evaluationTarget = hasRequestImage
223
+ ? 'the image'
224
+ : 'the user query (keyword input)';
225
+
210
226
  const userPrompt = `Evaluate these search results:
211
227
 
212
- IMAGE DESCRIPTION: ${imageDescription}
228
+ user query: ${query || 'N/A'}
229
+
230
+ IMAGE DESCRIPTION: ${hasRequestImage ? imageDescription : 'N/A'}
213
231
 
214
232
  SPECIFICATION (from image analysis):
215
- ${JSON.stringify(p4Specification, null, 2)}
233
+ ${hasRequestImage ? JSON.stringify(p4Specification, null, 2) : 'N/A'}
216
234
 
217
235
  TOP 20 RESULTS WITH METADATA:
218
236
  ${resultsSummary
@@ -227,7 +245,7 @@ ${JSON.stringify(attributeFrequency, null, 2)}
227
245
 
228
246
  TOTAL RESULTS: ${resultsSummary.length}
229
247
 
230
- Evaluate if these results match the image. If they do (same type of product), route to P3 and suggest filter options based on the COMMON ATTRIBUTES found above (like voltage, size, brand). Do NOT suggest the obvious category match.`;
248
+ Evaluate if these results match ${evaluationTarget}. If they do (same type of product), route to P3 and suggest filter options based on the COMMON ATTRIBUTES found above (like voltage, size, brand). Do NOT suggest the obvious category match.`;
231
249
 
232
250
  const productsForImages = activeProducts.slice(0, 30);
233
251
  const imageResults = await Promise.all(
@@ -268,24 +286,27 @@ Evaluate if these results match the image. If they do (same type of product), ro
268
286
  }
269
287
  });
270
288
 
271
- const requestImage = useRequestStore.getState().requestImages;
272
- const regions = useRequestStore.getState().regions;
273
- const croppedCanvas = cropImage(requestImage[0], regions[0]);
274
- const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
289
+ const multimodalInputs: any[] = [
290
+ {
291
+ metadata: userPrompt,
292
+ },
293
+ ];
294
+
295
+ if (hasRequestImage) {
296
+ const croppedCanvas = cropImage(requestImage[0], regions[0]);
297
+ const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
298
+ multimodalInputs.push({
299
+ metadata: 'request_image',
300
+ mimeType: 'image/jpeg',
301
+ image: base64Image,
302
+ });
303
+ }
304
+
305
+ multimodalInputs.push(...productImageParts);
275
306
 
276
307
  const response = await chatAnalysis({
277
308
  promptKey: 'result_evaluator_prompt',
278
- multimodalInputs: [
279
- {
280
- metadata: userPrompt,
281
- },
282
- {
283
- metadata: 'request_image',
284
- mimeType: 'image/jpeg',
285
- image: base64Image,
286
- },
287
- ...productImageParts,
288
- ],
309
+ multimodalInputs,
289
310
  });
290
311
 
291
312
  const evaluation: P2EvaluationResult = response;
@@ -315,6 +336,8 @@ Evaluate if these results match the image. If they do (same type of product), ro
315
336
  // Check lookup table for related parts on fresh image searches and store globally.
316
337
  const table = await loadRelatedPartsTable();
317
338
 
339
+ if (isStale()) return;
340
+
318
341
  const tableSuggestions = lookupRelatedParts(
319
342
  table,
320
343
  imageAnalysis?.specification?.product_category || '',
@@ -371,6 +394,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
371
394
  );
372
395
  } catch (err: any) {
373
396
  console.error('result_evaluator_prompt', err);
397
+ if (isStale()) return;
374
398
  setRelatedPartsTableSuggestions([]);
375
399
  setError(err.message || 'Failed to evaluate results');
376
400
 
@@ -390,7 +414,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
390
414
  ],
391
415
  );
392
416
  } finally {
393
- setIsLoading(false);
417
+ if (!isStale()) setIsLoading(false);
394
418
  }
395
419
  },
396
420
  // eslint-disable-next-line react-hooks/exhaustive-deps
package/src/index.css CHANGED
@@ -283,7 +283,7 @@ h2 {
283
283
 
284
284
  h1 {
285
285
  font-size: 25px;
286
- margin: 0 auto 25px;
286
+ margin: 0 auto;
287
287
  }
288
288
 
289
289
  h1.entry-title a {