@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Icon } from '@nyris/nyris-react-components';
|
|
2
|
+
import { AlertCircle, Check, Factory } from 'lucide-react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import Tooltip from 'components/Tooltip/TooltipComponent';
|
|
5
|
+
import { twMerge } from 'tailwind-merge';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
previewUrl?: string | null;
|
|
9
|
+
extractedScopeId: string;
|
|
10
|
+
isExtracting: boolean;
|
|
11
|
+
isSuccess?: boolean;
|
|
12
|
+
errorMessage?: string | null;
|
|
13
|
+
thumbnail?: 'image' | 'machine';
|
|
14
|
+
onClearImage?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ScopeSelectImagePreview({
|
|
18
|
+
previewUrl,
|
|
19
|
+
extractedScopeId,
|
|
20
|
+
isExtracting,
|
|
21
|
+
errorMessage = null,
|
|
22
|
+
thumbnail = 'image',
|
|
23
|
+
onClearImage,
|
|
24
|
+
}: Props) {
|
|
25
|
+
const { t } = useTranslation();
|
|
26
|
+
const serialNumberLabel =
|
|
27
|
+
window.settings.scopes?.typePlateSerialNumberLabel || 'Serial number';
|
|
28
|
+
const thumbnailClassName =
|
|
29
|
+
'flex h-20 w-20 shrink-0 items-center justify-center overflow-hidden rounded-md bg-[#FAFAFA]';
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="relative flex w-full items-center gap-3.5 rounded-xl border border-solid border-[#DDDEE7] bg-white p-2.5 pr-8">
|
|
33
|
+
{thumbnail === 'machine' || !previewUrl ? (
|
|
34
|
+
<div className={thumbnailClassName} aria-hidden="true">
|
|
35
|
+
<Factory className="h-8 w-8 text-[#2B2C46]" strokeWidth={1.75} />
|
|
36
|
+
</div>
|
|
37
|
+
) : (
|
|
38
|
+
<Tooltip
|
|
39
|
+
content={
|
|
40
|
+
<img
|
|
41
|
+
src={previewUrl}
|
|
42
|
+
alt=""
|
|
43
|
+
className="h-[350px] w-[350px] max-h-[70vh] max-w-[80vw] object-contain"
|
|
44
|
+
/>
|
|
45
|
+
}
|
|
46
|
+
side="top"
|
|
47
|
+
sideOffset={8}
|
|
48
|
+
>
|
|
49
|
+
<div className={twMerge(thumbnailClassName, 'cursor-pointer')}>
|
|
50
|
+
<img
|
|
51
|
+
src={previewUrl}
|
|
52
|
+
alt=""
|
|
53
|
+
className="h-full w-full object-contain"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
</Tooltip>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<div className="min-w-0 flex-1">
|
|
60
|
+
{isExtracting ? (
|
|
61
|
+
<div>
|
|
62
|
+
<p className="text-xs font-normal leading-none text-[#989BB7]">
|
|
63
|
+
{t(serialNumberLabel)}
|
|
64
|
+
</p>
|
|
65
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
66
|
+
<svg
|
|
67
|
+
width={16}
|
|
68
|
+
height={16}
|
|
69
|
+
viewBox="0 0 50 50"
|
|
70
|
+
fill="none"
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
className="loading-spinner shrink-0"
|
|
73
|
+
>
|
|
74
|
+
<circle
|
|
75
|
+
cx="25"
|
|
76
|
+
cy="25"
|
|
77
|
+
r="20"
|
|
78
|
+
stroke="#3E36DC"
|
|
79
|
+
strokeWidth="4"
|
|
80
|
+
strokeLinecap="round"
|
|
81
|
+
strokeDasharray="90 30"
|
|
82
|
+
/>
|
|
83
|
+
</svg>
|
|
84
|
+
<span className="text-sm text-[#989BB7]">{t('loading')}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
) : (
|
|
88
|
+
<>
|
|
89
|
+
{extractedScopeId ? (
|
|
90
|
+
<>
|
|
91
|
+
<p className="text-xs font-normal leading-none text-[#989BB7]">
|
|
92
|
+
{t(serialNumberLabel)}
|
|
93
|
+
</p>
|
|
94
|
+
<div className="mt-1 flex items-center gap-1.5">
|
|
95
|
+
<span
|
|
96
|
+
className="truncate text-lg font-bold leading-tight text-[#2B2C46]"
|
|
97
|
+
title={extractedScopeId}
|
|
98
|
+
>
|
|
99
|
+
{extractedScopeId}
|
|
100
|
+
</span>
|
|
101
|
+
{!errorMessage && (
|
|
102
|
+
<Check
|
|
103
|
+
className="h-[18px] w-[18px] shrink-0 text-green-600"
|
|
104
|
+
strokeWidth={2.5}
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</>
|
|
110
|
+
) : null}
|
|
111
|
+
{errorMessage ? (
|
|
112
|
+
<div
|
|
113
|
+
className={twMerge(
|
|
114
|
+
'flex items-center gap-1.5 text-sm font-normal text-[#2B2C46]',
|
|
115
|
+
extractedScopeId && 'mt-1.5',
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<AlertCircle className="h-4 w-4 shrink-0 text-[#2B2C46]" />
|
|
119
|
+
<span>{errorMessage}</span>
|
|
120
|
+
</div>
|
|
121
|
+
) : null}
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{onClearImage ? (
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
className="absolute top-2 right-2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-[#55566B] transition-colors hover:bg-gray-100 hover:text-[#2B2C46]"
|
|
130
|
+
aria-label={t('Clear image search')}
|
|
131
|
+
onClick={e => {
|
|
132
|
+
e.stopPropagation();
|
|
133
|
+
onClearImage();
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
<Icon name="close" width={11} height={11} />
|
|
137
|
+
</button>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { twMerge } from 'tailwind-merge';
|
|
4
|
+
import plateInfoImage from 'common/assets/images/plate_info.png';
|
|
5
|
+
import { Dialog, DialogContent, DialogTitle } from '../Modal/Dialog';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
isEmptyState?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ScopeSelectInfo({ isEmptyState = true }: Props) {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
|
14
|
+
const whereToFindTitle =
|
|
15
|
+
window.settings.scopes?.typePlateWhereToFindTitle || 'Where to find it';
|
|
16
|
+
const whereToFindDescription =
|
|
17
|
+
window.settings.scopes?.typePlateWhereToFindDescription ||
|
|
18
|
+
'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.';
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<div className="flex flex-col items-start gap-4 min-[640px]:flex-row min-[640px]:items-center">
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
className="shrink-0 cursor-pointer border-0 bg-transparent p-0"
|
|
26
|
+
onClick={() => setIsPreviewOpen(true)}
|
|
27
|
+
aria-label={t(whereToFindTitle)}
|
|
28
|
+
>
|
|
29
|
+
<img
|
|
30
|
+
src={plateInfoImage}
|
|
31
|
+
alt=""
|
|
32
|
+
className="h-auto w-full max-w-[150px] min-w-[150px] object-contain bg-primary px-2 py-6"
|
|
33
|
+
/>
|
|
34
|
+
</button>
|
|
35
|
+
<div className="min-w-0 flex-1">
|
|
36
|
+
<h3
|
|
37
|
+
className={twMerge(
|
|
38
|
+
'font-bold text-[#2B2C46]',
|
|
39
|
+
isEmptyState ? 'text-base' : 'text-sm',
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
42
|
+
{t(whereToFindTitle)}
|
|
43
|
+
</h3>
|
|
44
|
+
<p
|
|
45
|
+
className={twMerge(
|
|
46
|
+
'mt-1 font-normal text-[#989BB7]',
|
|
47
|
+
isEmptyState ? 'text-sm leading-5' : 'text-xs leading-4',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{t(whereToFindDescription)}
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
|
55
|
+
<DialogContent className="max-w-[90vw] w-fit bg-white p-4">
|
|
56
|
+
<DialogTitle className="h-0 w-0 hidden">
|
|
57
|
+
{t(whereToFindTitle)}
|
|
58
|
+
</DialogTitle>
|
|
59
|
+
<img
|
|
60
|
+
src={plateInfoImage}
|
|
61
|
+
alt=""
|
|
62
|
+
className="max-h-[80vh] max-w-[min(640px,80vw)] object-contain"
|
|
63
|
+
/>
|
|
64
|
+
</DialogContent>
|
|
65
|
+
</Dialog>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
manualScopeId: string;
|
|
7
|
+
hasManualId: boolean;
|
|
8
|
+
isResolving: boolean;
|
|
9
|
+
isApplying?: boolean;
|
|
10
|
+
submitLabel?: string;
|
|
11
|
+
onManualScopeIdChange: (value: string) => void;
|
|
12
|
+
onClearText: () => void;
|
|
13
|
+
onSubmit: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ScopeSelectInput({
|
|
17
|
+
manualScopeId,
|
|
18
|
+
hasManualId,
|
|
19
|
+
isResolving,
|
|
20
|
+
isApplying = false,
|
|
21
|
+
submitLabel,
|
|
22
|
+
onManualScopeIdChange,
|
|
23
|
+
onClearText,
|
|
24
|
+
onSubmit,
|
|
25
|
+
}: Props) {
|
|
26
|
+
const { t } = useTranslation();
|
|
27
|
+
const ctaLabel =
|
|
28
|
+
submitLabel || window.settings.scopes?.typePlatePopupCtaButton || 'Apply';
|
|
29
|
+
const manualInputLabel =
|
|
30
|
+
window.settings.scopes?.typePlateManualInputLabel ||
|
|
31
|
+
"Can't scan? Enter serial number manually.";
|
|
32
|
+
const placeholder =
|
|
33
|
+
window.settings.scopes?.typePlateManualInputPlaceholder || 'e.g. 2234GH';
|
|
34
|
+
const applyingLabel =
|
|
35
|
+
window.settings.scopes?.typePlatePopupApplyingLabel || 'Applying...';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="w-full max-w-[636px]">
|
|
39
|
+
<p className="mb-2 text-xs font-normal text-[#2B2C46]">
|
|
40
|
+
{t(manualInputLabel)}
|
|
41
|
+
</p>
|
|
42
|
+
<div className="flex items-center gap-2">
|
|
43
|
+
<div
|
|
44
|
+
className={twMerge(
|
|
45
|
+
'bg-white border border-solid border-[#DDDEE7] shadow-ds-2',
|
|
46
|
+
'flex h-12 min-w-0 flex-1 items-center justify-between pr-1.5',
|
|
47
|
+
'rounded-[32px]',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span className="flex h-full min-w-0 flex-1 items-center rounded-[32px]">
|
|
51
|
+
<input
|
|
52
|
+
type="text"
|
|
53
|
+
value={manualScopeId}
|
|
54
|
+
disabled={isResolving}
|
|
55
|
+
placeholder={t(placeholder)}
|
|
56
|
+
className={twMerge(
|
|
57
|
+
'h-full w-full border-0 bg-transparent pl-4 outline-none',
|
|
58
|
+
'rounded-[32px] text-[14px] text-[#2B2C46]',
|
|
59
|
+
'disabled:bg-[#F3F3F5] disabled:text-[#BBBDCF]',
|
|
60
|
+
)}
|
|
61
|
+
onChange={event => onManualScopeIdChange(event.target.value)}
|
|
62
|
+
onKeyDown={event => {
|
|
63
|
+
if (event.key === 'Enter') {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
if (hasManualId && !isResolving) {
|
|
66
|
+
onSubmit();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
</span>
|
|
72
|
+
{hasManualId && (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="flex min-h-10 min-w-10 items-center justify-center rounded-full hover:bg-gray-100 cursor-pointer"
|
|
76
|
+
aria-label={t('Clear text search')}
|
|
77
|
+
onClick={onClearText}
|
|
78
|
+
>
|
|
79
|
+
<Icon name="close" className="h-3 w-3 text-primary" />
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
disabled={!hasManualId || isResolving}
|
|
86
|
+
className={twMerge(
|
|
87
|
+
'h-12 shrink-0 rounded-[32px] px-6 text-[14px] font-semibold text-white',
|
|
88
|
+
'flex items-center justify-center gap-2 transition-colors',
|
|
89
|
+
'bg-primary hover:bg-[#2B2C46] disabled:hover:bg-primary',
|
|
90
|
+
'disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer',
|
|
91
|
+
)}
|
|
92
|
+
onClick={onSubmit}
|
|
93
|
+
>
|
|
94
|
+
{isApplying ? (
|
|
95
|
+
<>
|
|
96
|
+
<svg
|
|
97
|
+
width={16}
|
|
98
|
+
height={16}
|
|
99
|
+
viewBox="0 0 50 50"
|
|
100
|
+
fill="none"
|
|
101
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
102
|
+
className="loading-spinner shrink-0"
|
|
103
|
+
>
|
|
104
|
+
<circle
|
|
105
|
+
cx="25"
|
|
106
|
+
cy="25"
|
|
107
|
+
r="20"
|
|
108
|
+
stroke="#FFFFFF"
|
|
109
|
+
strokeWidth="4"
|
|
110
|
+
strokeLinecap="round"
|
|
111
|
+
strokeDasharray="90 30"
|
|
112
|
+
/>
|
|
113
|
+
</svg>
|
|
114
|
+
<span>{t(applyingLabel)}</span>
|
|
115
|
+
</>
|
|
116
|
+
) : (
|
|
117
|
+
t(ctaLabel)
|
|
118
|
+
)}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Dialog, DialogContent, DialogTitle } from '../Modal/Dialog';
|
|
3
|
+
import { ScopeSelectForm } from './ScopeSelectForm';
|
|
4
|
+
import { useMediaQuery } from 'react-responsive';
|
|
5
|
+
import {
|
|
6
|
+
Drawer,
|
|
7
|
+
DrawerContent,
|
|
8
|
+
DrawerHeader,
|
|
9
|
+
DrawerTitle,
|
|
10
|
+
} from 'components/Drawer/Drawer';
|
|
11
|
+
import { useTranslation } from 'react-i18next';
|
|
12
|
+
import CustomCamera from 'components/CustomCameraDrawer';
|
|
13
|
+
import {
|
|
14
|
+
extractScopeIdFromTypePlate,
|
|
15
|
+
useImageSearch,
|
|
16
|
+
} from 'hooks/useImageSearch';
|
|
17
|
+
import { lookupScopeById } from 'services/scopeLookup';
|
|
18
|
+
import { identifyTypePlateImage } from './identifyTypePlateImage';
|
|
19
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
20
|
+
import useRequestStore from 'stores/request/requestStore';
|
|
21
|
+
import { firstMachinePartsListingText } from 'stores/machinePartsListing/machinePartsListingStore';
|
|
22
|
+
import {
|
|
23
|
+
AppliedScopeInput,
|
|
24
|
+
ScopeSource,
|
|
25
|
+
toImageFile,
|
|
26
|
+
} from './scopeSelectHelpers';
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
openModal: boolean;
|
|
30
|
+
handleClose: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ScopeSelectModal({
|
|
34
|
+
openModal = false,
|
|
35
|
+
handleClose,
|
|
36
|
+
}: Props): JSX.Element {
|
|
37
|
+
const isMobile = useMediaQuery({ query: '(max-width: 776px)' });
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
const [manualScopeId, setManualScopeId] = useState('');
|
|
40
|
+
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
|
41
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
42
|
+
const [activeSource, setActiveSource] = useState<ScopeSource | null>(null);
|
|
43
|
+
const [lastAppliedInput, setLastAppliedInput] =
|
|
44
|
+
useState<AppliedScopeInput | null>(null);
|
|
45
|
+
const [lastExtractedFromImage, setLastExtractedFromImage] = useState('');
|
|
46
|
+
const [isManualMode, setIsManualMode] = useState(false);
|
|
47
|
+
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
|
48
|
+
const [isExtracting, setIsExtracting] = useState(false);
|
|
49
|
+
const [scopeError, setScopeError] = useState<string | null>(null);
|
|
50
|
+
const extractGenerationRef = useRef(0);
|
|
51
|
+
const activeSourceRef = useRef<ScopeSource | null>(null);
|
|
52
|
+
const isOpenRef = useRef(openModal);
|
|
53
|
+
const discardPendingScopeRef = useRef<(() => void) | null>(null);
|
|
54
|
+
|
|
55
|
+
const resetFormState = useCallback(() => {
|
|
56
|
+
extractGenerationRef.current += 1;
|
|
57
|
+
setIsExtracting(false);
|
|
58
|
+
setScopeError(null);
|
|
59
|
+
setIsManualMode(false);
|
|
60
|
+
setManualScopeId('');
|
|
61
|
+
setPendingFile(null);
|
|
62
|
+
setLastExtractedFromImage('');
|
|
63
|
+
setActiveSource(null);
|
|
64
|
+
activeSourceRef.current = null;
|
|
65
|
+
setLastAppliedInput(null);
|
|
66
|
+
setPreviewUrl(prev => {
|
|
67
|
+
if (prev) {
|
|
68
|
+
try {
|
|
69
|
+
URL.revokeObjectURL(prev);
|
|
70
|
+
} catch {}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const closeModal = useCallback(() => {
|
|
77
|
+
discardPendingScopeRef.current?.();
|
|
78
|
+
discardPendingScopeRef.current = null;
|
|
79
|
+
setIsCameraOpen(false);
|
|
80
|
+
resetFormState();
|
|
81
|
+
handleClose();
|
|
82
|
+
}, [handleClose, resetFormState]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
isOpenRef.current = openModal;
|
|
86
|
+
if (!openModal) {
|
|
87
|
+
resetFormState();
|
|
88
|
+
}
|
|
89
|
+
}, [openModal, resetFormState]);
|
|
90
|
+
|
|
91
|
+
const handleActiveSourceChange = useCallback(
|
|
92
|
+
(source: ScopeSource | null) => {
|
|
93
|
+
activeSourceRef.current = source;
|
|
94
|
+
setActiveSource(source);
|
|
95
|
+
},
|
|
96
|
+
[],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const location = useLocation();
|
|
100
|
+
const navigate = useNavigate();
|
|
101
|
+
const { singleImageSearch, searchMachinePartsListing } = useImageSearch();
|
|
102
|
+
|
|
103
|
+
const handleScopeSuccess = useCallback(() => {
|
|
104
|
+
closeModal();
|
|
105
|
+
const { query, requestImages, regions } = useRequestStore.getState();
|
|
106
|
+
const keyword = (query || '').trim();
|
|
107
|
+
const searchImage = requestImages[0];
|
|
108
|
+
const onResults = location.pathname.startsWith('/result');
|
|
109
|
+
|
|
110
|
+
if (searchImage) {
|
|
111
|
+
if (!onResults) {
|
|
112
|
+
navigate('/result');
|
|
113
|
+
}
|
|
114
|
+
void singleImageSearch({
|
|
115
|
+
image: searchImage,
|
|
116
|
+
imageRegion: regions[0],
|
|
117
|
+
settings: window.settings,
|
|
118
|
+
showFeedback: true,
|
|
119
|
+
newSearch: false,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (keyword) {
|
|
125
|
+
if (!onResults) {
|
|
126
|
+
navigate('/result');
|
|
127
|
+
}
|
|
128
|
+
void singleImageSearch({
|
|
129
|
+
text: keyword,
|
|
130
|
+
settings: window.settings,
|
|
131
|
+
showFeedback: true,
|
|
132
|
+
newSearch: false,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const text = firstMachinePartsListingText();
|
|
138
|
+
if (!text) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!onResults) {
|
|
143
|
+
navigate('/result');
|
|
144
|
+
}
|
|
145
|
+
void searchMachinePartsListing({
|
|
146
|
+
text,
|
|
147
|
+
showFeedback: true,
|
|
148
|
+
clearPostFilters: true,
|
|
149
|
+
});
|
|
150
|
+
}, [
|
|
151
|
+
closeModal,
|
|
152
|
+
location.pathname,
|
|
153
|
+
navigate,
|
|
154
|
+
searchMachinePartsListing,
|
|
155
|
+
singleImageSearch,
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const setSelectedImage = useCallback(
|
|
159
|
+
(file: File) => {
|
|
160
|
+
discardPendingScopeRef.current?.();
|
|
161
|
+
setIsManualMode(false);
|
|
162
|
+
setManualScopeId('');
|
|
163
|
+
const nextPreviewUrl = URL.createObjectURL(file);
|
|
164
|
+
setPreviewUrl(prev => {
|
|
165
|
+
if (prev && prev !== nextPreviewUrl) {
|
|
166
|
+
try {
|
|
167
|
+
URL.revokeObjectURL(prev);
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
return nextPreviewUrl;
|
|
171
|
+
});
|
|
172
|
+
setPendingFile(file);
|
|
173
|
+
setLastExtractedFromImage('');
|
|
174
|
+
setScopeError(null);
|
|
175
|
+
handleActiveSourceChange('image');
|
|
176
|
+
const extractGeneration = extractGenerationRef.current + 1;
|
|
177
|
+
extractGenerationRef.current = extractGeneration;
|
|
178
|
+
setIsExtracting(true);
|
|
179
|
+
const defaultErrorMessage =
|
|
180
|
+
window.settings.scopes?.typePlatePopupErrorMessage ||
|
|
181
|
+
'The serial number was not found. Please check again.';
|
|
182
|
+
void identifyTypePlateImage({
|
|
183
|
+
file,
|
|
184
|
+
isCurrent: () =>
|
|
185
|
+
extractGeneration === extractGenerationRef.current &&
|
|
186
|
+
isOpenRef.current &&
|
|
187
|
+
activeSourceRef.current === 'image',
|
|
188
|
+
extract: extractScopeIdFromTypePlate,
|
|
189
|
+
lookup: lookupScopeById,
|
|
190
|
+
})
|
|
191
|
+
.then(result => {
|
|
192
|
+
if (result.status === 'ignored') {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (result.status !== 'identified') {
|
|
196
|
+
setLastExtractedFromImage('');
|
|
197
|
+
setScopeError(t(defaultErrorMessage));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setLastExtractedFromImage(result.serialNumber);
|
|
202
|
+
setScopeError(null);
|
|
203
|
+
})
|
|
204
|
+
.finally(() => {
|
|
205
|
+
if (
|
|
206
|
+
extractGeneration === extractGenerationRef.current &&
|
|
207
|
+
isOpenRef.current
|
|
208
|
+
) {
|
|
209
|
+
setIsExtracting(false);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
[handleActiveSourceChange, t],
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const clearPreview = useCallback(() => {
|
|
217
|
+
extractGenerationRef.current += 1;
|
|
218
|
+
setIsExtracting(false);
|
|
219
|
+
setScopeError(null);
|
|
220
|
+
setPreviewUrl(prev => {
|
|
221
|
+
if (prev) {
|
|
222
|
+
try {
|
|
223
|
+
URL.revokeObjectURL(prev);
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
});
|
|
228
|
+
setPendingFile(null);
|
|
229
|
+
setLastExtractedFromImage('');
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
const handleSwitchToManual = useCallback(() => {
|
|
233
|
+
setIsManualMode(true);
|
|
234
|
+
setScopeError(null);
|
|
235
|
+
handleActiveSourceChange('manual');
|
|
236
|
+
if (previewUrl) {
|
|
237
|
+
clearPreview();
|
|
238
|
+
}
|
|
239
|
+
}, [clearPreview, handleActiveSourceChange, previewUrl]);
|
|
240
|
+
|
|
241
|
+
const isSuccess =
|
|
242
|
+
Boolean(lastExtractedFromImage) && !scopeError && !isExtracting;
|
|
243
|
+
const popupTitle = isSuccess
|
|
244
|
+
? window.settings.scopes?.typePlatePopupTitleSuccess || 'Machine identified'
|
|
245
|
+
: window.settings.scopes?.typePlatePopupTitle || 'Identify your machine';
|
|
246
|
+
|
|
247
|
+
const form = (
|
|
248
|
+
<ScopeSelectForm
|
|
249
|
+
handleClose={closeModal}
|
|
250
|
+
manualScopeId={manualScopeId}
|
|
251
|
+
pendingFile={pendingFile}
|
|
252
|
+
previewUrl={previewUrl}
|
|
253
|
+
activeSource={activeSource}
|
|
254
|
+
lastAppliedInput={lastAppliedInput}
|
|
255
|
+
lastExtractedFromImage={lastExtractedFromImage}
|
|
256
|
+
isExtracting={isExtracting}
|
|
257
|
+
scopeError={scopeError}
|
|
258
|
+
isManualMode={isManualMode}
|
|
259
|
+
onManualScopeIdChange={setManualScopeId}
|
|
260
|
+
onSelectedImage={setSelectedImage}
|
|
261
|
+
onClearPreview={clearPreview}
|
|
262
|
+
onOpenCamera={() => setIsCameraOpen(true)}
|
|
263
|
+
onApplied={setLastAppliedInput}
|
|
264
|
+
onActiveSourceChange={handleActiveSourceChange}
|
|
265
|
+
onExtractedFromImage={setLastExtractedFromImage}
|
|
266
|
+
onScopeErrorChange={setScopeError}
|
|
267
|
+
onSwitchToManual={handleSwitchToManual}
|
|
268
|
+
onSuccess={handleScopeSuccess}
|
|
269
|
+
discardPendingScopeRef={discardPendingScopeRef}
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
{!isMobile ? (
|
|
276
|
+
<Dialog open={openModal} onOpenChange={open => !open && closeModal()}>
|
|
277
|
+
<DialogContent
|
|
278
|
+
className="bg-white p-0 max-w-[640px] w-[90%] max-h-[95vh] m-auto overflow-y-hidden rounded-lg pt-2.5"
|
|
279
|
+
closeButton={false}
|
|
280
|
+
>
|
|
281
|
+
<DialogTitle className="h-0 w-0 hidden">
|
|
282
|
+
{t(popupTitle)}
|
|
283
|
+
</DialogTitle>
|
|
284
|
+
{form}
|
|
285
|
+
</DialogContent>
|
|
286
|
+
</Dialog>
|
|
287
|
+
) : (
|
|
288
|
+
<Drawer open={openModal} onOpenChange={open => !open && closeModal()}>
|
|
289
|
+
<DrawerContent className="bg-white p-0 m-auto overflow-y-hidden pt-2.5 h-full outline-none rounded-none">
|
|
290
|
+
<DrawerHeader className="h-0 w-0 hidden">
|
|
291
|
+
<DrawerTitle>{t(popupTitle)}</DrawerTitle>
|
|
292
|
+
</DrawerHeader>
|
|
293
|
+
{form}
|
|
294
|
+
</DrawerContent>
|
|
295
|
+
</Drawer>
|
|
296
|
+
)}
|
|
297
|
+
<CustomCamera
|
|
298
|
+
show={isCameraOpen}
|
|
299
|
+
onClose={() => setIsCameraOpen(false)}
|
|
300
|
+
handleImageUpload={(image: File | string) => {
|
|
301
|
+
void toImageFile(image).then(file => {
|
|
302
|
+
if (file) {
|
|
303
|
+
setSelectedImage(file);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
</>
|
|
309
|
+
);
|
|
310
|
+
}
|