@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.
- package/build/asset-manifest.json +7 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +42 -0
- package/build/static/css/main.a7ff7a7f.css +4 -0
- package/build/static/css/main.a7ff7a7f.css.map +1 -0
- package/build/static/js/main.916c157c.js +3 -0
- package/build/static/js/{main.93fdbd6e.js.map → main.916c157c.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 +42 -0
- package/src/common/assets/images/plate_info.png +0 -0
- package/src/components/Cart.tsx +41 -0
- package/src/components/ChatAssistant/ChatAssistant.test.tsx +213 -0
- package/src/components/ChatAssistant/ChatAssistant.tsx +18 -1
- package/src/components/ChatAssistant/MobileChatAssistant.tsx +18 -1
- package/src/components/ChatAssistant/OptionChip.tsx +23 -1
- package/src/components/ChatAssistant/useChatAssistantLogic.ts +35 -14
- 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 +61 -26
- 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/chatProductMetadata.test.ts +307 -0
- package/src/hooks/useBadResultsRecovery.ts +26 -49
- package/src/hooks/useEffectiveGroundingResults.ts +8 -6
- package/src/hooks/useGoodResultsChat.ts +28 -39
- package/src/hooks/useImageSearch.ts +290 -12
- package/src/hooks/useMachinePartsListingPagination.ts +95 -0
- package/src/hooks/useResultEvaluator.ts +63 -73
- 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 +225 -34
- 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/chat/conversationStore.test.ts +133 -0
- package/src/stores/chat/conversationStore.ts +55 -14
- 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 +41 -0
- package/src/utils/chatProductMetadata.test.ts +130 -0
- package/src/utils/chatProductMetadata.ts +93 -0
- package/src/utils/compressImage.ts +3 -3
- 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.916c157c.js.LICENSE.txt} +0 -0
package/src/pages/Result.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import SidePanel from 'components/SidePanel';
|
|
|
11
11
|
import Loading from 'components/Loading';
|
|
12
12
|
|
|
13
13
|
import Feedback from 'components/Feedback';
|
|
14
|
-
import {
|
|
14
|
+
import { trackResultFeedback } from 'services/Feedback';
|
|
15
15
|
import useRequestStore from 'stores/request/requestStore';
|
|
16
16
|
import RfqBanner from 'components/Rfq/RfqBanner';
|
|
17
17
|
import InquiryBanner from 'components/Inquiry/InquiryBanner';
|
|
@@ -22,15 +22,24 @@ import ImagePreview from 'components/ImagePreview';
|
|
|
22
22
|
import PoweredBy from 'components/PoweredBy';
|
|
23
23
|
import CurrentRefinements from 'components/CurrentRefinements';
|
|
24
24
|
import { GoBackButton } from 'components/GoBackButton';
|
|
25
|
+
import { useNavigate } from 'react-router';
|
|
25
26
|
import { useCurrentRefinements } from 'react-instantsearch';
|
|
26
27
|
import CustomCamera from 'components/CustomCameraDrawer';
|
|
27
28
|
import { useTranslation } from 'react-i18next';
|
|
28
29
|
import { useImageSearch } from 'hooks/useImageSearch';
|
|
30
|
+
import { useMachinePartsListingPagination } from 'hooks/useMachinePartsListingPagination';
|
|
29
31
|
import MatchNotificationBanner from 'components/MatchNotificationBanner';
|
|
30
32
|
import ImageUpload from 'components/ImageUpload';
|
|
31
33
|
import SmartFilter from 'components/SmartFilter';
|
|
32
34
|
import { ChatAssistant, MobileChatAssistant } from 'components/ChatAssistant';
|
|
33
35
|
import IdentifiedAttributes from 'components/IdentifiedAttributes';
|
|
36
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
37
|
+
import { TypePlateUpload } from 'components/TypePlateUpload';
|
|
38
|
+
import { ScopeSelectModal } from 'components/ScopeSelect/ScopeSelectModal';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SCOPE_RESULTS_LABEL =
|
|
41
|
+
'Searching within parts for {{scope_id}}. ' +
|
|
42
|
+
'You can search with keyword or image to find the right item';
|
|
34
43
|
|
|
35
44
|
function Results() {
|
|
36
45
|
const settings = window.settings;
|
|
@@ -55,8 +64,10 @@ function Results() {
|
|
|
55
64
|
const showFeedback = useUiStore(state => state.showFeedback);
|
|
56
65
|
const setIsFindApiLoading = useUiStore(state => state.setIsFindApiLoading);
|
|
57
66
|
const isAiModeOpen = useUiStore(state => state.isAiModeOpen);
|
|
67
|
+
const requestQuality = useResultStore(state => state.requestQuality);
|
|
68
|
+
const [isRequestQualityExpanded, setIsRequestQualityExpanded] =
|
|
69
|
+
useState(true);
|
|
58
70
|
|
|
59
|
-
const requestId = useResultStore(state => state.requestId);
|
|
60
71
|
const imageAnalysis = useResultStore(state => state.imageAnalysis);
|
|
61
72
|
|
|
62
73
|
const specificationFilter = useRequestStore(
|
|
@@ -64,13 +75,32 @@ function Results() {
|
|
|
64
75
|
);
|
|
65
76
|
const requestImages = useRequestStore(state => state.requestImages);
|
|
66
77
|
const specifications = useRequestStore(state => state.specifications);
|
|
67
|
-
const query = useRequestStore(state => state.query);
|
|
68
78
|
const regions = useRequestStore(state => state.regions);
|
|
69
|
-
|
|
79
|
+
const query = useRequestStore(state => state.query);
|
|
80
|
+
const scopeItemKeys = useScopeStore(state => state.itemKeys);
|
|
81
|
+
const activeScopeId = useScopeStore(state => state.scopeId);
|
|
82
|
+
const resetScope = useScopeStore(state => state.resetScope);
|
|
83
|
+
const navigate = useNavigate();
|
|
70
84
|
const [showPostFilter, setShowPostFilter] = useState(false);
|
|
71
85
|
const [isOpenModalCamera, setOpenModalCamera] = useState<boolean>(false);
|
|
72
86
|
|
|
73
87
|
const isShowFilter = query || requestImages[0];
|
|
88
|
+
const isScopeFeatureEnabled = Boolean(window.settings?.scopes?.scopeID);
|
|
89
|
+
const isScopeFilterActive =
|
|
90
|
+
isScopeFeatureEnabled && scopeItemKeys.length > 0 && Boolean(activeScopeId);
|
|
91
|
+
const [isTypePlateOpen, setIsTypePlateOpen] = useState(false);
|
|
92
|
+
const [isNoScopeBannerDismissed, setIsNoScopeBannerDismissed] =
|
|
93
|
+
useState<boolean>(false);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (isScopeFilterActive) {
|
|
97
|
+
setIsNoScopeBannerDismissed(false);
|
|
98
|
+
}
|
|
99
|
+
}, [isScopeFilterActive]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setIsNoScopeBannerDismissed(false);
|
|
103
|
+
}, [query, requestImages]);
|
|
74
104
|
|
|
75
105
|
const { items } = useCurrentRefinements();
|
|
76
106
|
|
|
@@ -80,6 +110,46 @@ function Results() {
|
|
|
80
110
|
document.title = 'Search results';
|
|
81
111
|
}, [cadenas?.cadenas3dWebView, cadenas?.cadenasAPIKey]);
|
|
82
112
|
const { singleImageSearch } = useImageSearch();
|
|
113
|
+
const listingPagination = useMachinePartsListingPagination();
|
|
114
|
+
// Scoped keyword/image results come from Find API, which has no paging, so
|
|
115
|
+
// the Algolia page-size control would only change hidden results.
|
|
116
|
+
const isFindApiSearchUnderAlgolia =
|
|
117
|
+
isScopeFilterActive &&
|
|
118
|
+
Boolean(settings.algolia?.enabled) &&
|
|
119
|
+
!listingPagination.isListing;
|
|
120
|
+
|
|
121
|
+
const clearActiveScopeFilter = () => {
|
|
122
|
+
const keyword = (useRequestStore.getState().query || '').trim();
|
|
123
|
+
const searchImage = useRequestStore.getState().requestImages[0];
|
|
124
|
+
const region = useRequestStore.getState().regions[0];
|
|
125
|
+
resetScope();
|
|
126
|
+
if (searchImage) {
|
|
127
|
+
void singleImageSearch({
|
|
128
|
+
image: searchImage,
|
|
129
|
+
imageRegion: region,
|
|
130
|
+
settings,
|
|
131
|
+
showFeedback: false,
|
|
132
|
+
newSearch: false,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (keyword) {
|
|
137
|
+
if (window.settings.algolia?.enabled) {
|
|
138
|
+
useRequestStore.getState().setAlgoliaFilter('');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
void singleImageSearch({
|
|
142
|
+
text: keyword,
|
|
143
|
+
settings,
|
|
144
|
+
showFeedback: false,
|
|
145
|
+
newSearch: false,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
useResultStore.getState().reset();
|
|
150
|
+
useRequestStore.getState().reset();
|
|
151
|
+
navigate('/');
|
|
152
|
+
};
|
|
83
153
|
|
|
84
154
|
const getImageFromUrl = async (
|
|
85
155
|
url: string,
|
|
@@ -134,7 +204,7 @@ function Results() {
|
|
|
134
204
|
}, 3000);
|
|
135
205
|
setFeedbackStatus('submitted');
|
|
136
206
|
setShowFeedback(false);
|
|
137
|
-
|
|
207
|
+
trackResultFeedback(data);
|
|
138
208
|
};
|
|
139
209
|
|
|
140
210
|
useEffect(() => {
|
|
@@ -236,6 +306,33 @@ function Results() {
|
|
|
236
306
|
/>
|
|
237
307
|
</div>
|
|
238
308
|
)}
|
|
309
|
+
{window.settings.requestQuality && requestQuality && (
|
|
310
|
+
<div className="mx-4 rounded-3xl bg-[#F3F4F8] border border-[#e4e3ff] p-3.5 text-[#3B3E5F]">
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
className="w-full flex items-center justify-between gap-2"
|
|
314
|
+
onClick={() =>
|
|
315
|
+
setIsRequestQualityExpanded(prev => !prev)
|
|
316
|
+
}
|
|
317
|
+
>
|
|
318
|
+
<span className="text-[#3B3E5F] font-bold text-sm leading-4">
|
|
319
|
+
Request Image Quality
|
|
320
|
+
</span>
|
|
321
|
+
<Icon
|
|
322
|
+
name="arrow_down"
|
|
323
|
+
className={twMerge(
|
|
324
|
+
'fill-[#3B3E5F] w-3 h-3 shrink-0 transition-transform',
|
|
325
|
+
isRequestQualityExpanded && 'rotate-180',
|
|
326
|
+
)}
|
|
327
|
+
/>
|
|
328
|
+
</button>
|
|
329
|
+
{isRequestQualityExpanded && (
|
|
330
|
+
<div className="text-[#767A9F] text-sm mt-2.5">
|
|
331
|
+
{requestQuality}
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
239
336
|
{window.settings.showImageDetails &&
|
|
240
337
|
(imageAnalysis?.imageDescription ||
|
|
241
338
|
Object.keys(imageAnalysis?.specification || {}).length >
|
|
@@ -354,6 +451,69 @@ function Results() {
|
|
|
354
451
|
<GoBackButton className="col-span-full mb-2 desktop:mb-0 mt-2 desktop:mt-0" />
|
|
355
452
|
<CurrentRefinements className="col-span-full" />
|
|
356
453
|
<MatchNotificationBanner className="col-span-full" />
|
|
454
|
+
{isScopeFilterActive && (
|
|
455
|
+
<div className="col-span-full flex items-center justify-between gap-4 rounded-2xl border border-solid border-theme-primary bg-white px-4 py-[.8rem] text-sm text-primary">
|
|
456
|
+
<p className="min-w-0 flex-1 font-normal text-left">
|
|
457
|
+
{t(
|
|
458
|
+
window.settings.scopes
|
|
459
|
+
?.typePlateResultsScopeLabel ||
|
|
460
|
+
DEFAULT_SCOPE_RESULTS_LABEL,
|
|
461
|
+
{ scope_id: activeScopeId },
|
|
462
|
+
)}
|
|
463
|
+
</p>
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-primary hover:text-black cursor-pointer"
|
|
467
|
+
onClick={clearActiveScopeFilter}
|
|
468
|
+
aria-label={t(
|
|
469
|
+
window.settings.scopes
|
|
470
|
+
?.typePlateResultsClearScopeLabel ||
|
|
471
|
+
'Search all products',
|
|
472
|
+
)}
|
|
473
|
+
title={t(
|
|
474
|
+
window.settings.scopes
|
|
475
|
+
?.typePlateResultsClearScopeLabel ||
|
|
476
|
+
'Search all products',
|
|
477
|
+
)}
|
|
478
|
+
>
|
|
479
|
+
<Icon name="close" className="h-3 w-3" />
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
{isScopeFeatureEnabled &&
|
|
484
|
+
!isScopeFilterActive &&
|
|
485
|
+
!isNoScopeBannerDismissed && (
|
|
486
|
+
<div className="col-span-full flex items-center justify-between gap-4 rounded-2xl border border-solid border-theme-primary bg-white px-4 py-2.5 text-sm text-[#2B2C46]">
|
|
487
|
+
<div className="min-w-0 flex-1 text-left">
|
|
488
|
+
<p className="font-normal text-[#2B2C46]">
|
|
489
|
+
{t(
|
|
490
|
+
window.settings.scopes
|
|
491
|
+
?.typePlateResultsNoScopeLabel ||
|
|
492
|
+
'Scan your type plate to see only parts for your equipment.',
|
|
493
|
+
)}
|
|
494
|
+
</p>
|
|
495
|
+
</div>
|
|
496
|
+
<div className="flex shrink-0 items-center gap-2 desktop:gap-3">
|
|
497
|
+
<TypePlateUpload
|
|
498
|
+
alternativeView
|
|
499
|
+
showModal={false}
|
|
500
|
+
open={isTypePlateOpen}
|
|
501
|
+
onOpenChange={setIsTypePlateOpen}
|
|
502
|
+
/>
|
|
503
|
+
<button
|
|
504
|
+
type="button"
|
|
505
|
+
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-primary hover:text-black cursor-pointer"
|
|
506
|
+
onClick={() =>
|
|
507
|
+
setIsNoScopeBannerDismissed(true)
|
|
508
|
+
}
|
|
509
|
+
aria-label={t('Close')}
|
|
510
|
+
title={t('Close')}
|
|
511
|
+
>
|
|
512
|
+
<Icon name="close" className="h-3 w-3" />
|
|
513
|
+
</button>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
357
517
|
<ProductList />
|
|
358
518
|
</div>
|
|
359
519
|
|
|
@@ -386,15 +546,26 @@ function Results() {
|
|
|
386
546
|
</div>
|
|
387
547
|
)}
|
|
388
548
|
<div>
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
549
|
+
{listingPagination.isListing ? (
|
|
550
|
+
<Pagination
|
|
551
|
+
isLoading={isFindApiLoading || isAlgoliaLoading}
|
|
552
|
+
pageCount={listingPagination.pageCount}
|
|
553
|
+
pageIndex={listingPagination.pageIndex}
|
|
554
|
+
onPageChange={listingPagination.changePage}
|
|
555
|
+
/>
|
|
556
|
+
) : (
|
|
557
|
+
!isScopeFilterActive && (
|
|
558
|
+
<Pagination
|
|
559
|
+
isLoading={isFindApiLoading || isAlgoliaLoading}
|
|
560
|
+
className={
|
|
561
|
+
productsFromAlgolia.length === 0 &&
|
|
562
|
+
!isAlgoliaLoading
|
|
563
|
+
? 'opacity-0 hidden'
|
|
564
|
+
: ''
|
|
565
|
+
}
|
|
566
|
+
/>
|
|
567
|
+
)
|
|
568
|
+
)}
|
|
398
569
|
<div
|
|
399
570
|
style={{
|
|
400
571
|
display: 'flex',
|
|
@@ -442,6 +613,9 @@ function Results() {
|
|
|
442
613
|
{ label: '40', value: 40 },
|
|
443
614
|
{ label: '50', value: 50 },
|
|
444
615
|
]}
|
|
616
|
+
onFindApiPageSizeChange={listingPagination.changePageSize}
|
|
617
|
+
useFindApiPageSize={listingPagination.isListing}
|
|
618
|
+
showPageSize={!isFindApiSearchUnderAlgolia}
|
|
445
619
|
/>
|
|
446
620
|
</div>
|
|
447
621
|
</div>
|
|
@@ -571,6 +745,12 @@ function Results() {
|
|
|
571
745
|
openModal={showPostFilter}
|
|
572
746
|
handleClose={() => setShowPostFilter(false)}
|
|
573
747
|
/>
|
|
748
|
+
{isScopeFeatureEnabled && (
|
|
749
|
+
<ScopeSelectModal
|
|
750
|
+
openModal={isTypePlateOpen}
|
|
751
|
+
handleClose={() => setIsTypePlateOpen(false)}
|
|
752
|
+
/>
|
|
753
|
+
)}
|
|
574
754
|
<CustomCamera
|
|
575
755
|
show={isOpenModalCamera}
|
|
576
756
|
onClose={() => {
|
package/src/services/Feedback.ts
CHANGED
|
@@ -1,74 +1,265 @@
|
|
|
1
1
|
import NyrisAPI, {
|
|
2
|
+
ConversionRevenue,
|
|
2
3
|
FeedbackEventPayload,
|
|
3
4
|
NyrisAPISettings,
|
|
4
5
|
RectCoords,
|
|
5
6
|
} from '@nyris/nyris-api';
|
|
6
7
|
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
8
|
+
import useResultStore from 'stores/result/resultStore';
|
|
9
|
+
import useConversationStore from 'stores/chat/conversationStore';
|
|
10
|
+
import { CONVERSION_SUBTYPE, normalizeSubtype } from './feedbackSubtypes';
|
|
7
11
|
|
|
8
|
-
export
|
|
9
|
-
requestId
|
|
12
|
+
export interface FeedbackIds {
|
|
13
|
+
requestId?: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProductRef {
|
|
18
|
+
sku?: string;
|
|
19
|
+
position?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ProductRefs {
|
|
23
|
+
skus?: (string | undefined)[];
|
|
24
|
+
positions?: (number | undefined)[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type ConversionDetails = ProductRefs & {
|
|
28
|
+
quantity?: number;
|
|
29
|
+
revenue?: ConversionRevenue;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const compactStrings = (values?: (string | undefined)[]) =>
|
|
33
|
+
values?.filter((value): value is string => !!value) ?? [];
|
|
34
|
+
|
|
35
|
+
const compactNumbers = (values?: (number | undefined)[]) =>
|
|
36
|
+
values?.filter(
|
|
37
|
+
(value): value is number => typeof value === 'number' && value >= 0,
|
|
38
|
+
) ?? [];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds the `product_ids` / `positions` part of a click or conversion payload,
|
|
42
|
+
* dropping empty arrays so the payload matches the API examples.
|
|
43
|
+
*/
|
|
44
|
+
const productData = ({ skus, positions }: ProductRefs) => {
|
|
45
|
+
const product_ids = compactStrings(skus);
|
|
46
|
+
const cleanPositions = compactNumbers(positions);
|
|
47
|
+
return {
|
|
48
|
+
...(product_ids.length ? { product_ids } : {}),
|
|
49
|
+
...(cleanPositions.length ? { positions: cleanPositions } : {}),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const resolveIds = (ids?: FeedbackIds): FeedbackIds => {
|
|
54
|
+
if (ids && (ids.requestId || ids.sessionId)) {
|
|
55
|
+
return ids;
|
|
56
|
+
}
|
|
57
|
+
const { requestId, sessionId } = useResultStore.getState();
|
|
58
|
+
return { requestId, sessionId };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* An event is suppressed when its event type OR its subtype appears in
|
|
63
|
+
* `window.settings.disabledFeedbackEvents` (case-insensitive).
|
|
64
|
+
*/
|
|
65
|
+
export const isFeedbackEventEnabled = (event: string, subtype?: string) => {
|
|
66
|
+
const disabled = (window.settings?.disabledFeedbackEvents || [])
|
|
67
|
+
.filter((entry): entry is string => typeof entry === 'string')
|
|
68
|
+
.map(entry => entry.trim().toLowerCase())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
|
|
71
|
+
if (disabled.length === 0) return true;
|
|
72
|
+
if (disabled.includes(event.trim().toLowerCase())) return false;
|
|
73
|
+
|
|
74
|
+
const normalized = normalizeSubtype(subtype);
|
|
75
|
+
return !(normalized && disabled.includes(normalized));
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Central dispatcher: resolves request/session ids from the result store when
|
|
80
|
+
* not provided, applies the disabled-events setting, normalizes the subtype,
|
|
81
|
+
* and sends the event.
|
|
82
|
+
*/
|
|
83
|
+
export const trackFeedbackEvent = async (
|
|
84
|
+
payload: FeedbackEventPayload,
|
|
85
|
+
ids?: FeedbackIds,
|
|
86
|
+
) => {
|
|
87
|
+
const data = payload.data as { subtype?: string } | undefined;
|
|
88
|
+
const subtype = normalizeSubtype(data?.subtype);
|
|
89
|
+
|
|
90
|
+
if (!isFeedbackEventEnabled(payload.event, subtype)) return;
|
|
91
|
+
|
|
92
|
+
const normalizedPayload = (
|
|
93
|
+
data
|
|
94
|
+
? {
|
|
95
|
+
...payload,
|
|
96
|
+
data: subtype ? { ...data, subtype } : omitSubtype(data),
|
|
97
|
+
}
|
|
98
|
+
: payload
|
|
99
|
+
) as FeedbackEventPayload;
|
|
100
|
+
|
|
101
|
+
const { requestId, sessionId } = resolveIds(ids);
|
|
102
|
+
return sendFeedbackByApi(
|
|
103
|
+
window.settings,
|
|
104
|
+
sessionId,
|
|
105
|
+
requestId,
|
|
106
|
+
normalizedPayload,
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const omitSubtype = <T extends { subtype?: string }>(data: T) => {
|
|
111
|
+
const { subtype: _ignored, ...rest } = data;
|
|
112
|
+
return rest;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** `click` event for a single result or a product-less interaction. */
|
|
116
|
+
export const trackClick = (
|
|
117
|
+
subtype: string,
|
|
118
|
+
product?: ProductRef,
|
|
119
|
+
ids?: FeedbackIds,
|
|
120
|
+
) =>
|
|
121
|
+
trackFeedbackEvent(
|
|
122
|
+
{
|
|
123
|
+
event: 'click',
|
|
124
|
+
data: {
|
|
125
|
+
subtype,
|
|
126
|
+
...productData({
|
|
127
|
+
skus: [product?.sku],
|
|
128
|
+
positions: [product?.position],
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
ids,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
/** `conversion` event. Pass `undefined` as subtype for a legacy conversion. */
|
|
136
|
+
export const trackConversion = (
|
|
137
|
+
subtype: string | undefined,
|
|
138
|
+
details: ConversionDetails = {},
|
|
139
|
+
ids?: FeedbackIds,
|
|
140
|
+
) => {
|
|
141
|
+
const { quantity, revenue, ...refs } = details;
|
|
142
|
+
return trackFeedbackEvent(
|
|
143
|
+
{
|
|
144
|
+
event: 'conversion',
|
|
145
|
+
data: {
|
|
146
|
+
...(subtype ? { subtype } : {}),
|
|
147
|
+
...productData(refs),
|
|
148
|
+
...(typeof quantity === 'number' ? { quantity } : {}),
|
|
149
|
+
...(revenue ? { revenue } : {}),
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
ids,
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `conversion` / `purchase`. There is no checkout flow in the search suite
|
|
158
|
+
* today, so this is provided for host integrations and future use.
|
|
159
|
+
*/
|
|
160
|
+
export const trackPurchase = (details: ConversionDetails, ids?: FeedbackIds) =>
|
|
161
|
+
trackConversion(CONVERSION_SUBTYPE.PURCHASE, details, ids);
|
|
162
|
+
|
|
163
|
+
/** `feedback` event from the "Did we find what you were looking for?" prompt. */
|
|
164
|
+
export const trackResultFeedback = (
|
|
10
165
|
success: boolean,
|
|
166
|
+
comment?: string,
|
|
167
|
+
ids?: FeedbackIds,
|
|
11
168
|
) => {
|
|
12
|
-
|
|
169
|
+
useConversationStore.getState().setSearchResolved(success);
|
|
170
|
+
return trackFeedbackEvent(
|
|
171
|
+
{
|
|
172
|
+
event: 'feedback',
|
|
173
|
+
data: {
|
|
174
|
+
success,
|
|
175
|
+
...(comment ? { comment } : {}),
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
ids,
|
|
179
|
+
);
|
|
180
|
+
};
|
|
13
181
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
182
|
+
/** `region` event; converts x1/y1/x2/y2 to the API's x/y/w/h rectangle. */
|
|
183
|
+
export const trackRegion = (
|
|
184
|
+
region: RectCoords,
|
|
185
|
+
subtype: string,
|
|
186
|
+
ids?: FeedbackIds,
|
|
187
|
+
) => {
|
|
188
|
+
const { x1, x2, y1, y2 } = region;
|
|
189
|
+
return trackFeedbackEvent(
|
|
190
|
+
{
|
|
191
|
+
event: 'region',
|
|
192
|
+
data: { subtype, rect: { x: x1, y: y1, w: x2 - x1, h: y2 - y1 } },
|
|
193
|
+
},
|
|
194
|
+
ids,
|
|
195
|
+
);
|
|
18
196
|
};
|
|
19
197
|
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Legacy wrappers kept for backwards compatibility.
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
export const feedbackSuccessEpic = async (
|
|
203
|
+
requestId: string,
|
|
204
|
+
success: boolean,
|
|
205
|
+
) => trackResultFeedback(success, undefined, { requestId });
|
|
206
|
+
|
|
20
207
|
export const feedbackClickEpic = async (
|
|
21
208
|
requestId: string,
|
|
22
209
|
sessionId: string,
|
|
23
210
|
position: number,
|
|
24
211
|
id?: string,
|
|
25
|
-
) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
212
|
+
) =>
|
|
213
|
+
trackFeedbackEvent(
|
|
214
|
+
{
|
|
215
|
+
event: 'click',
|
|
216
|
+
data: { positions: [position], ...(id ? { product_ids: [id] } : {}) },
|
|
217
|
+
},
|
|
218
|
+
{ requestId, sessionId },
|
|
219
|
+
);
|
|
32
220
|
|
|
33
221
|
export const feedbackConversionEpic = async (
|
|
34
222
|
requestId: string,
|
|
35
223
|
sessionId: string,
|
|
36
224
|
position: number,
|
|
37
225
|
id?: string,
|
|
38
|
-
) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
226
|
+
) =>
|
|
227
|
+
trackConversion(
|
|
228
|
+
undefined,
|
|
229
|
+
{ skus: [id], positions: [position] },
|
|
230
|
+
{
|
|
231
|
+
requestId,
|
|
232
|
+
sessionId,
|
|
233
|
+
},
|
|
234
|
+
);
|
|
45
235
|
|
|
46
236
|
export const feedbackRegionEpic = async (
|
|
47
237
|
requestId: string,
|
|
48
238
|
sessionId: string,
|
|
49
239
|
region: RectCoords,
|
|
50
240
|
) => {
|
|
51
|
-
const settings = window.settings;
|
|
52
241
|
const { x1, x2, y1, y2 } = region;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
242
|
+
return trackFeedbackEvent(
|
|
243
|
+
{
|
|
244
|
+
event: 'region',
|
|
245
|
+
data: { rect: { x: x1, y: y1, w: x2 - x1, h: y2 - y1 } },
|
|
246
|
+
},
|
|
247
|
+
{ requestId, sessionId },
|
|
248
|
+
);
|
|
58
249
|
};
|
|
59
250
|
|
|
251
|
+
/** Transport. Silently skips when there is no request id to attach to. */
|
|
60
252
|
export const sendFeedbackByApi = async (
|
|
61
253
|
settings: NyrisAPISettings,
|
|
62
254
|
sessionId: string | undefined,
|
|
63
255
|
requestId: string | undefined,
|
|
64
256
|
payload: FeedbackEventPayload,
|
|
65
257
|
) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
258
|
+
if (!requestId || !window.settings.showFeedback) return;
|
|
259
|
+
try {
|
|
260
|
+
const api = new NyrisAPI(getNyrisSettings(settings));
|
|
261
|
+
await api.sendFeedback({ sessionId, requestId, payload });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.log('error sendFeedbackByApi', error);
|
|
73
264
|
}
|
|
74
265
|
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommended subtype vocabulary for Feedback API events.
|
|
3
|
+
*
|
|
4
|
+
* Subtypes are free strings on the API side, so customers can add their own,
|
|
5
|
+
* but these constants are the default vocabulary the search suite converges on.
|
|
6
|
+
* Casing is normalized (trimmed + lowercased) before sending; the API does the
|
|
7
|
+
* same at ingestion.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const FEEDBACK_EVENT = {
|
|
11
|
+
CLICK: 'click',
|
|
12
|
+
CONVERSION: 'conversion',
|
|
13
|
+
FEEDBACK: 'feedback',
|
|
14
|
+
REGION: 'region',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/** Interaction actions on the result set. */
|
|
18
|
+
export const CLICK_SUBTYPE = {
|
|
19
|
+
RESULT_CARD: 'result_card',
|
|
20
|
+
EXPAND_DETAILS: 'expand_details',
|
|
21
|
+
SIMILAR_PART_SEARCH: 'similar_part_search',
|
|
22
|
+
COPY_DETAILS: 'copy_details',
|
|
23
|
+
RAISE_INQUIRY: 'raise_inquiry',
|
|
24
|
+
FILTER_APPLIED: 'filter_applied',
|
|
25
|
+
PRE_FILTER_APPLIED: 'pre_filter_applied',
|
|
26
|
+
CHAT_WITH_VIZO: 'chat_with_vizo',
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
/** Actions that create business value. Omit subtype for legacy conversions. */
|
|
30
|
+
export const CONVERSION_SUBTYPE = {
|
|
31
|
+
ADD_TO_CART: 'add_to_cart',
|
|
32
|
+
PURCHASE: 'purchase',
|
|
33
|
+
DOWNLOAD_CART: 'download_cart',
|
|
34
|
+
SHARE_CART: 'share_cart',
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** Explicit result-quality feedback. */
|
|
38
|
+
export const FEEDBACK_SUBTYPE = {
|
|
39
|
+
THUMBS_UP: 'thumbs_up',
|
|
40
|
+
THUMBS_DOWN: 'thumbs_down',
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
/** How the searched image region was selected. */
|
|
44
|
+
export const REGION_SUBTYPE = {
|
|
45
|
+
CROP: 'crop',
|
|
46
|
+
OBJECT_DETECTION_CLICK: 'object_detection_click',
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export type FeedbackEventType =
|
|
50
|
+
(typeof FEEDBACK_EVENT)[keyof typeof FEEDBACK_EVENT];
|
|
51
|
+
export type ClickSubtype = (typeof CLICK_SUBTYPE)[keyof typeof CLICK_SUBTYPE];
|
|
52
|
+
export type ConversionSubtype =
|
|
53
|
+
(typeof CONVERSION_SUBTYPE)[keyof typeof CONVERSION_SUBTYPE];
|
|
54
|
+
export type FeedbackSubtype =
|
|
55
|
+
(typeof FEEDBACK_SUBTYPE)[keyof typeof FEEDBACK_SUBTYPE];
|
|
56
|
+
export type RegionSubtype =
|
|
57
|
+
(typeof REGION_SUBTYPE)[keyof typeof REGION_SUBTYPE];
|
|
58
|
+
|
|
59
|
+
/** Normalizes a subtype the same way the API does at ingestion. */
|
|
60
|
+
export const normalizeSubtype = (subtype?: string): string | undefined => {
|
|
61
|
+
const normalized = subtype?.trim().toLowerCase();
|
|
62
|
+
return normalized ? normalized : undefined;
|
|
63
|
+
};
|
package/src/services/filter.ts
CHANGED
|
@@ -33,6 +33,6 @@ export const getScopes = async (
|
|
|
33
33
|
scopeIds: string,
|
|
34
34
|
settings: NyrisAPISettings,
|
|
35
35
|
): Promise<ScopeResponse> => {
|
|
36
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
36
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
37
37
|
return nyrisApi.getScopes(scopeIds);
|
|
38
38
|
};
|