@nyris/nyris-webapp 0.3.97 → 0.3.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +41 -0
  4. package/build/static/css/main.5cb72c09.css +4 -0
  5. package/build/static/css/main.5cb72c09.css.map +1 -0
  6. package/build/static/js/main.2339271b.js +3 -0
  7. package/build/static/js/{main.9e7fb38a.js.map → main.2339271b.js.map} +1 -1
  8. package/build/static/media/plate_info.6beaea36d35053be3a45.png +0 -0
  9. package/package.json +3 -3
  10. package/public/js/settings.example.js +41 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/AuthApiKeySync.tsx +34 -0
  13. package/src/components/Cart.tsx +41 -0
  14. package/src/components/ChatAssistant/useChatAssistantLogic.ts +3 -2
  15. package/src/components/GoBackButton.tsx +29 -2
  16. package/src/components/Header.tsx +3 -0
  17. package/src/components/HitsPerPage.tsx +55 -18
  18. package/src/components/ImagePreview.tsx +31 -1
  19. package/src/components/ImageUpload.tsx +14 -3
  20. package/src/components/Inquiry/InquiryModal.tsx +3 -0
  21. package/src/components/LogoutModal.tsx +2 -0
  22. package/src/components/Pagination.tsx +33 -6
  23. package/src/components/PostFilter/PostFilter.tsx +8 -1
  24. package/src/components/PostFilter/PostFilterComponent.tsx +8 -22
  25. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  26. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  27. package/src/components/PreFilter/PreFilter.tsx +10 -2
  28. package/src/components/Product/Product.tsx +60 -10
  29. package/src/components/Product/ProductDetailView.tsx +23 -1
  30. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  31. package/src/components/Product/ProductList.tsx +51 -16
  32. package/src/components/Rfq/RfqModal.tsx +3 -0
  33. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  34. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  35. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  36. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  37. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  38. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  39. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  41. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  43. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  44. package/src/components/SidePanel.tsx +43 -0
  45. package/src/components/TextSearch.tsx +80 -14
  46. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  47. package/src/components/TypePlateUpload.tsx +132 -0
  48. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  49. package/src/hooks/useGoodResultsChat.ts +8 -22
  50. package/src/hooks/useImageSearch.ts +325 -20
  51. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  52. package/src/hooks/useResultEvaluator.ts +43 -19
  53. package/src/index.css +1 -1
  54. package/src/index.tsx +4 -0
  55. package/src/layouts/AppLayout.tsx +95 -9
  56. package/src/pages/Home.tsx +60 -14
  57. package/src/pages/Logout.tsx +2 -0
  58. package/src/pages/Result.tsx +206 -20
  59. package/src/providers/AuthProvider.tsx +5 -0
  60. package/src/services/Feedback.ts +224 -35
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +4 -3
  63. package/src/services/initialMachinePartsText.payload.test.ts +61 -0
  64. package/src/services/initialMachinePartsText.test.ts +33 -0
  65. package/src/services/initialMachinePartsText.ts +11 -0
  66. package/src/services/machinePartsPage.test.ts +74 -0
  67. package/src/services/machinePartsPage.ts +120 -0
  68. package/src/services/normalizeChatResponse.ts +26 -0
  69. package/src/services/scopeLookup.test.ts +67 -0
  70. package/src/services/scopeLookup.ts +39 -0
  71. package/src/services/visualSearch.listingOptions.test.ts +79 -0
  72. package/src/services/visualSearch.ts +76 -7
  73. package/src/services/vizo.ts +17 -5
  74. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  75. package/src/stores/request/filter/filter.initialState.ts +1 -0
  76. package/src/stores/request/filter/filter.slice.ts +2 -0
  77. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  78. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  79. package/src/stores/scope/scopeStore.ts +67 -4
  80. package/src/stores/types.ts +19 -14
  81. package/src/translations.ts +213 -0
  82. package/src/types.ts +40 -0
  83. package/src/utils/derivePostFilterFacets.ts +68 -0
  84. package/src/utils/fetchProductImage.ts +1 -0
  85. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  86. package/src/utils/nyrisApiKey.ts +115 -0
  87. package/src/utils/productCardImageUrl.test.ts +41 -0
  88. package/src/utils/productCardImageUrl.ts +49 -0
  89. package/src/utils/textSearchFilter.test.ts +41 -0
  90. package/src/utils/textSearchFilter.ts +13 -6
  91. package/build/static/css/main.7c7f5447.css +0 -4
  92. package/build/static/css/main.7c7f5447.css.map +0 -1
  93. package/build/static/js/main.9e7fb38a.js +0 -3
  94. /package/build/static/js/{main.9e7fb38a.js.LICENSE.txt → main.2339271b.js.LICENSE.txt} +0 -0
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nyris/nyris-webapp",
3
- "version": "0.3.97",
3
+ "version": "0.3.99",
4
4
  "dependencies": {
5
5
  "@auth0/auth0-react": "^2.2.4",
6
6
  "@emailjs/browser": "^4.3.3",
7
7
  "@google/genai": "^1.46.0",
8
- "@nyris/nyris-api": "^0.3.97",
9
- "@nyris/nyris-react-components": "^0.3.97",
8
+ "@nyris/nyris-api": "^0.3.99",
9
+ "@nyris/nyris-react-components": "^0.3.99",
10
10
  "@radix-ui/react-accordion": "^1.2.2",
11
11
  "@radix-ui/react-dialog": "^1.1.4",
12
12
  "@radix-ui/react-popover": "^1.1.4",
@@ -23,6 +23,7 @@ var settings = {
23
23
  },
24
24
  ],
25
25
  // Nyris - visual search
26
+ // When auth0.enabled is true, leave apiKey empty — the app uses Auth0 claim
26
27
  apiKey: '',
27
28
  aiApiKey: '',
28
29
  baseUrl: 'https://api.nyris.io',
@@ -104,6 +105,7 @@ var settings = {
104
105
  },
105
106
  showPoweredByNyris: '',
106
107
  postFilterOption: '',
108
+ postFilterUseApi: false,
107
109
  preFilterOption: '',
108
110
  experienceVisualSearch: false,
109
111
  experienceVisualSearchImages: [],
@@ -111,6 +113,45 @@ var settings = {
111
113
  noSimilarSearch: false,
112
114
  useSmartFilters: false,
113
115
  showFeedback: false,
116
+ // Feedback API events that should never be sent. Use event types
117
+ // ('click', 'conversion', 'feedback', 'region') or subtypes:
118
+ // click: result_card, expand_details, similar_part_search, copy_details,
119
+ // raise_inquiry, filter_applied, pre_filter_applied, chat_with_vizo
120
+ // conversion: add_to_cart, purchase, download_cart, share_cart
121
+ // feedback: thumbs_up, thumbs_down
122
+ // region: crop, object_detection_click
123
+ // e.g. ['copy_details', 'filter_applied']
124
+ disabledFeedbackEvents: [],
125
+ requestQuality: false,
126
+ scopes: {
127
+ scopeID: false,
128
+ typePlateButtonLabel: 'Scan type plate',
129
+ typePlateStepOne: 'Identify machine',
130
+ typePlateStepTwo: 'Find part',
131
+ typePlatePopupTitle: 'Identify your machine',
132
+ typePlatePopupTitleSuccess: 'Machine identified',
133
+ typePlatePopupCtaButton: 'Search within this machine',
134
+ typePlatePopupCtaTextButton: 'Enter serial number manually',
135
+ typePlatePopupFindMachineButton: 'Find machine',
136
+ typePlatePopupSkipButton: 'Skip and search all parts',
137
+ typePlatePopupBackButton: 'Back to results',
138
+ typePlatePopupUploadButton: 'Upload image',
139
+ typePlatePopupUploadNewButton: 'Upload new type plate',
140
+ typePlatePopupTakePhotoButton: 'Take a photo',
141
+ typePlatePopupApplyingLabel: 'Applying...',
142
+ typePlateResultsScopeLabel:
143
+ 'Searching within parts for {{scope_id}}. You can search with keyword or image to find the right item',
144
+ typePlateResultsNoScopeLabel:
145
+ 'Scan your type plate to see only parts for your equipment.',
146
+ typePlateResultsClearScopeLabel: 'Search all products',
147
+ typePlatePopupErrorMessage: 'The serial number was not found. Please check again.',
148
+ typePlateManualInputLabel: "Can't scan? Enter serial number manually.",
149
+ typePlateManualInputPlaceholder: 'e.g. 2234GH',
150
+ typePlateWhereToFindTitle: 'Where to find it',
151
+ typePlateWhereToFindDescription:
152
+ 'A metal or foil label on the machine housing, usually near the motor or the main panel. Make sure the serial number is sharp and readable.',
153
+ typePlateSerialNumberLabel: 'Serial number',
154
+ },
114
155
  geoLocation: false,
115
156
  geoLocationMessage: '',
116
157
  showImageDetails: false,
@@ -0,0 +1,34 @@
1
+ import { useAuth0 } from '@auth0/auth0-react';
2
+ import { useEffect } from 'react';
3
+ import {
4
+ applyAuthApiKey,
5
+ clearAuthApiKey,
6
+ readAuthApiKeyFromUser,
7
+ } from 'utils/nyrisApiKey';
8
+
9
+ /**
10
+ * Keeps window.settings.apiKey in sync with Auth0 claim
11
+ * `https://nyris.io/authApiKey`.
12
+ * Must only render inside Auth0Provider when auth0.enabled is true.
13
+ */
14
+ export function AuthApiKeySync() {
15
+ const { user, isAuthenticated, isLoading } = useAuth0();
16
+
17
+ useEffect(() => {
18
+ if (isLoading) {
19
+ return;
20
+ }
21
+
22
+ if (!isAuthenticated || !user) {
23
+ clearAuthApiKey();
24
+ return;
25
+ }
26
+
27
+ const authApiKey = readAuthApiKeyFromUser(
28
+ user as Record<string, unknown>,
29
+ );
30
+ applyAuthApiKey(authApiKey);
31
+ }, [user, isAuthenticated, isLoading]);
32
+
33
+ return null;
34
+ }
@@ -8,6 +8,9 @@ import { Send } from 'lucide-react';
8
8
  import emailjs from '@emailjs/browser';
9
9
  import { useAuth0 } from '@auth0/auth0-react';
10
10
  import NoImage from '../common/assets/images/no-image.svg';
11
+ import { trackConversion } from 'services/Feedback';
12
+ import { CONVERSION_SUBTYPE } from 'services/feedbackSubtypes';
13
+ import useResultStore from 'stores/result/resultStore';
11
14
 
12
15
  const EMAILJS_SERVICE_ID = 'service_zfsxshi';
13
16
  const EMAILJS_TEMPLATE_ID = window.settings.cart?.emailTemplateId || '';
@@ -102,6 +105,42 @@ const Cart = () => {
102
105
  const getQuantityDisplayValue = (sku: string, quantity: number) =>
103
106
  quantityEdits[sku] ?? String(quantity || 1);
104
107
 
108
+ /**
109
+ * Sends one conversion per originating search request. Items remember the
110
+ * request they were added from; items without one fall back to the current
111
+ * request id.
112
+ */
113
+ const trackCartConversion = (subtype: string) => {
114
+ const current = useResultStore.getState();
115
+ const groups = new Map<string, { sessionId?: string; parts: any[] }>();
116
+
117
+ cartParts.forEach((part: any) => {
118
+ const requestId = part.feedbackRequestId || current.requestId;
119
+ if (!requestId) return;
120
+ const group = groups.get(requestId) || {
121
+ sessionId: part.feedbackSessionId || current.sessionId,
122
+ parts: [] as any[],
123
+ };
124
+ group.parts.push(part);
125
+ groups.set(requestId, group);
126
+ });
127
+
128
+ groups.forEach(({ sessionId, parts }, requestId) => {
129
+ trackConversion(
130
+ subtype,
131
+ {
132
+ skus: parts.map(part => part.sku),
133
+ positions: parts.map(part => part.feedbackPosition),
134
+ quantity: parts.reduce(
135
+ (total, part) => total + (part.quantity || 1),
136
+ 0,
137
+ ),
138
+ },
139
+ { requestId, sessionId },
140
+ );
141
+ });
142
+ };
143
+
105
144
  const generateCSVContent = (): string => {
106
145
  const rows = [['Product Sku', 'Product Name', 'Quantity']];
107
146
  cartParts.forEach((part: any) => {
@@ -124,6 +163,7 @@ const Cart = () => {
124
163
  // clearSessionProducts();
125
164
 
126
165
  toast.success('Your data has been downloaded.');
166
+ trackCartConversion(CONVERSION_SUBTYPE.DOWNLOAD_CART);
127
167
  };
128
168
 
129
169
  const sendCSVByEmail = async (
@@ -159,6 +199,7 @@ const Cart = () => {
159
199
  );
160
200
 
161
201
  toast.success('Parts list has been sent by email.');
202
+ trackCartConversion(CONVERSION_SUBTYPE.SHARE_CART);
162
203
  setIsEmailInputVisible(false);
163
204
  setUserEmail('');
164
205
  } catch (error) {
@@ -12,6 +12,7 @@ import {
12
12
  fetchProductImageBase64,
13
13
  getProductImageUrl,
14
14
  } from 'utils/fetchProductImage';
15
+ import { getEffectiveApiKey } from 'utils/nyrisApiKey';
15
16
 
16
17
  export function useChatAssistantLogic() {
17
18
  const [inputValue, setInputValue] = useState('');
@@ -482,7 +483,7 @@ Each result includes a unique SKU in brackets. Use the result thumbnails and met
482
483
  setCatalogSearching(true);
483
484
  try {
484
485
  const settings = (window as any).settings;
485
- const nyrisKey = settings?.apiKey || settings?.nyrisApiKey;
486
+ const nyrisKey = getEffectiveApiKey();
486
487
  const baseUrl = settings?.baseUrl || 'https://api.nyris.io';
487
488
 
488
489
  store.setPreCatalogResults([...activeProducts]);
@@ -580,7 +581,7 @@ Each result includes a unique SKU in brackets. Use the result thumbnails and met
580
581
 
581
582
  try {
582
583
  const settings = (window as any).settings;
583
- const nyrisKey = settings?.apiKey || settings?.nyrisApiKey;
584
+ const nyrisKey = getEffectiveApiKey();
584
585
  const baseUrl = settings?.baseUrl || 'https://api.nyris.io';
585
586
 
586
587
  let searchQuery = rawText;
@@ -7,8 +7,11 @@ import useResultStore from 'stores/result/resultStore';
7
7
  import useRequestStore from 'stores/request/requestStore';
8
8
  import { twMerge } from 'tailwind-merge';
9
9
  import useUiStore from 'stores/ui/uiStore';
10
+ import useScopeStore from 'stores/scope/scopeStore';
11
+ import { useImageSearch } from 'hooks/useImageSearch';
10
12
 
11
13
  export const GoBackButton = ({ className }: { className?: string }) => {
14
+ const { singleImageSearch } = useImageSearch();
12
15
  const firstSearchResults = useResultStore(state => state.firstSearchResults);
13
16
  const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
14
17
  const setImageAnalysis = useResultStore(state => state.setImageAnalysis);
@@ -27,12 +30,35 @@ export const GoBackButton = ({ className }: { className?: string }) => {
27
30
  const setAlgoliaFilter = useRequestStore(state => state.setAlgoliaFilter);
28
31
 
29
32
  const isFindApiLoading = useUiStore(state => state.isFindApiLoading);
33
+ const scopeId = useScopeStore(state => state.scopeId);
34
+ const scopeItemKeyCount = useScopeStore(state => state.itemKeys.length);
35
+ const machineScopeActive =
36
+ Boolean(window.settings.scopes?.scopeID) &&
37
+ Boolean(scopeId) &&
38
+ scopeItemKeyCount > 0;
30
39
 
31
40
  const { refine: clearPostFilters } = useClearRefinements();
32
41
 
33
42
  const { t } = useTranslation();
34
43
 
35
44
  const onGoBack = () => {
45
+ // The saved snapshot may predate the machine scope, so search the original
46
+ // image again; find() applies the active scope.
47
+ if (machineScopeActive && firstSearchImage) {
48
+ setPreFilter(firstSearchPreFilter);
49
+ resetRegions();
50
+ clearPostFilters();
51
+ void singleImageSearch({
52
+ image: firstSearchImage,
53
+ settings: window.settings,
54
+ showFeedback: true,
55
+ newSearch: false,
56
+ compress: false,
57
+ clearPostFilter: true,
58
+ });
59
+ return;
60
+ }
61
+
36
62
  setFindApiProducts(firstSearchResults);
37
63
  if (firstSearchImage) {
38
64
  setRequestImages([firstSearchImage]);
@@ -58,9 +84,10 @@ export const GoBackButton = ({ className }: { className?: string }) => {
58
84
  };
59
85
 
60
86
  if (
61
- firstSearchResults.length === 0 ||
87
+ !firstSearchImage ||
62
88
  requestImages[0] === firstSearchImage ||
63
- isFindApiLoading
89
+ isFindApiLoading ||
90
+ (!machineScopeActive && firstSearchResults.length === 0)
64
91
  )
65
92
  return <></>;
66
93
 
@@ -12,6 +12,7 @@ import LogoutModal from './LogoutModal';
12
12
  import useResultStore from 'stores/result/resultStore';
13
13
  import Cart from './Cart';
14
14
  import useUiStore from 'stores/ui/uiStore';
15
+ import useScopeStore from 'stores/scope/scopeStore';
15
16
 
16
17
  function Header() {
17
18
  const { theme, auth0 } = window.settings;
@@ -20,6 +21,7 @@ function Header() {
20
21
 
21
22
  const reset = useRequestStore(state => state.reset);
22
23
  const resetResultStore = useResultStore(state => state.reset);
24
+ const resetScope = useScopeStore(state => state.resetScope);
23
25
  const setSpecifications = useRequestStore(state => state.setSpecifications);
24
26
  const showSidePanel = useUiStore(state => state.showSidePanel);
25
27
  const setShowSidePanel = useUiStore(state => state.setShowSidePanel);
@@ -84,6 +86,7 @@ function Header() {
84
86
  onClick={() => {
85
87
  reset();
86
88
  resetResultStore();
89
+ resetScope();
87
90
  setSpecifications(null);
88
91
  }}
89
92
  className="pl-6"
@@ -2,13 +2,31 @@ import { useTranslation } from 'react-i18next';
2
2
  import { useHitsPerPage, UseHitsPerPageProps } from 'react-instantsearch';
3
3
  import PoweredBy from './PoweredBy';
4
4
  import { twMerge } from 'tailwind-merge';
5
+ import useMachinePartsListingStore from 'stores/machinePartsListing/machinePartsListingStore';
5
6
 
6
- export const HitsPerPage = (props: UseHitsPerPageProps) => {
7
+ interface Props extends UseHitsPerPageProps {
8
+ onFindApiPageSizeChange?: (pageSize: number) => void;
9
+ useFindApiPageSize?: boolean;
10
+ showPageSize?: boolean;
11
+ }
12
+
13
+ export const HitsPerPage = ({
14
+ onFindApiPageSizeChange,
15
+ useFindApiPageSize = false,
16
+ showPageSize = true,
17
+ ...props
18
+ }: Props) => {
7
19
  const { items, refine } = useHitsPerPage(props);
8
20
  const { value: currentValue } =
9
21
  items.find(({ isRefined }) => isRefined)! || {};
10
-
22
+ const pageSize = useMachinePartsListingStore(state => state.pageSize);
11
23
  const { t } = useTranslation();
24
+ const isAlgoliaEnabled = window.settings.algolia.enabled;
25
+ const controlAlgoliaPageSize = isAlgoliaEnabled && !useFindApiPageSize;
26
+ const options = controlAlgoliaPageSize ? items : props.items || items;
27
+ const selectedValue = controlAlgoliaPageSize
28
+ ? String(currentValue)
29
+ : String(pageSize);
12
30
 
13
31
  return (
14
32
  <>
@@ -18,25 +36,44 @@ export const HitsPerPage = (props: UseHitsPerPageProps) => {
18
36
  'shadow-ds-1',
19
37
  )}
20
38
  >
21
- <div className="self-stretch pl-4 pr-6 border-r border-solid border-[#DDDEE7] justify-center items-center gap-1 flex">
22
- <div className="text-[#3B3E5F] text-[13px] font-normal font-['Source Sans 3'] leading-none tracking-tight">
23
- {t('Items per page')}:
39
+ {showPageSize && (
40
+ <div className="self-stretch pl-4 pr-6 border-r border-solid border-[#DDDEE7] justify-center items-center gap-1 flex">
41
+ <div className="text-[#3B3E5F] text-[13px] font-normal font-['Source Sans 3'] leading-none tracking-tight">
42
+ {t('Items per page')}:
43
+ </div>
44
+ <select
45
+ className="bg-transparent text-[13px] outline-none"
46
+ onChange={event => {
47
+ const nextPageSize = Number(event.target.value);
48
+ if (controlAlgoliaPageSize) {
49
+ refine(nextPageSize);
50
+ return;
51
+ }
52
+ onFindApiPageSizeChange?.(nextPageSize);
53
+ }}
54
+ value={selectedValue}
55
+ >
56
+ {options.map(item => (
57
+ <option key={item.value} value={item.value}>
58
+ {item.label}
59
+ </option>
60
+ ))}
61
+ </select>
24
62
  </div>
25
- <select
26
- className="bg-transparent text-[13px] outline-none"
27
- onChange={event => {
28
- refine(Number(event.target.value));
63
+ )}
64
+ {!isAlgoliaEnabled || !showPageSize ? (
65
+ <div
66
+ style={{
67
+ display: 'flex',
68
+ justifyContent: 'end',
69
+ marginLeft: 'auto',
29
70
  }}
30
- value={String(currentValue)}
31
71
  >
32
- {items.map(item => (
33
- <option key={item.value} value={item.value}>
34
- {item.label}
35
- </option>
36
- ))}
37
- </select>
38
- </div>
39
- {window.settings.showPoweredByNyris && <PoweredBy />}
72
+ {window.settings.showPoweredByNyris && <PoweredBy />}
73
+ </div>
74
+ ) : (
75
+ window.settings.showPoweredByNyris && <PoweredBy />
76
+ )}
40
77
  </div>
41
78
  </>
42
79
  );
@@ -12,6 +12,8 @@ import { DEFAULT_REGION } from '../constants';
12
12
  import { useImageSearch } from 'hooks/useImageSearch';
13
13
  import useRequestStore from 'stores/request/requestStore';
14
14
  import useResultStore from 'stores/result/resultStore';
15
+ import { hasActiveMachineScope } from 'stores/scope/scopeStore';
16
+ import { firstMachinePartsListingText } from 'stores/machinePartsListing/machinePartsListingStore';
15
17
 
16
18
  function ImagePreviewComponent({
17
19
  showAdjustInfo = false,
@@ -52,7 +54,7 @@ function ImagePreviewComponent({
52
54
  const detectedRegions = useResultStore(state => state.detectedRegions);
53
55
  const resetResultStore = useResultStore(state => state.reset);
54
56
 
55
- const { singleImageSearch } = useImageSearch();
57
+ const { singleImageSearch, searchMachinePartsListing } = useImageSearch();
56
58
 
57
59
  const currentIndex = requestImages.length - 1;
58
60
 
@@ -86,6 +88,33 @@ function ImagePreviewComponent({
86
88
  }, []);
87
89
 
88
90
  const onImageRemove = () => {
91
+ if (hasActiveMachineScope()) {
92
+ const keyword = (useRequestStore.getState().query || '').trim();
93
+ resetRegions();
94
+ setRequestImages([]);
95
+ if (keyword) {
96
+ void singleImageSearch({
97
+ text: keyword,
98
+ settings,
99
+ showFeedback: false,
100
+ newSearch: false,
101
+ });
102
+ } else {
103
+ const text = firstMachinePartsListingText();
104
+ useRequestStore.getState().setQuery('');
105
+ useRequestStore.getState().setValueInput('');
106
+ if (text) {
107
+ void searchMachinePartsListing({
108
+ text,
109
+ showFeedback: false,
110
+ clearPostFilters: true,
111
+ });
112
+ }
113
+ }
114
+ navigate('/result');
115
+ return;
116
+ }
117
+
89
118
  resetRegions();
90
119
  setRequestImages([]);
91
120
 
@@ -106,6 +135,7 @@ function ImagePreviewComponent({
106
135
  imageRegion: r,
107
136
  showFeedback: true,
108
137
  compress: false,
138
+ regionSelectedByUser: true,
109
139
  }).then((res: any) => {
110
140
  const specificationPrefilter =
111
141
  res?.image_analysis?.specification?.prefilter_value || null;
@@ -12,9 +12,11 @@ import { useUploadSearch } from 'hooks/useUploadSearch';
12
12
  function ImageUpload({
13
13
  onCameraClick,
14
14
  disableDisclaimer,
15
+ guideOpen,
15
16
  }: {
16
17
  onCameraClick?: () => void;
17
18
  disableDisclaimer?: boolean;
19
+ guideOpen?: boolean;
18
20
  }) {
19
21
  const settings = window.settings;
20
22
  const user = useAuth0().user;
@@ -31,13 +33,13 @@ function ImageUpload({
31
33
 
32
34
  const showPreFilter = useMemo(() => {
33
35
  if (settings.shouldUseUserMetadata && user) {
34
- if (user['/user_metadata'].value) {
36
+ if (user['/user_metadata']?.value) {
35
37
  setMetaFilter(user['/user_metadata'].value);
36
38
  }
37
39
  }
38
40
 
39
41
  if (settings.shouldUseUserMetadata && user) {
40
- if (settings.preFilterOption && !user['/user_metadata'].value) {
42
+ if (settings.preFilterOption && !user['/user_metadata']?.value) {
41
43
  return true;
42
44
  }
43
45
  return false;
@@ -91,7 +93,16 @@ function ImageUpload({
91
93
  e.target.value = '';
92
94
  }}
93
95
  />
94
- <Tooltip content={t('Search with an image')}>
96
+ <Tooltip
97
+ content={
98
+ guideOpen
99
+ ? t('You can now upload an image to search.')
100
+ : t('Search with an image')
101
+ }
102
+ open={guideOpen ? true : undefined}
103
+ side="top"
104
+ sideOffset={10}
105
+ >
95
106
  <label
96
107
  className={twMerge(
97
108
  'min-w-10 min-h-10 flex justify-center items-center cursor-pointer rounded-full bg-gray-100 desktop:bg-transparent hover:bg-gray-100',
@@ -14,6 +14,8 @@ import Tooltip from 'components/Tooltip/TooltipComponent';
14
14
 
15
15
  import { useTranslation } from 'react-i18next';
16
16
  import { createImage } from '../../services/visualSearch';
17
+ import { trackClick } from 'services/Feedback';
18
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
17
19
  interface Props {
18
20
  requestImage: any;
19
21
  selectedRegion: any;
@@ -123,6 +125,7 @@ export default function InquiryModal({
123
125
  try {
124
126
  await emailjs.send(serviceId, templateId, body);
125
127
  ToastHelper.success(t('Request sent successfully'));
128
+ trackClick(CLICK_SUBTYPE.RAISE_INQUIRY);
126
129
  } catch (error) {
127
130
  toast(
128
131
  () => {
@@ -1,5 +1,6 @@
1
1
  import { useAuth0 } from '@auth0/auth0-react';
2
2
  import { Dialog, DialogContent } from './Modal/Dialog';
3
+ import { clearAuthApiKey } from 'utils/nyrisApiKey';
3
4
 
4
5
  function LogoutModal({
5
6
  setShowLogoutModal,
@@ -41,6 +42,7 @@ function LogoutModal({
41
42
  cursor: 'pointer',
42
43
  }}
43
44
  onClick={() => {
45
+ clearAuthApiKey();
44
46
  logout({
45
47
  logoutParams: { returnTo: window.location.origin },
46
48
  });
@@ -1,19 +1,46 @@
1
1
  import { usePagination } from 'react-instantsearch';
2
2
  import { Icon } from '@nyris/nyris-react-components';
3
3
  import { twMerge } from 'tailwind-merge';
4
+ import { visibleMachinePartsPageIndexes } from 'services/machinePartsPage';
4
5
 
5
6
  export const Pagination = ({
6
7
  isLoading,
7
8
  className,
9
+ pageCount,
10
+ pageIndex = 0,
11
+ onPageChange,
8
12
  }: {
9
13
  isLoading: boolean;
10
14
  className?: string;
15
+ pageCount?: number;
16
+ pageIndex?: number;
17
+ onPageChange?: (pageIndex: number) => void;
11
18
  }) => {
12
- const { pages, currentRefinement, isFirstPage, isLastPage, refine } =
13
- usePagination();
19
+ const algoliaPagination = usePagination();
20
+ const isControlled = onPageChange !== undefined;
21
+ const currentRefinement = isControlled
22
+ ? pageIndex
23
+ : algoliaPagination.currentRefinement;
24
+ const isFirstPage = isControlled
25
+ ? currentRefinement <= 0
26
+ : algoliaPagination.isFirstPage;
27
+ const isLastPage = isControlled
28
+ ? currentRefinement >= (pageCount ?? 1) - 1
29
+ : algoliaPagination.isLastPage;
30
+ const pages = isControlled
31
+ ? visibleMachinePartsPageIndexes(currentRefinement, pageCount ?? 0)
32
+ : algoliaPagination.pages;
14
33
  const previousPageIndex = currentRefinement - 1;
15
34
  const nextPageIndex = currentRefinement + 1;
16
35
 
36
+ const selectPage = (page: number) => {
37
+ if (onPageChange) {
38
+ onPageChange(page);
39
+ return;
40
+ }
41
+ algoliaPagination.refine(page);
42
+ };
43
+
17
44
  return (
18
45
  <div
19
46
  className={twMerge(
@@ -22,13 +49,14 @@ export const Pagination = ({
22
49
  }`,
23
50
  className,
24
51
  )}
52
+ data-page-count={isControlled ? pageCount : undefined}
25
53
  >
26
54
  <div className="w-12 h-12 p-3.5 justify-center items-center flex">
27
55
  <div
28
56
  className="w-5 h-5 relative flex-col justify-start items-start flex cursor-pointer"
29
57
  onClick={() => {
30
58
  if (isFirstPage) return;
31
- refine(previousPageIndex);
59
+ selectPage(previousPageIndex);
32
60
  }}
33
61
  >
34
62
  <Icon name="caret_left" className="w-5 h-5" />
@@ -43,8 +71,7 @@ export const Pagination = ({
43
71
  className="w-12 px-4 pt-[15px] flex-col justify-end items-center gap-[11px] inline-flex cursor-pointer"
44
72
  onClick={event => {
45
73
  event.preventDefault();
46
-
47
- refine(page);
74
+ selectPage(page);
48
75
  }}
49
76
  >
50
77
  <div className="text-center text-sm font-normal leading-[18px] tracking-tight">
@@ -62,7 +89,7 @@ export const Pagination = ({
62
89
  className="w-5 h-5 relative flex-col justify-start items-start flex cursor-pointer"
63
90
  onClick={() => {
64
91
  if (isLastPage) return;
65
- refine(nextPageIndex);
92
+ selectPage(nextPageIndex);
66
93
  }}
67
94
  >
68
95
  <Icon name="caret_right" className="w-5 h-5" />
@@ -8,6 +8,8 @@ import { useRefinementList } from 'react-instantsearch';
8
8
  import { Icon } from '@nyris/nyris-react-components';
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import useScopeStore from 'stores/scope/scopeStore';
11
+ import { trackClick } from 'services/Feedback';
12
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
11
13
 
12
14
  function PostFilter({
13
15
  attribute,
@@ -145,7 +147,12 @@ function PostFilter({
145
147
  <input
146
148
  type="checkbox"
147
149
  checked={item.isRefined}
148
- onChange={() => refine(item.value)}
150
+ onChange={() => {
151
+ if (!item.isRefined) {
152
+ trackClick(CLICK_SUBTYPE.FILTER_APPLIED);
153
+ }
154
+ refine(item.value);
155
+ }}
149
156
  className="cursor-pointer"
150
157
  />
151
158
  <span className="text-xs font-normal pl-2 pr-1">