@nyris/nyris-webapp 0.3.98 → 0.4.0

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 (97) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +42 -0
  4. package/build/static/css/main.a7ff7a7f.css +4 -0
  5. package/build/static/css/main.a7ff7a7f.css.map +1 -0
  6. package/build/static/js/main.916c157c.js +3 -0
  7. package/build/static/js/{main.93fdbd6e.js.map → main.916c157c.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 +42 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/Cart.tsx +41 -0
  13. package/src/components/ChatAssistant/ChatAssistant.test.tsx +213 -0
  14. package/src/components/ChatAssistant/ChatAssistant.tsx +18 -1
  15. package/src/components/ChatAssistant/MobileChatAssistant.tsx +18 -1
  16. package/src/components/ChatAssistant/OptionChip.tsx +23 -1
  17. package/src/components/ChatAssistant/useChatAssistantLogic.ts +35 -14
  18. package/src/components/GoBackButton.tsx +29 -2
  19. package/src/components/Header.tsx +3 -0
  20. package/src/components/HitsPerPage.tsx +55 -18
  21. package/src/components/ImagePreview.tsx +31 -1
  22. package/src/components/ImageUpload.tsx +12 -1
  23. package/src/components/Inquiry/InquiryModal.tsx +61 -26
  24. package/src/components/Pagination.tsx +33 -6
  25. package/src/components/PostFilter/PostFilter.tsx +8 -1
  26. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  27. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  28. package/src/components/PreFilter/PreFilter.tsx +10 -2
  29. package/src/components/Product/Product.tsx +60 -10
  30. package/src/components/Product/ProductDetailView.tsx +23 -1
  31. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  32. package/src/components/Product/ProductList.tsx +51 -18
  33. package/src/components/Rfq/RfqModal.tsx +3 -0
  34. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  35. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  36. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  37. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  38. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  39. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  41. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  43. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  44. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  45. package/src/components/SidePanel.tsx +33 -0
  46. package/src/components/TextSearch.tsx +77 -11
  47. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  48. package/src/components/TypePlateUpload.tsx +132 -0
  49. package/src/hooks/chatProductMetadata.test.ts +307 -0
  50. package/src/hooks/useBadResultsRecovery.ts +26 -49
  51. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  52. package/src/hooks/useGoodResultsChat.ts +28 -39
  53. package/src/hooks/useImageSearch.ts +290 -12
  54. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  55. package/src/hooks/useResultEvaluator.ts +63 -73
  56. package/src/index.css +1 -1
  57. package/src/layouts/AppLayout.tsx +43 -3
  58. package/src/pages/Home.tsx +60 -14
  59. package/src/pages/Result.tsx +194 -14
  60. package/src/services/Feedback.ts +225 -34
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +1 -1
  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 +71 -3
  73. package/src/services/vizo.ts +4 -3
  74. package/src/stores/chat/conversationStore.test.ts +133 -0
  75. package/src/stores/chat/conversationStore.ts +55 -14
  76. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  77. package/src/stores/request/filter/filter.initialState.ts +1 -0
  78. package/src/stores/request/filter/filter.slice.ts +2 -0
  79. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  80. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  81. package/src/stores/scope/scopeStore.ts +67 -4
  82. package/src/stores/types.ts +19 -14
  83. package/src/translations.ts +213 -0
  84. package/src/types.ts +41 -0
  85. package/src/utils/chatProductMetadata.test.ts +130 -0
  86. package/src/utils/chatProductMetadata.ts +93 -0
  87. package/src/utils/compressImage.ts +3 -3
  88. package/src/utils/fetchProductImage.ts +1 -0
  89. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  90. package/src/utils/productCardImageUrl.test.ts +41 -0
  91. package/src/utils/productCardImageUrl.ts +49 -0
  92. package/src/utils/textSearchFilter.test.ts +41 -0
  93. package/src/utils/textSearchFilter.ts +13 -6
  94. package/build/static/css/main.81216c5d.css +0 -4
  95. package/build/static/css/main.81216c5d.css.map +0 -1
  96. package/build/static/js/main.93fdbd6e.js +0 -3
  97. /package/build/static/js/{main.93fdbd6e.js.LICENSE.txt → main.916c157c.js.LICENSE.txt} +0 -0
@@ -7,18 +7,20 @@
7
7
  */
8
8
 
9
9
  import { useCallback, useRef } from 'react';
10
- import useConversationStore, {
11
- QuickReplyOption,
12
- ActionType,
13
- } from 'stores/chat/conversationStore';
10
+ import useConversationStore from 'stores/chat/conversationStore';
14
11
  import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
15
12
  import useResultStore from 'stores/result/resultStore';
16
13
  import useRequestStore from 'stores/request/requestStore';
17
14
  import { chatAnalysis } from 'services/vizo';
15
+ import { getChatProductMetadata } from 'utils/chatProductMetadata';
18
16
  import {
19
17
  fetchProductImageBase64,
20
18
  getProductImageUrl,
21
19
  } from 'utils/fetchProductImage';
20
+ import {
21
+ normalizeChatAssistantResponse,
22
+ P3Response,
23
+ } from 'utils/normalizeChatAssistantResponse';
22
24
 
23
25
  /**
24
26
  * Extracts search keywords from a user's natural language description.
@@ -138,18 +140,6 @@ function extractSearchKeywords(description: string): string {
138
140
  );
139
141
  }
140
142
 
141
- interface P3Response {
142
- message: string;
143
- action: {
144
- type: ActionType;
145
- key?: string;
146
- value?: string;
147
- matched_ids?: string[];
148
- matched_indices?: number[]; // Legacy fallback
149
- };
150
- options: QuickReplyOption[];
151
- }
152
-
153
143
  export function useGoodResultsChat() {
154
144
  const {
155
145
  messages,
@@ -245,7 +235,7 @@ export function useGoodResultsChat() {
245
235
  id: p.sku || p.objectID || `product-${i}`,
246
236
  title: title || 'Untitled',
247
237
  brand: p.brand || '',
248
- description: (p.description || '').substring(0, 200),
238
+ description: p.description || '',
249
239
  mpn: p.customIds?.mpn || p.ids?.mpn || '',
250
240
  };
251
241
  });
@@ -278,7 +268,7 @@ export function useGoodResultsChat() {
278
268
  content: m.content,
279
269
  }));
280
270
 
281
- // Fetch product thumbnail images with caching — send images for ALL results (up to 50)
271
+ // Fetch product thumbnail images with caching — send images for ALL results (up to 30)
282
272
  // Cache persists across filter operations; only clears on new search session
283
273
  const productsForImages = poolForAI.slice(0, 30);
284
274
  const currentSessionId = useConversationStore.getState().sessionId;
@@ -328,19 +318,18 @@ ${JSON.stringify(attributeFrequency, null, 2)}
328
318
 
329
319
  ${visualNotice}
330
320
 
331
- PRODUCT LIST (up to 50 results — each has a unique ID in [brackets] and AI-generated TAGS for type/color/shape/material. Use TAGS for filtering — they are pre-analyzed from product images. Return matching IDs in matched_ids array):
321
+ PRODUCT LIST (up to 30 results — each JSON entry has a unique id and complete product metadata. When present, metadata._tags contains AI-generated type/color/shape/material tags pre-analyzed from product images. Use these tags for filtering. Return matching id values in matched_ids array):
332
322
  `,
333
323
  });
334
324
 
335
- // Part 2: Product entries with interleaved images (for all results up to 50)
325
+ // Part 2: Product entries with interleaved images (for all results up to 30)
336
326
  visibleResults.forEach((r, i) => {
337
- // Include AI-generated tags if available (from useProductTagger)
338
- const originalProduct = poolForAI[i];
339
- const tags = originalProduct?._tags;
340
- const tagStr = tags
341
- ? ` | TAGS: type=${tags.productType}, color=${tags.color}, shape=${tags.shape}, material=${tags.material}`
342
- : '';
343
- const metaLine = `id: ${r.id} | ${r.title} | brand: ${r.brand} | mpn: ${r.mpn} | desc: ${r.description}${tagStr}`;
327
+ const metaLine = JSON.stringify(
328
+ getChatProductMetadata({
329
+ id: r.id,
330
+ metadata: getChatProductMetadata(poolForAI[i]),
331
+ }),
332
+ );
344
333
 
345
334
  // Include product image if available
346
335
  if (
@@ -374,7 +363,7 @@ USER MESSAGE: ${userMessage}
374
363
  Respond to the user's message with an appropriate action and follow-up options.`,
375
364
  });
376
365
 
377
- const response: P3Response = await chatAnalysis({
366
+ const rawResponse = await chatAnalysis({
378
367
  promptKey: 'chat_assistant_prompt',
379
368
  multimodalInputs: userParts.map(part => ({
380
369
  metadata: part.text,
@@ -382,11 +371,8 @@ Respond to the user's message with an appropriate action and follow-up options.`
382
371
  mimeType: part.inlineData?.mimeType,
383
372
  })),
384
373
  });
385
-
386
- const responseText = response;
387
-
388
- // Parse the JSON response
389
- const p3Response: P3Response = responseText;
374
+ const p3Response: P3Response =
375
+ normalizeChatAssistantResponse(rawResponse);
390
376
 
391
377
  // Silent mode: only suggest filters, never apply actions
392
378
  if (options?.silent && p3Response.action) {
@@ -601,12 +587,15 @@ Respond to the user's message with an appropriate action and follow-up options.`
601
587
  }
602
588
 
603
589
  // Add assistant message
604
- addMessage({
605
- role: 'assistant',
606
- content: finalMessage,
607
- action: p3Response.action,
608
- options: finalOptions,
609
- });
590
+ addMessage(
591
+ {
592
+ role: 'assistant',
593
+ content: finalMessage,
594
+ action: p3Response.action,
595
+ options: finalOptions,
596
+ },
597
+ { isApiResponse: true },
598
+ );
610
599
  } catch (err: any) {
611
600
  console.error('[P3] Error:', err);
612
601
  setError(err.message || 'Failed to process message');
@@ -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
+ }