@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
@@ -0,0 +1,514 @@
1
+ import { useEffect, useRef, useState, type MutableRefObject } from 'react';
2
+ import { Icon } from '@nyris/nyris-react-components';
3
+ import { AlertCircle } from 'lucide-react';
4
+ import { applyScopeById } from 'hooks/useImageSearch';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useLocation } from 'react-router';
7
+ import { useMediaQuery } from 'react-responsive';
8
+ import { ScopeSelectInput } from './ScopeSelectInput';
9
+ import { ScopeSelectImageActions } from './ScopeSelectImageActions';
10
+ import { ScopeSelectImagePreview } from './ScopeSelectImagePreview';
11
+ import { ScopeSelectInfo } from './ScopeSelectInfo';
12
+ import {
13
+ AppliedScopeInput,
14
+ ScopeSource,
15
+ getImageFileKey,
16
+ isSameAppliedInput,
17
+ notifyScopeResult,
18
+ } from './scopeSelectHelpers';
19
+ import useScopeStore from 'stores/scope/scopeStore';
20
+ import { twMerge } from 'tailwind-merge';
21
+
22
+ interface Props {
23
+ handleClose: () => void;
24
+ manualScopeId: string;
25
+ pendingFile: File | null;
26
+ previewUrl: string | null;
27
+ activeSource: ScopeSource | null;
28
+ lastAppliedInput: AppliedScopeInput | null;
29
+ lastExtractedFromImage: string;
30
+ isExtracting: boolean;
31
+ scopeError: string | null;
32
+ isManualMode: boolean;
33
+ onManualScopeIdChange: (value: string) => void;
34
+ onSelectedImage: (file: File) => void;
35
+ onClearPreview: () => void;
36
+ onOpenCamera: () => void;
37
+ onApplied: (input: AppliedScopeInput | null) => void;
38
+ onActiveSourceChange: (source: ScopeSource | null) => void;
39
+ onExtractedFromImage: (value: string) => void;
40
+ onScopeErrorChange: (error: string | null) => void;
41
+ onSwitchToManual: () => void;
42
+ onSuccess?: (itemKeys?: string[]) => void;
43
+ discardPendingScopeRef: MutableRefObject<(() => void) | null>;
44
+ }
45
+
46
+ export function ScopeSelectForm({
47
+ handleClose,
48
+ manualScopeId,
49
+ pendingFile,
50
+ previewUrl,
51
+ activeSource,
52
+ lastAppliedInput,
53
+ lastExtractedFromImage,
54
+ isExtracting,
55
+ scopeError,
56
+ isManualMode,
57
+ onManualScopeIdChange,
58
+ onSelectedImage,
59
+ onClearPreview,
60
+ onOpenCamera,
61
+ onApplied,
62
+ onActiveSourceChange,
63
+ onScopeErrorChange,
64
+ onSwitchToManual,
65
+ onSuccess,
66
+ discardPendingScopeRef,
67
+ }: Props) {
68
+ const { t } = useTranslation();
69
+ const { pathname } = useLocation();
70
+ const isMobile = useMediaQuery({ query: '(max-width: 776px)' });
71
+ const fileInputRef = useRef<HTMLInputElement>(null);
72
+ const previousScopeRef = useRef<{
73
+ itemKeys: string[];
74
+ filterAttributes: ReturnType<typeof useScopeStore.getState>['filterAttributes'];
75
+ scopeId: string | null;
76
+ } | null>(null);
77
+ const manualScopeAcceptedRef = useRef(false);
78
+
79
+ discardPendingScopeRef.current = () => {
80
+ if (manualScopeAcceptedRef.current || !previousScopeRef.current) {
81
+ return;
82
+ }
83
+ const previous = previousScopeRef.current;
84
+ previousScopeRef.current = null;
85
+ if (previous.scopeId && previous.itemKeys.length > 0) {
86
+ useScopeStore.getState().setScope(
87
+ previous.itemKeys,
88
+ previous.filterAttributes,
89
+ previous.scopeId,
90
+ );
91
+ } else {
92
+ useScopeStore.getState().resetScope();
93
+ }
94
+ };
95
+
96
+ const acceptManualScope = () => {
97
+ manualScopeAcceptedRef.current = true;
98
+ previousScopeRef.current = null;
99
+ };
100
+ const [isApplying, setIsApplying] = useState(false);
101
+ const [isLookingUp, setIsLookingUp] = useState(false);
102
+ const [confirmedManualScopeId, setConfirmedManualScopeId] = useState<
103
+ string | null
104
+ >(null);
105
+ const activeScopeId = useScopeStore(state => state.scopeId);
106
+ const scopeItemKeys = useScopeStore(state => state.itemKeys);
107
+ const primaryColor = window.settings.theme.primaryColor || '#3E36DC';
108
+ const findMachineLabel =
109
+ window.settings.scopes?.typePlatePopupFindMachineButton ||
110
+ window.settings.scopes?.typePlateFindMachineButton ||
111
+ 'Find machine';
112
+ const enterManuallyLabel =
113
+ window.settings.scopes?.typePlatePopupCtaTextButton ||
114
+ 'Enter serial number manually';
115
+ const skipButtonLabel = pathname.startsWith('/result')
116
+ ? window.settings.scopes?.typePlatePopupBackButton || 'Back to results'
117
+ : window.settings.scopes?.typePlatePopupSkipButton ||
118
+ window.settings.scopes?.typePlateSkipButton ||
119
+ 'Skip and search all parts';
120
+ const uploadImageLabel =
121
+ window.settings.scopes?.typePlatePopupUploadButton || 'Upload image';
122
+ const uploadNewTypeplateLabel =
123
+ window.settings.scopes?.typePlatePopupUploadNewButton ||
124
+ 'Upload new type plate';
125
+ const applyingLabel =
126
+ window.settings.scopes?.typePlatePopupApplyingLabel || 'Applying...';
127
+
128
+ const hasManualId = manualScopeId.trim().length > 0;
129
+ const isScopeStillApplied =
130
+ Boolean(activeScopeId) && scopeItemKeys.length > 0;
131
+ const confirmedManualValue = confirmedManualScopeId?.trim() ?? '';
132
+ const isManualIdentified =
133
+ !previewUrl &&
134
+ confirmedManualValue.length > 0 &&
135
+ confirmedManualValue === manualScopeId.trim() &&
136
+ !scopeError &&
137
+ !isExtracting;
138
+ const isSuccess =
139
+ (Boolean(lastExtractedFromImage) || isManualIdentified) &&
140
+ !scopeError &&
141
+ !isApplying &&
142
+ !isLookingUp &&
143
+ !isExtracting;
144
+ const popupTitle = isSuccess
145
+ ? window.settings.scopes?.typePlatePopupTitleSuccess || 'Machine identified'
146
+ : window.settings.scopes?.typePlatePopupTitle || 'Identify your machine';
147
+
148
+ const activeScopeValue =
149
+ activeSource === 'image'
150
+ ? lastExtractedFromImage
151
+ : activeSource === 'manual'
152
+ ? manualScopeId.trim()
153
+ : manualScopeId.trim() || lastExtractedFromImage.trim();
154
+
155
+ const canApply =
156
+ !isApplying &&
157
+ !isLookingUp &&
158
+ !isExtracting &&
159
+ activeScopeValue.length > 0 &&
160
+ (activeSource !== 'manual' || isManualIdentified);
161
+ const hasImageError = Boolean(previewUrl && scopeError);
162
+ const isImageIdentified =
163
+ Boolean(previewUrl) &&
164
+ Boolean(lastExtractedFromImage) &&
165
+ !scopeError &&
166
+ !isExtracting;
167
+
168
+ const handleClearImage = () => {
169
+ onScopeErrorChange(null);
170
+ onClearPreview();
171
+ onActiveSourceChange(null);
172
+ if (fileInputRef.current) {
173
+ fileInputRef.current.value = '';
174
+ }
175
+ };
176
+
177
+ useEffect(() => {
178
+ if (!manualScopeId.trim()) {
179
+ setConfirmedManualScopeId(null);
180
+ }
181
+ }, [manualScopeId]);
182
+
183
+ const onFindMachine = async () => {
184
+ const value = manualScopeId.trim();
185
+ if (!value || isLookingUp || isApplying || isExtracting) {
186
+ return;
187
+ }
188
+
189
+ const currentScope = useScopeStore.getState();
190
+ const scopeBeforeLookup = previousScopeRef.current ?? {
191
+ itemKeys: [...currentScope.itemKeys],
192
+ filterAttributes: [...currentScope.filterAttributes],
193
+ scopeId: currentScope.scopeId,
194
+ };
195
+ setIsLookingUp(true);
196
+ onScopeErrorChange(null);
197
+ onActiveSourceChange('manual');
198
+ const defaultErrorMessage =
199
+ window.settings.scopes?.typePlatePopupErrorMessage ||
200
+ 'The serial number was not found. Please check again.';
201
+ try {
202
+ const result = await applyScopeById(value);
203
+ if (result.status === 'applied') {
204
+ previousScopeRef.current = scopeBeforeLookup;
205
+ setConfirmedManualScopeId(value);
206
+ onApplied({ source: 'manual', value });
207
+ return;
208
+ }
209
+ setConfirmedManualScopeId(null);
210
+ onScopeErrorChange(t(defaultErrorMessage));
211
+ } catch {
212
+ setConfirmedManualScopeId(null);
213
+ onScopeErrorChange(t(defaultErrorMessage));
214
+ } finally {
215
+ setIsLookingUp(false);
216
+ }
217
+ };
218
+
219
+ const onApply = async () => {
220
+ if (!canApply || !activeScopeValue) {
221
+ return;
222
+ }
223
+ if (
224
+ activeSource === 'manual' &&
225
+ manualScopeId.trim() !== confirmedManualValue
226
+ ) {
227
+ return;
228
+ }
229
+
230
+ const currentSource: ScopeSource =
231
+ activeSource === 'image'
232
+ ? 'image'
233
+ : activeSource === 'manual'
234
+ ? 'manual'
235
+ : manualScopeId.trim()
236
+ ? 'manual'
237
+ : 'image';
238
+
239
+ const currentInput: AppliedScopeInput =
240
+ currentSource === 'image'
241
+ ? {
242
+ source: 'image',
243
+ fileKey: pendingFile
244
+ ? getImageFileKey(pendingFile)
245
+ : previewUrl || activeScopeValue,
246
+ }
247
+ : {
248
+ source: 'manual',
249
+ value: activeScopeValue,
250
+ };
251
+
252
+ if (
253
+ lastAppliedInput &&
254
+ isSameAppliedInput(currentInput, lastAppliedInput) &&
255
+ isScopeStillApplied
256
+ ) {
257
+ acceptManualScope();
258
+ if (onSuccess) {
259
+ onSuccess(scopeItemKeys);
260
+ } else {
261
+ handleClose();
262
+ }
263
+ return;
264
+ }
265
+
266
+ setIsApplying(true);
267
+ onScopeErrorChange(null);
268
+ const defaultErrorMessage =
269
+ window.settings.scopes?.typePlatePopupErrorMessage ||
270
+ 'The serial number was not found. Please check again.';
271
+ try {
272
+ const result = await applyScopeById(activeScopeValue);
273
+ if (result.status === 'applied') {
274
+ notifyScopeResult(result, t);
275
+ onApplied(currentInput);
276
+ acceptManualScope();
277
+ if (onSuccess) {
278
+ onSuccess(result.itemKeys);
279
+ } else {
280
+ handleClose();
281
+ }
282
+ return;
283
+ }
284
+ onScopeErrorChange(t(defaultErrorMessage));
285
+ } catch {
286
+ onScopeErrorChange(t(defaultErrorMessage));
287
+ } finally {
288
+ setIsApplying(false);
289
+ }
290
+ };
291
+
292
+ const searchWithMachineButton = (
293
+ <button
294
+ type="button"
295
+ disabled={!canApply}
296
+ className={twMerge(
297
+ 'h-12 w-fit self-start rounded-[32px] px-6 text-[14px] font-semibold text-white',
298
+ 'flex items-center justify-center gap-2 transition-opacity',
299
+ 'disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer',
300
+ )}
301
+ style={{ backgroundColor: primaryColor }}
302
+ onClick={() => {
303
+ void onApply();
304
+ }}
305
+ >
306
+ {isApplying ? (
307
+ <>
308
+ <svg
309
+ width={16}
310
+ height={16}
311
+ viewBox="0 0 50 50"
312
+ fill="none"
313
+ xmlns="http://www.w3.org/2000/svg"
314
+ className="loading-spinner shrink-0"
315
+ >
316
+ <circle
317
+ cx="25"
318
+ cy="25"
319
+ r="20"
320
+ stroke="#FFFFFF"
321
+ strokeWidth="4"
322
+ strokeLinecap="round"
323
+ strokeDasharray="90 30"
324
+ />
325
+ </svg>
326
+ <span>{t(applyingLabel)}</span>
327
+ </>
328
+ ) : (
329
+ t(window.settings.scopes?.typePlatePopupCtaButton || 'Apply')
330
+ )}
331
+ </button>
332
+ );
333
+
334
+ const replaceImageActions = (
335
+ <ScopeSelectImageActions
336
+ isMobile={isMobile}
337
+ isResolving={isApplying || isLookingUp || isExtracting}
338
+ onLoadImage={() => fileInputRef.current?.click()}
339
+ onTakePhoto={onOpenCamera}
340
+ uploadLabel={uploadImageLabel}
341
+ />
342
+ );
343
+
344
+ const confirmationImageActions = (
345
+ <ScopeSelectImageActions
346
+ isMobile={isMobile}
347
+ isResolving={isApplying || isLookingUp || isExtracting}
348
+ onLoadImage={() => fileInputRef.current?.click()}
349
+ onTakePhoto={onOpenCamera}
350
+ uploadLabel={uploadNewTypeplateLabel}
351
+ />
352
+ );
353
+
354
+ const identifiedConfirmationActions = isMobile ? (
355
+ <>
356
+ {confirmationImageActions}
357
+ {searchWithMachineButton}
358
+ </>
359
+ ) : (
360
+ <div className="flex flex-wrap items-center gap-3">
361
+ {searchWithMachineButton}
362
+ {confirmationImageActions}
363
+ </div>
364
+ );
365
+
366
+ return (
367
+ <div className="relative flex h-full flex-col overflow-hidden">
368
+ <div className="sticky top-0 z-100 flex w-full justify-between bg-white">
369
+ <div className="min-w-0 pl-4 desktop:mt-6">
370
+ <div className="text-2xl font-bold text-black mb-2">
371
+ {t(popupTitle)}
372
+ </div>
373
+ </div>
374
+ <div
375
+ onClick={handleClose}
376
+ className="mr-2 flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center"
377
+ >
378
+ <Icon name="close" />
379
+ </div>
380
+ </div>
381
+
382
+ <div className="flex-1 overflow-y-auto px-6 py-6 desktop:bg-[#FAFAFA]">
383
+ <div className="flex max-w-[636px] flex-col gap-4">
384
+ {previewUrl && (
385
+ <ScopeSelectImagePreview
386
+ previewUrl={previewUrl}
387
+ extractedScopeId={lastExtractedFromImage}
388
+ isExtracting={isExtracting}
389
+ errorMessage={activeSource === 'image' ? scopeError : null}
390
+ onClearImage={handleClearImage}
391
+ />
392
+ )}
393
+
394
+ {isManualIdentified && (
395
+ <ScopeSelectImagePreview
396
+ thumbnail="machine"
397
+ extractedScopeId={confirmedManualValue}
398
+ isExtracting={false}
399
+ />
400
+ )}
401
+
402
+ {!previewUrl && !isManualIdentified && <ScopeSelectInfo />}
403
+
404
+ {!previewUrl && !isManualIdentified && (
405
+ <ScopeSelectImageActions
406
+ isMobile={isMobile}
407
+ isResolving={isApplying || isLookingUp || isExtracting}
408
+ onLoadImage={() => fileInputRef.current?.click()}
409
+ onTakePhoto={onOpenCamera}
410
+ />
411
+ )}
412
+
413
+ {!previewUrl && !isManualIdentified && (
414
+ <ScopeSelectInput
415
+ manualScopeId={manualScopeId}
416
+ hasManualId={hasManualId}
417
+ isResolving={isApplying || isLookingUp || isExtracting}
418
+ isApplying={isLookingUp}
419
+ submitLabel={findMachineLabel}
420
+ onManualScopeIdChange={value => {
421
+ onScopeErrorChange(null);
422
+ onActiveSourceChange('manual');
423
+ if (
424
+ confirmedManualValue &&
425
+ value.trim() !== confirmedManualValue
426
+ ) {
427
+ setConfirmedManualScopeId(null);
428
+ }
429
+ onManualScopeIdChange(value);
430
+ }}
431
+ onClearText={() => {
432
+ onScopeErrorChange(null);
433
+ onManualScopeIdChange('');
434
+ }}
435
+ onSubmit={() => {
436
+ void onFindMachine();
437
+ }}
438
+ />
439
+ )}
440
+
441
+ {isManualIdentified && identifiedConfirmationActions}
442
+
443
+ {scopeError && (isManualMode || activeSource === 'manual') ? (
444
+ <div className="flex items-center gap-1.5 text-sm font-normal text-[#2B2C46]">
445
+ <AlertCircle className="h-4 w-4 shrink-0 text-[#2B2C46]" />
446
+ <span>{scopeError}</span>
447
+ </div>
448
+ ) : null}
449
+
450
+ {previewUrl &&
451
+ (hasImageError ? (
452
+ isMobile ? (
453
+ replaceImageActions
454
+ ) : (
455
+ <button
456
+ type="button"
457
+ disabled={isApplying || isExtracting}
458
+ className={twMerge(
459
+ 'flex h-12 w-fit items-center gap-2 rounded-[32px] border border-solid',
460
+ 'border-[#DDDEE7] bg-white px-4 text-[14px] text-[#2B2C46] shadow-ds-2 cursor-pointer',
461
+ 'disabled:cursor-not-allowed disabled:bg-[#F3F3F5] disabled:text-[#BBBDCF] disabled:opacity-50',
462
+ )}
463
+ onClick={() => fileInputRef.current?.click()}
464
+ >
465
+ <Icon name="gallery" width={16} height={16} fill="#3B3E5F" />
466
+ {t(uploadImageLabel)}
467
+ </button>
468
+ )
469
+ ) : isImageIdentified ? (
470
+ identifiedConfirmationActions
471
+ ) : (
472
+ searchWithMachineButton
473
+ ))}
474
+ </div>
475
+ <input
476
+ ref={fileInputRef}
477
+ type="file"
478
+ accept="image/*"
479
+ className="hidden"
480
+ onChange={event => {
481
+ const file = event.target.files?.[0];
482
+ event.target.value = '';
483
+ if (!file) {
484
+ return;
485
+ }
486
+ onScopeErrorChange(null);
487
+ onSelectedImage(file);
488
+ }}
489
+ />
490
+ </div>
491
+
492
+ <div className="mt-auto flex flex-col items-center justify-center gap-2 px-6 py-4">
493
+ {hasImageError && !isExtracting && !isManualMode && (
494
+ <button
495
+ type="button"
496
+ className="text-sm font-normal text-[#989BB7] underline-offset-2 hover:underline cursor-pointer"
497
+ onClick={onSwitchToManual}
498
+ >
499
+ {t(enterManuallyLabel)}
500
+ </button>
501
+ )}
502
+ {(!previewUrl || isImageIdentified) && (
503
+ <button
504
+ type="button"
505
+ className="text-sm font-normal text-[#989BB7] underline-offset-2 hover:underline cursor-pointer"
506
+ onClick={handleClose}
507
+ >
508
+ {t(skipButtonLabel)}
509
+ </button>
510
+ )}
511
+ </div>
512
+ </div>
513
+ );
514
+ }
@@ -0,0 +1,62 @@
1
+ import { Icon } from '@nyris/nyris-react-components';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ interface Props {
6
+ isMobile: boolean;
7
+ isResolving: boolean;
8
+ onLoadImage: () => void;
9
+ onTakePhoto: () => void;
10
+ uploadLabel?: string;
11
+ }
12
+
13
+ export function ScopeSelectImageActions({
14
+ isMobile,
15
+ isResolving,
16
+ onLoadImage,
17
+ onTakePhoto,
18
+ uploadLabel,
19
+ }: Props) {
20
+ const { t } = useTranslation();
21
+ const resolvedUploadLabel =
22
+ uploadLabel ||
23
+ window.settings.scopes?.typePlatePopupUploadButton ||
24
+ 'Upload image';
25
+ const takePhotoLabel =
26
+ window.settings.scopes?.typePlatePopupTakePhotoButton || 'Take a photo';
27
+
28
+ return (
29
+ <div className="flex flex-wrap items-center gap-3">
30
+ <button
31
+ type="button"
32
+ disabled={isResolving}
33
+ className={twMerge(
34
+ 'flex h-12 items-center gap-2 rounded-[32px] border border-solid',
35
+ 'border-[#DDDEE7] bg-white px-4 text-[14px] text-[#2B2C46] shadow-ds-2 cursor-pointer',
36
+ 'disabled:cursor-not-allowed disabled:bg-[#F3F3F5] disabled:text-[#BBBDCF]',
37
+ 'disabled:opacity-50',
38
+ )}
39
+ onClick={onLoadImage}
40
+ >
41
+ <Icon name="gallery" width={16} height={16} fill="#3B3E5F" />
42
+ {t(resolvedUploadLabel)}
43
+ </button>
44
+ {isMobile && (
45
+ <button
46
+ type="button"
47
+ disabled={isResolving}
48
+ className={twMerge(
49
+ 'flex h-12 items-center gap-2 rounded-[32px] border border-solid',
50
+ 'border-[#DDDEE7] bg-white px-4 text-[14px] text-[#2B2C46] shadow-ds-2 cursor-pointer',
51
+ 'disabled:cursor-not-allowed disabled:bg-[#F3F3F5] disabled:text-[#BBBDCF]',
52
+ 'disabled:opacity-50',
53
+ )}
54
+ onClick={onTakePhoto}
55
+ >
56
+ <Icon name="camera_ai" width={16} height={16} fill="#3B3E5F" />
57
+ {t(takePhotoLabel)}
58
+ </button>
59
+ )}
60
+ </div>
61
+ );
62
+ }