@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.
- package/build/asset-manifest.json +7 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +40 -0
- package/build/static/css/main.5cb72c09.css +4 -0
- package/build/static/css/main.5cb72c09.css.map +1 -0
- package/build/static/js/main.2339271b.js +3 -0
- package/build/static/js/{main.93fdbd6e.js.map → main.2339271b.js.map} +1 -1
- package/build/static/media/plate_info.6beaea36d35053be3a45.png +0 -0
- package/package.json +3 -3
- package/public/js/settings.example.js +40 -0
- package/src/common/assets/images/plate_info.png +0 -0
- package/src/components/Cart.tsx +41 -0
- package/src/components/GoBackButton.tsx +29 -2
- package/src/components/Header.tsx +3 -0
- package/src/components/HitsPerPage.tsx +55 -18
- package/src/components/ImagePreview.tsx +31 -1
- package/src/components/ImageUpload.tsx +12 -1
- package/src/components/Inquiry/InquiryModal.tsx +3 -0
- package/src/components/Pagination.tsx +33 -6
- package/src/components/PostFilter/PostFilter.tsx +8 -1
- package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
- package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
- package/src/components/PreFilter/PreFilter.tsx +10 -2
- package/src/components/Product/Product.tsx +60 -10
- package/src/components/Product/ProductDetailView.tsx +23 -1
- package/src/components/Product/ProductDetailViewModal.tsx +5 -1
- package/src/components/Product/ProductList.tsx +51 -18
- package/src/components/Rfq/RfqModal.tsx +3 -0
- package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
- package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
- package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
- package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
- package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
- package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
- package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
- package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
- package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
- package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
- package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
- package/src/components/SidePanel.tsx +33 -0
- package/src/components/TextSearch.tsx +77 -11
- package/src/components/Tooltip/TooltipComponent.tsx +12 -4
- package/src/components/TypePlateUpload.tsx +132 -0
- package/src/hooks/useEffectiveGroundingResults.ts +8 -6
- package/src/hooks/useGoodResultsChat.ts +8 -22
- package/src/hooks/useImageSearch.ts +290 -12
- package/src/hooks/useMachinePartsListingPagination.ts +95 -0
- package/src/hooks/useResultEvaluator.ts +43 -19
- package/src/index.css +1 -1
- package/src/layouts/AppLayout.tsx +43 -3
- package/src/pages/Home.tsx +60 -14
- package/src/pages/Result.tsx +194 -14
- package/src/services/Feedback.ts +223 -35
- package/src/services/feedbackSubtypes.ts +63 -0
- package/src/services/filter.ts +1 -1
- package/src/services/initialMachinePartsText.payload.test.ts +61 -0
- package/src/services/initialMachinePartsText.test.ts +33 -0
- package/src/services/initialMachinePartsText.ts +11 -0
- package/src/services/machinePartsPage.test.ts +74 -0
- package/src/services/machinePartsPage.ts +120 -0
- package/src/services/normalizeChatResponse.ts +26 -0
- package/src/services/scopeLookup.test.ts +67 -0
- package/src/services/scopeLookup.ts +39 -0
- package/src/services/visualSearch.listingOptions.test.ts +79 -0
- package/src/services/visualSearch.ts +71 -3
- package/src/services/vizo.ts +4 -3
- package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
- package/src/stores/request/filter/filter.initialState.ts +1 -0
- package/src/stores/request/filter/filter.slice.ts +2 -0
- package/src/stores/result/prodcuts/products.initialState.ts +1 -0
- package/src/stores/result/prodcuts/products.slice.ts +4 -0
- package/src/stores/scope/scopeStore.ts +67 -4
- package/src/stores/types.ts +19 -14
- package/src/translations.ts +213 -0
- package/src/types.ts +40 -0
- package/src/utils/fetchProductImage.ts +1 -0
- package/src/utils/normalizeChatAssistantResponse.ts +227 -0
- package/src/utils/productCardImageUrl.test.ts +41 -0
- package/src/utils/productCardImageUrl.ts +49 -0
- package/src/utils/textSearchFilter.test.ts +41 -0
- package/src/utils/textSearchFilter.ts +13 -6
- package/build/static/css/main.81216c5d.css +0 -4
- package/build/static/css/main.81216c5d.css.map +0 -1
- package/build/static/js/main.93fdbd6e.js +0 -3
- /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 {
|
|
22
|
-
import {
|
|
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
|
-
!
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|