@nyris/nyris-webapp 0.3.96 → 0.3.98
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 +6 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +7 -1
- package/build/static/css/main.81216c5d.css +4 -0
- package/build/static/css/main.81216c5d.css.map +1 -0
- package/build/static/js/main.93fdbd6e.js +3 -0
- package/build/static/js/{main.fd12cce3.js.map → main.93fdbd6e.js.map} +1 -1
- package/package.json +3 -3
- package/public/js/settings.example.js +7 -1
- package/src/components/AuthApiKeySync.tsx +34 -0
- package/src/components/Cart.tsx +200 -85
- package/src/components/ChatAssistant/useChatAssistantLogic.ts +3 -2
- package/src/components/Header.tsx +1 -1
- package/src/components/ImageUpload.tsx +2 -2
- package/src/components/Inquiry/InquiryModal.tsx +2 -2
- package/src/components/LogoutModal.tsx +2 -0
- package/src/components/PostFilter/PostFilter.tsx +36 -3
- package/src/components/PostFilter/PostFilterComponent.tsx +8 -22
- package/src/components/PostFilter/PostFilterFindApi.tsx +41 -3
- package/src/components/PreFilter/PreFilter.tsx +30 -2
- package/src/components/Product/Product.tsx +2 -2
- package/src/components/Product/ProductDetailView.tsx +2 -2
- package/src/components/Product/ProductList.tsx +2 -0
- package/src/components/ScopeMissing.tsx +50 -0
- package/src/components/SidePanel.tsx +10 -0
- package/src/components/SmartFilter.tsx +59 -69
- package/src/components/TextSearch.tsx +6 -4
- package/src/hooks/useImageSearch.ts +44 -9
- package/src/hooks/useResultEvaluator.ts +8 -8
- package/src/index.tsx +4 -0
- package/src/layouts/AppLayout.tsx +77 -10
- package/src/pages/Logout.tsx +2 -0
- package/src/pages/Result.tsx +14 -7
- package/src/providers/AuthProvider.tsx +5 -0
- package/src/routes/ProctedRoute.tsx +52 -9
- package/src/services/Feedback.ts +2 -1
- package/src/services/filter.ts +16 -2
- package/src/services/visualSearch.ts +15 -5
- package/src/services/vizo.ts +13 -2
- package/src/stores/scope/scopeStore.ts +35 -0
- package/src/styles/Cart.scss +22 -0
- package/src/types.ts +6 -1
- package/src/utils/derivePostFilterFacets.ts +68 -0
- package/src/utils/nyrisApiKey.ts +115 -0
- package/build/static/css/main.5fff4ca0.css +0 -4
- package/build/static/css/main.5fff4ca0.css.map +0 -1
- package/build/static/js/main.fd12cce3.js +0 -3
- /package/build/static/js/{main.fd12cce3.js.LICENSE.txt → main.93fdbd6e.js.LICENSE.txt} +0 -0
|
@@ -22,6 +22,12 @@ import { getFilters } from 'services/filter';
|
|
|
22
22
|
import { useUploadSearch } from 'hooks/useUploadSearch';
|
|
23
23
|
import { ToastHelper } from 'helpers/ToastHelper';
|
|
24
24
|
import { useTranslation } from 'react-i18next';
|
|
25
|
+
import {
|
|
26
|
+
clearAuthApiKey,
|
|
27
|
+
isNyrisApiKeyReady,
|
|
28
|
+
useAuthApiKeyStore,
|
|
29
|
+
} from 'utils/nyrisApiKey';
|
|
30
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
25
31
|
|
|
26
32
|
i18n.use(initReactI18next).init({
|
|
27
33
|
resources: translations,
|
|
@@ -33,7 +39,7 @@ i18n.use(initReactI18next).init({
|
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
function AppLayout(): JSX.Element {
|
|
36
|
-
const { isAuthenticated } = useAuth0();
|
|
42
|
+
const { isAuthenticated, logout } = useAuth0();
|
|
37
43
|
const { t } = useTranslation();
|
|
38
44
|
const { auth0, alogoliaFilterField, showPoweredByNyris } = window.settings;
|
|
39
45
|
const { uploadFile } = useUploadSearch();
|
|
@@ -49,6 +55,7 @@ function AppLayout(): JSX.Element {
|
|
|
49
55
|
|
|
50
56
|
const algoliaFilter = useRequestStore(state => state.algoliaFilter);
|
|
51
57
|
const metaFilter = useRequestStore(state => state.metaFilter);
|
|
58
|
+
const scopeItemKeys = useScopeStore(state => state.filterAttributes);
|
|
52
59
|
|
|
53
60
|
const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
|
|
54
61
|
const setIsAlgoliaLoading = useUiStore(state => state.setIsAlgoliaLoading);
|
|
@@ -88,7 +95,7 @@ function AppLayout(): JSX.Element {
|
|
|
88
95
|
useEffect(() => {
|
|
89
96
|
setAlgoliaProducts(items);
|
|
90
97
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
91
|
-
}, [items]);
|
|
98
|
+
}, [items, scopeItemKeys]);
|
|
92
99
|
|
|
93
100
|
useEffect(() => {
|
|
94
101
|
if (status === 'loading' || status === 'stalled') {
|
|
@@ -112,13 +119,13 @@ function AppLayout(): JSX.Element {
|
|
|
112
119
|
|
|
113
120
|
const showPreFilter = useMemo(() => {
|
|
114
121
|
if (settings.shouldUseUserMetadata && user) {
|
|
115
|
-
if (user['/user_metadata']
|
|
122
|
+
if (user['/user_metadata']?.value) {
|
|
116
123
|
setMetaFilter(user['/user_metadata'].value);
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
if (settings.shouldUseUserMetadata && user) {
|
|
121
|
-
if (settings.preFilterOption && !user['/user_metadata']
|
|
128
|
+
if (settings.preFilterOption && !user['/user_metadata']?.value) {
|
|
122
129
|
return true;
|
|
123
130
|
}
|
|
124
131
|
return false;
|
|
@@ -132,7 +139,15 @@ function AppLayout(): JSX.Element {
|
|
|
132
139
|
user,
|
|
133
140
|
]);
|
|
134
141
|
|
|
142
|
+
const authApiKeyVersion = useAuthApiKeyStore(state => state.version);
|
|
143
|
+
const isMissingAuthApiKey = useAuthApiKeyStore(state => state.isMissingClaim);
|
|
144
|
+
const isApiKeyReady = isNyrisApiKeyReady();
|
|
145
|
+
|
|
135
146
|
const getPreFilters = useCallback(async () => {
|
|
147
|
+
if (!isNyrisApiKeyReady()) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
setPreFilterLoading(true);
|
|
137
152
|
try {
|
|
138
153
|
const res = await getFilters(1000, settings);
|
|
@@ -145,10 +160,10 @@ function AppLayout(): JSX.Element {
|
|
|
145
160
|
}, [settings, setPreFilterList, setPreFilterLoading]);
|
|
146
161
|
|
|
147
162
|
useEffect(() => {
|
|
148
|
-
if (showPreFilter) {
|
|
163
|
+
if (showPreFilter && !settings.scopeFilter && isApiKeyReady) {
|
|
149
164
|
getPreFilters();
|
|
150
165
|
}
|
|
151
|
-
}, [getPreFilters, showPreFilter]);
|
|
166
|
+
}, [getPreFilters, showPreFilter, settings.scopeFilter, isApiKeyReady, authApiKeyVersion]);
|
|
152
167
|
|
|
153
168
|
const getImageFromClipboard = useCallback(
|
|
154
169
|
(event: ClipboardEvent): File | null => {
|
|
@@ -206,17 +221,69 @@ function AppLayout(): JSX.Element {
|
|
|
206
221
|
return;
|
|
207
222
|
}
|
|
208
223
|
|
|
224
|
+
// Auth0: only listen once authApiKey is resolved (not while waiting / missing claim).
|
|
225
|
+
if (auth0.enabled && !isApiKeyReady) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
209
229
|
document.addEventListener('paste', handleClipboardPaste);
|
|
210
230
|
|
|
211
231
|
return () => {
|
|
212
232
|
document.removeEventListener('paste', handleClipboardPaste);
|
|
213
233
|
};
|
|
214
|
-
}, [handleClipboardPaste, showLayout]);
|
|
234
|
+
}, [auth0.enabled, handleClipboardPaste, isApiKeyReady, showLayout]);
|
|
235
|
+
|
|
236
|
+
const applyScopeFilter = (filters: string | undefined) => {
|
|
237
|
+
const scopeFilterClause =
|
|
238
|
+
window.settings.scopeFilter && scopeItemKeys.length
|
|
239
|
+
? `(${scopeItemKeys[0].values.map(key => `${window.settings.alogoliaFilterField}:'${key}'`).join(' OR ')})`
|
|
240
|
+
: '';
|
|
241
|
+
|
|
242
|
+
if (!window.settings.scopeFilter || !window.settings.auth0?.enabled) {
|
|
243
|
+
return filters;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!scopeFilterClause) {
|
|
247
|
+
return `sku:NOT_FOUND`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return filters
|
|
251
|
+
? `${scopeFilterClause} AND (${filters})`
|
|
252
|
+
: scopeFilterClause;
|
|
253
|
+
};
|
|
215
254
|
|
|
216
255
|
if (!showLayout) {
|
|
217
256
|
return <Outlet />;
|
|
218
257
|
}
|
|
219
258
|
|
|
259
|
+
// Auth0 mode: wait for AuthApiKeySync before rendering search UI / prefilters.
|
|
260
|
+
if (auth0.enabled && !isApiKeyReady && !isMissingAuthApiKey) {
|
|
261
|
+
return <div />;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (isMissingAuthApiKey) {
|
|
265
|
+
return (
|
|
266
|
+
<div className="full-height flex flex-col items-center justify-center bg-[#F3F4F8] p-6 text-center">
|
|
267
|
+
<p className="mt-2 max-w-md text-sm text-gray-600">
|
|
268
|
+
Search is currently unavailable for your account. Please contact your
|
|
269
|
+
administrator.
|
|
270
|
+
</p>
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
className="mt-6 bg-[#2B2C46] px-6 py-3 text-sm text-white"
|
|
274
|
+
onClick={() => {
|
|
275
|
+
clearAuthApiKey();
|
|
276
|
+
logout({
|
|
277
|
+
logoutParams: { returnTo: window.location.origin },
|
|
278
|
+
});
|
|
279
|
+
}}
|
|
280
|
+
>
|
|
281
|
+
{t('Log out')}
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
220
287
|
return (
|
|
221
288
|
<div className="full-height flex flex-col bg-[#F3F4F8]">
|
|
222
289
|
<div>
|
|
@@ -226,7 +293,7 @@ function AppLayout(): JSX.Element {
|
|
|
226
293
|
{window.settings?.algolia.enabled && (
|
|
227
294
|
<Configure
|
|
228
295
|
query={query}
|
|
229
|
-
filters={
|
|
296
|
+
filters={applyScopeFilter(
|
|
230
297
|
specifications && !requestImages.length
|
|
231
298
|
? `${alogoliaFilterField}:'${specifications.prefilter_value}'`
|
|
232
299
|
: !query && !algoliaFilter.includes('score=1')
|
|
@@ -237,8 +304,8 @@ function AppLayout(): JSX.Element {
|
|
|
237
304
|
algoliaFilter ? 'AND ' : ''
|
|
238
305
|
}${alogoliaFilterField}:'${metaFilter}'`
|
|
239
306
|
: ''
|
|
240
|
-
}
|
|
241
|
-
}
|
|
307
|
+
}`,
|
|
308
|
+
)}
|
|
242
309
|
// facets={['brand', 'keyword_0']}
|
|
243
310
|
hitsPerPage={20}
|
|
244
311
|
/>
|
package/src/pages/Logout.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
3
|
+
import { clearAuthApiKey } from 'utils/nyrisApiKey';
|
|
3
4
|
|
|
4
5
|
const Logout = () => {
|
|
5
6
|
const { user, isAuthenticated, isLoading, loginWithRedirect, logout } =
|
|
@@ -10,6 +11,7 @@ const Logout = () => {
|
|
|
10
11
|
if (!isLoading && !user && !isAuthenticated && auth0.enabled) {
|
|
11
12
|
loginWithRedirect();
|
|
12
13
|
} else if (isAuthenticated) {
|
|
14
|
+
clearAuthApiKey();
|
|
13
15
|
logout({ logoutParams: { returnTo: window.location.origin } });
|
|
14
16
|
} else if (!auth0.enabled) {
|
|
15
17
|
window.location.href = '/';
|
package/src/pages/Result.tsx
CHANGED
|
@@ -174,16 +174,21 @@ function Results() {
|
|
|
174
174
|
]);
|
|
175
175
|
|
|
176
176
|
const disablePostFilter = useMemo(() => {
|
|
177
|
-
|
|
177
|
+
// Find-API post-filters do not depend on Algolia hits.
|
|
178
|
+
if (!settings.algolia.enabled || settings.postFilterOption) {
|
|
178
179
|
return false;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
|
-
return
|
|
182
|
-
? false
|
|
183
|
-
: true;
|
|
182
|
+
return productsFromAlgolia.length > 0 ? false : true;
|
|
184
183
|
}, [settings, productsFromAlgolia]);
|
|
185
184
|
|
|
186
|
-
const
|
|
185
|
+
const postFilterSelections = useRequestStore(
|
|
186
|
+
state => state.postFilterSelections,
|
|
187
|
+
);
|
|
188
|
+
const hasFindApiPostFilterSelection = Object.values(
|
|
189
|
+
postFilterSelections || {},
|
|
190
|
+
).some(values => values?.length > 0);
|
|
191
|
+
const isPostFilterApplied = items.length > 0 || hasFindApiPostFilterSelection;
|
|
187
192
|
|
|
188
193
|
return (
|
|
189
194
|
<>
|
|
@@ -322,7 +327,7 @@ function Results() {
|
|
|
322
327
|
>
|
|
323
328
|
{t('Showing results for machine', {
|
|
324
329
|
prefilter_title:
|
|
325
|
-
window.settings.
|
|
330
|
+
window.settings.visualSearchFilterKey,
|
|
326
331
|
prefilter_value: specifications.prefilter_value,
|
|
327
332
|
})}
|
|
328
333
|
</div>
|
|
@@ -364,7 +369,8 @@ function Results() {
|
|
|
364
369
|
|
|
365
370
|
{feedbackStatus === 'visible' &&
|
|
366
371
|
!showFeedbackSuccess &&
|
|
367
|
-
productsFromAlgolia.length > 0
|
|
372
|
+
(productsFromAlgolia.length > 0 ||
|
|
373
|
+
productsFromFindApi.length > 0) && (
|
|
368
374
|
<div className={'feedback-floating'}>
|
|
369
375
|
<div className="feedback-section feedback-backdrop-blur" />
|
|
370
376
|
|
|
@@ -570,6 +576,7 @@ function Results() {
|
|
|
570
576
|
onClose={() => {
|
|
571
577
|
setOpenModalCamera(s => !s);
|
|
572
578
|
}}
|
|
579
|
+
newSearch={true}
|
|
573
580
|
/>
|
|
574
581
|
{isAiModeOpen && (
|
|
575
582
|
<div className="desktop:hidden">
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Auth0Provider } from '@auth0/auth0-react';
|
|
2
|
+
import { AuthApiKeySync } from 'components/AuthApiKeySync';
|
|
3
|
+
import { bootstrapAuth0ApiKeyGuard } from 'utils/nyrisApiKey';
|
|
4
|
+
|
|
5
|
+
bootstrapAuth0ApiKeyGuard();
|
|
2
6
|
|
|
3
7
|
const AuthProvider = ({ children }: any) => {
|
|
4
8
|
const settings = window.settings;
|
|
@@ -15,6 +19,7 @@ const AuthProvider = ({ children }: any) => {
|
|
|
15
19
|
redirect_uri: window.location.origin,
|
|
16
20
|
}}
|
|
17
21
|
>
|
|
22
|
+
<AuthApiKeySync />
|
|
18
23
|
{children}
|
|
19
24
|
</Auth0Provider>
|
|
20
25
|
);
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
2
2
|
import EmailVerification from 'components/EmailVerification';
|
|
3
|
+
import ScopeMissing from 'components/ScopeMissing';
|
|
3
4
|
import { useEffect, ReactElement } from 'react';
|
|
5
|
+
import { getScopes } from 'services/filter';
|
|
6
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
4
7
|
|
|
5
8
|
export default function ProtectedRoute({
|
|
6
9
|
children,
|
|
7
10
|
}: {
|
|
8
11
|
children: ReactElement;
|
|
9
12
|
}) {
|
|
10
|
-
const { auth0 } = window.settings;
|
|
13
|
+
const { auth0, scopeFilter } = window.settings;
|
|
11
14
|
|
|
12
15
|
const { user, isAuthenticated, isLoading, loginWithRedirect } = useAuth0();
|
|
16
|
+
const setScope = useScopeStore(state => state.setScope);
|
|
17
|
+
const filterAttributes = useScopeStore(state => state.filterAttributes);
|
|
18
|
+
|
|
19
|
+
const setScopeLoading = useScopeStore(state => state.setScopeLoading);
|
|
20
|
+
const scopeIsLoading = useScopeStore(state => state.isLoading);
|
|
21
|
+
const hasFetchedScope = useScopeStore(state => state.hasFetched);
|
|
22
|
+
const setScopeFetched = useScopeStore(state => state.setScopeFetched);
|
|
13
23
|
|
|
14
24
|
useEffect(() => {
|
|
15
25
|
if (!isLoading && !user && !isAuthenticated && auth0.enabled) {
|
|
@@ -17,21 +27,54 @@ export default function ProtectedRoute({
|
|
|
17
27
|
}
|
|
18
28
|
}, [loginWithRedirect, isLoading, user, isAuthenticated, auth0]);
|
|
19
29
|
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!scopeFilter || !isAuthenticated || !user?.email || hasFetchedScope) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setScopeFetched(true);
|
|
35
|
+
setScopeLoading(true);
|
|
36
|
+
getScopes(user.email, window.settings)
|
|
37
|
+
.then(response => {
|
|
38
|
+
setScope(response.itemKeys || [], response.filterAttributes || []);
|
|
39
|
+
})
|
|
40
|
+
.catch((e: any) => {
|
|
41
|
+
console.log('err getScopes', e);
|
|
42
|
+
})
|
|
43
|
+
.finally(() => {
|
|
44
|
+
setScopeLoading(false);
|
|
45
|
+
});
|
|
46
|
+
}, [
|
|
47
|
+
scopeFilter,
|
|
48
|
+
isAuthenticated,
|
|
49
|
+
user,
|
|
50
|
+
hasFetchedScope,
|
|
51
|
+
setScope,
|
|
52
|
+
setScopeLoading,
|
|
53
|
+
setScopeFetched,
|
|
54
|
+
]);
|
|
55
|
+
|
|
20
56
|
if (!auth0.enabled) {
|
|
21
57
|
return children;
|
|
22
58
|
}
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// }
|
|
27
|
-
|
|
28
|
-
if (!isLoading && isAuthenticated && user?.email_verified) {
|
|
29
|
-
return children;
|
|
60
|
+
if (isLoading || !isAuthenticated) {
|
|
61
|
+
return <div></div>;
|
|
30
62
|
}
|
|
31
63
|
|
|
32
|
-
if (!
|
|
64
|
+
if (!user?.email_verified) {
|
|
33
65
|
return <EmailVerification />;
|
|
34
66
|
}
|
|
35
67
|
|
|
36
|
-
|
|
68
|
+
if (scopeFilter) {
|
|
69
|
+
// wait for the scope fetch to finish before deciding access, otherwise ScopeMissing flashes early
|
|
70
|
+
if (scopeIsLoading || !hasFetchedScope) {
|
|
71
|
+
return <div></div>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (filterAttributes.length === 0) {
|
|
75
|
+
return <ScopeMissing />;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return children;
|
|
37
80
|
}
|
package/src/services/Feedback.ts
CHANGED
|
@@ -3,6 +3,7 @@ import NyrisAPI, {
|
|
|
3
3
|
NyrisAPISettings,
|
|
4
4
|
RectCoords,
|
|
5
5
|
} from '@nyris/nyris-api';
|
|
6
|
+
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
6
7
|
|
|
7
8
|
export const feedbackSuccessEpic = async (
|
|
8
9
|
requestId: string,
|
|
@@ -62,7 +63,7 @@ export const sendFeedbackByApi = async (
|
|
|
62
63
|
requestId: string | undefined,
|
|
63
64
|
payload: FeedbackEventPayload,
|
|
64
65
|
) => {
|
|
65
|
-
const api = new NyrisAPI(settings);
|
|
66
|
+
const api = new NyrisAPI(getNyrisSettings(settings));
|
|
66
67
|
if (requestId) {
|
|
67
68
|
try {
|
|
68
69
|
await api.sendFeedback({ sessionId, requestId, payload }).then(() => {});
|
package/src/services/filter.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import NyrisAPI, { NyrisAPISettings } from '@nyris/nyris-api';
|
|
2
|
+
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
2
3
|
|
|
3
4
|
export interface Filter {
|
|
4
5
|
key?: string;
|
|
@@ -9,7 +10,7 @@ export const getFilters = async (
|
|
|
9
10
|
number: number = 10,
|
|
10
11
|
settings: NyrisAPISettings,
|
|
11
12
|
) => {
|
|
12
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
13
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
13
14
|
return nyrisApi.getFilters(number);
|
|
14
15
|
};
|
|
15
16
|
|
|
@@ -18,7 +19,20 @@ export const searchFilters = async (
|
|
|
18
19
|
value: string,
|
|
19
20
|
settingsNyris: NyrisAPISettings,
|
|
20
21
|
) => {
|
|
21
|
-
const nyrisApi = new NyrisAPI(settingsNyris);
|
|
22
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settingsNyris));
|
|
22
23
|
const newValue = value ? value : '';
|
|
23
24
|
return nyrisApi.searchFilters(key, newValue);
|
|
24
25
|
};
|
|
26
|
+
|
|
27
|
+
export interface ScopeResponse {
|
|
28
|
+
itemKeys: string[];
|
|
29
|
+
filterAttributes: Filter[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const getScopes = async (
|
|
33
|
+
scopeIds: string,
|
|
34
|
+
settings: NyrisAPISettings,
|
|
35
|
+
): Promise<ScopeResponse> => {
|
|
36
|
+
const nyrisApi = new NyrisAPI(settings);
|
|
37
|
+
return nyrisApi.getScopes(scopeIds);
|
|
38
|
+
};
|
|
@@ -9,6 +9,7 @@ import { isEqual } from 'lodash';
|
|
|
9
9
|
|
|
10
10
|
import { DEFAULT_REGION } from '../constants';
|
|
11
11
|
import { getUserLocation } from 'helpers/getGeoLocation';
|
|
12
|
+
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
12
13
|
|
|
13
14
|
export interface Filter {
|
|
14
15
|
key?: string;
|
|
@@ -41,7 +42,7 @@ export const findRegions = async (
|
|
|
41
42
|
image: HTMLCanvasElement,
|
|
42
43
|
settings: NyrisAPISettings,
|
|
43
44
|
) => {
|
|
44
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
45
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
45
46
|
let regions = await nyrisApi.findRegions(image);
|
|
46
47
|
const selectedRegion = getRegionByMaxConfidence(regions);
|
|
47
48
|
return {
|
|
@@ -58,14 +59,16 @@ export const find = async ({
|
|
|
58
59
|
region,
|
|
59
60
|
filters,
|
|
60
61
|
text,
|
|
62
|
+
scopeIds,
|
|
61
63
|
}: {
|
|
62
64
|
image?: HTMLCanvasElement;
|
|
63
65
|
settings: NyrisAPISettings;
|
|
64
66
|
region?: RectCoords;
|
|
65
67
|
filters?: Filter[];
|
|
66
68
|
text?: string;
|
|
69
|
+
scopeIds?: string;
|
|
67
70
|
}) => {
|
|
68
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
71
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
69
72
|
let options: ImageSearchOptions = text ? { text } : {};
|
|
70
73
|
|
|
71
74
|
if (window.settings?.geoLocation) {
|
|
@@ -82,7 +85,14 @@ export const find = async ({
|
|
|
82
85
|
if (region) {
|
|
83
86
|
options = { ...options, cropRect: region };
|
|
84
87
|
}
|
|
85
|
-
return nyrisApi.find(
|
|
88
|
+
return nyrisApi.find(
|
|
89
|
+
options,
|
|
90
|
+
image,
|
|
91
|
+
filters,
|
|
92
|
+
undefined,
|
|
93
|
+
undefined,
|
|
94
|
+
scopeIds ? [scopeIds] : [],
|
|
95
|
+
);
|
|
86
96
|
};
|
|
87
97
|
|
|
88
98
|
export const findCad = ({
|
|
@@ -94,7 +104,7 @@ export const findCad = ({
|
|
|
94
104
|
settings: NyrisAPISettings;
|
|
95
105
|
filters?: Filter[];
|
|
96
106
|
}) => {
|
|
97
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
107
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
98
108
|
|
|
99
109
|
return nyrisApi.findByCad(file, {}, filters);
|
|
100
110
|
};
|
|
@@ -106,7 +116,7 @@ export const getRequestImage = ({
|
|
|
106
116
|
url: string;
|
|
107
117
|
settings: NyrisAPISettings;
|
|
108
118
|
}) => {
|
|
109
|
-
const nyrisApi = new NyrisAPI(settings);
|
|
119
|
+
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
110
120
|
|
|
111
121
|
return nyrisApi.getCadRequestImage(url);
|
|
112
122
|
};
|
package/src/services/vizo.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { RectCoords } from '@nyris/nyris-api';
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import { cropImage } from 'utils/cropImageToBase64';
|
|
4
|
+
import { getEffectiveApiKey } from 'utils/nyrisApiKey';
|
|
5
|
+
|
|
6
|
+
function resolveAiOrNyrisApiKey(explicitApiKey?: string): string {
|
|
7
|
+
if (explicitApiKey) {
|
|
8
|
+
return explicitApiKey;
|
|
9
|
+
}
|
|
10
|
+
if (window.settings.aiApiKey) {
|
|
11
|
+
return window.settings.aiApiKey;
|
|
12
|
+
}
|
|
13
|
+
return getEffectiveApiKey();
|
|
14
|
+
}
|
|
4
15
|
|
|
5
16
|
export interface ChatMultimodalInput {
|
|
6
17
|
metadata: string;
|
|
@@ -111,7 +122,7 @@ export async function rerankSmartFilters({
|
|
|
111
122
|
formData,
|
|
112
123
|
{
|
|
113
124
|
headers: {
|
|
114
|
-
'x-api-key': apiKey
|
|
125
|
+
'x-api-key': resolveAiOrNyrisApiKey(apiKey),
|
|
115
126
|
},
|
|
116
127
|
},
|
|
117
128
|
);
|
|
@@ -156,7 +167,7 @@ export async function generateSmartFilters({
|
|
|
156
167
|
formData,
|
|
157
168
|
{
|
|
158
169
|
headers: {
|
|
159
|
-
'x-api-key':
|
|
170
|
+
'x-api-key': resolveAiOrNyrisApiKey(),
|
|
160
171
|
},
|
|
161
172
|
},
|
|
162
173
|
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { Filter } from '@nyris/nyris-api';
|
|
3
|
+
|
|
4
|
+
interface ScopeState {
|
|
5
|
+
itemKeys: string[];
|
|
6
|
+
filterAttributes: Filter[];
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
hasFetched: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ScopeAction {
|
|
12
|
+
setScope: (itemKeys: string[], filterAttributes: Filter[]) => void;
|
|
13
|
+
setScopeLoading: (isLoading: boolean) => void;
|
|
14
|
+
setScopeFetched: (hasFetched: boolean) => void;
|
|
15
|
+
resetScope: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const initialState: ScopeState = {
|
|
19
|
+
itemKeys: [],
|
|
20
|
+
filterAttributes: [],
|
|
21
|
+
isLoading: false,
|
|
22
|
+
hasFetched: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const useScopeStore = create<ScopeState & ScopeAction>()(set => ({
|
|
26
|
+
...initialState,
|
|
27
|
+
|
|
28
|
+
setScope: (itemKeys, filterAttributes) =>
|
|
29
|
+
set({ itemKeys, filterAttributes, hasFetched: true }),
|
|
30
|
+
setScopeLoading: isLoading => set({ isLoading }),
|
|
31
|
+
setScopeFetched: hasFetched => set({ hasFetched }),
|
|
32
|
+
resetScope: () => set(initialState),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
export default useScopeStore;
|
package/src/styles/Cart.scss
CHANGED
|
@@ -205,6 +205,28 @@
|
|
|
205
205
|
margin-right: 8px;
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
|
+
.cart-email-button {
|
|
209
|
+
background-color: #fff;
|
|
210
|
+
color: #3e36dc;
|
|
211
|
+
border: 1px solid #3e36dc;
|
|
212
|
+
width: 100%;
|
|
213
|
+
height: 48px;
|
|
214
|
+
display: flex;
|
|
215
|
+
flex-direction: row;
|
|
216
|
+
align-items: center;
|
|
217
|
+
justify-content: center;
|
|
218
|
+
font-weight: 600;
|
|
219
|
+
font-size: 16px;
|
|
220
|
+
border-radius: 8px;
|
|
221
|
+
margin-top: 8px;
|
|
222
|
+
&:disabled {
|
|
223
|
+
opacity: 0.6;
|
|
224
|
+
cursor: not-allowed;
|
|
225
|
+
}
|
|
226
|
+
svg {
|
|
227
|
+
margin-right: 8px;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
208
230
|
}
|
|
209
231
|
}
|
|
210
232
|
}
|
package/src/types.ts
CHANGED
|
@@ -79,7 +79,11 @@ export interface AppSettings extends NyrisAPISettings {
|
|
|
79
79
|
brandName?: string;
|
|
80
80
|
cadenas?: Cadenas;
|
|
81
81
|
cadSearch?: boolean;
|
|
82
|
-
cart?:
|
|
82
|
+
cart?: {
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
buttonText: string;
|
|
85
|
+
emailTemplateId: string;
|
|
86
|
+
};
|
|
83
87
|
clarityId?: string;
|
|
84
88
|
CTAButton?: CTAButtonSettings;
|
|
85
89
|
experienceVisualSearch?: boolean;
|
|
@@ -110,6 +114,7 @@ export interface AppSettings extends NyrisAPISettings {
|
|
|
110
114
|
refinements?: any;
|
|
111
115
|
regions: boolean;
|
|
112
116
|
rfq?: Rfq;
|
|
117
|
+
scopeFilter?: boolean;
|
|
113
118
|
secondaryCTAButton?: SecondaryCTAButton;
|
|
114
119
|
secondaryCTAButtonText?: string;
|
|
115
120
|
secondaryTitle: string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
interface PostFilterFacetValue {
|
|
2
|
+
value: string;
|
|
3
|
+
count: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PostFilterFacet {
|
|
7
|
+
name: string;
|
|
8
|
+
values: PostFilterFacetValue[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ProductWithFilters {
|
|
12
|
+
filters?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Aggregates post-filter facet options from result.filters when the find API
|
|
17
|
+
* does not return top-level facets (e.g. visual search). Matches the pre-02ede11
|
|
18
|
+
* PostFilterFindApi counting behavior.
|
|
19
|
+
*/
|
|
20
|
+
export function derivePostFilterFacetsFromResults(
|
|
21
|
+
results: ProductWithFilters[] | undefined | null,
|
|
22
|
+
): PostFilterFacet[] {
|
|
23
|
+
if (!results?.length) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const countsByAttribute: Record<string, Record<string, number>> = {};
|
|
28
|
+
|
|
29
|
+
results.forEach(product => {
|
|
30
|
+
const filters = product?.filters;
|
|
31
|
+
if (!filters || typeof filters !== 'object') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Object.entries(filters).forEach(([attribute, filterValues]) => {
|
|
36
|
+
if (!Array.isArray(filterValues)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!countsByAttribute[attribute]) {
|
|
41
|
+
countsByAttribute[attribute] = {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
filterValues.forEach((val: unknown) => {
|
|
45
|
+
if (typeof val === 'string' && val) {
|
|
46
|
+
countsByAttribute[attribute][val] =
|
|
47
|
+
(countsByAttribute[attribute][val] || 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return Object.entries(countsByAttribute).map(([name, counts]) => ({
|
|
54
|
+
name,
|
|
55
|
+
values: Object.entries(counts).map(([value, count]) => ({ value, count })),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolvePostFilterFacets(
|
|
60
|
+
apiFacets: unknown,
|
|
61
|
+
results: ProductWithFilters[] | undefined | null,
|
|
62
|
+
): PostFilterFacet[] {
|
|
63
|
+
if (Array.isArray(apiFacets) && apiFacets.length > 0) {
|
|
64
|
+
return apiFacets as PostFilterFacet[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return derivePostFilterFacetsFromResults(results);
|
|
68
|
+
}
|