@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
|
@@ -10,7 +10,9 @@ import { isEmpty } from 'lodash';
|
|
|
10
10
|
import useResultStore from 'stores/result/resultStore';
|
|
11
11
|
import useRequestStore from 'stores/request/requestStore';
|
|
12
12
|
import useUiStore from 'stores/ui/uiStore';
|
|
13
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
13
14
|
import { find } from 'services/visualSearch';
|
|
15
|
+
import { useAuth0 } from '@auth0/auth0-react';
|
|
14
16
|
|
|
15
17
|
let postFilterRequestCounter = 0;
|
|
16
18
|
|
|
@@ -31,6 +33,8 @@ function PostFilterFindApi({
|
|
|
31
33
|
onVisibilityChange?: (attribute: string, isVisible: boolean) => void;
|
|
32
34
|
isLastVisible?: boolean;
|
|
33
35
|
}) {
|
|
36
|
+
const { user } = useAuth0();
|
|
37
|
+
|
|
34
38
|
const [itemsLimit, setItemsLimit] = useState(10);
|
|
35
39
|
const [searchInput, setSearchInput] = useState<string>('');
|
|
36
40
|
|
|
@@ -52,6 +56,23 @@ function PostFilterFindApi({
|
|
|
52
56
|
const { t } = useTranslation();
|
|
53
57
|
const settings = window.settings;
|
|
54
58
|
|
|
59
|
+
const scopeFilterEnabled = settings.scopeFilter;
|
|
60
|
+
const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
|
|
61
|
+
|
|
62
|
+
const scopedAllowedValues = useMemo(() => {
|
|
63
|
+
if (!scopeFilterEnabled || !window.settings.auth0?.enabled) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// todo: support multiple attributes in the future
|
|
67
|
+
const scopeAttribute = scopeFilterAttributes[0];
|
|
68
|
+
if (!scopeAttribute) {
|
|
69
|
+
return new Set<string>();
|
|
70
|
+
}
|
|
71
|
+
return new Set(
|
|
72
|
+
scopeAttribute.values.map(value => value.toLocaleLowerCase()),
|
|
73
|
+
);
|
|
74
|
+
}, [scopeFilterEnabled, scopeFilterAttributes]);
|
|
75
|
+
|
|
55
76
|
const matchedFacet = useMemo(() => {
|
|
56
77
|
const facets = Array.isArray(postFilterFacets) ? postFilterFacets : [];
|
|
57
78
|
return facets.find(
|
|
@@ -96,6 +117,11 @@ function PostFilterFindApi({
|
|
|
96
117
|
count: valueCounts[label] || 0,
|
|
97
118
|
isRefined: selectedValues.includes(label),
|
|
98
119
|
}))
|
|
120
|
+
.filter(
|
|
121
|
+
item =>
|
|
122
|
+
!scopedAllowedValues ||
|
|
123
|
+
scopedAllowedValues.has(item.value.toLocaleLowerCase()),
|
|
124
|
+
)
|
|
99
125
|
.filter(
|
|
100
126
|
item =>
|
|
101
127
|
!searchInput ||
|
|
@@ -103,13 +129,22 @@ function PostFilterFindApi({
|
|
|
103
129
|
)
|
|
104
130
|
.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label))
|
|
105
131
|
.slice(0, itemsLimit);
|
|
106
|
-
}, [
|
|
132
|
+
}, [
|
|
133
|
+
attribute,
|
|
134
|
+
itemsLimit,
|
|
135
|
+
postFilterSelections,
|
|
136
|
+
scopedAllowedValues,
|
|
137
|
+
searchInput,
|
|
138
|
+
valueCounts,
|
|
139
|
+
]);
|
|
107
140
|
|
|
108
141
|
const selectedValues = postFilterSelections?.[attribute] || [];
|
|
109
142
|
const hasSelection = selectedValues.length > 0;
|
|
110
143
|
const hasAvailableValues = Object.keys(valueCounts).length > 0;
|
|
111
144
|
const hasSearch = searchable && searchInput.length > 0;
|
|
112
|
-
const isVisible =
|
|
145
|
+
const isVisible =
|
|
146
|
+
(!scopedAllowedValues || scopedAllowedValues.size > 0) &&
|
|
147
|
+
(hasAvailableValues || hasSelection || hasSearch);
|
|
113
148
|
|
|
114
149
|
useEffect(() => {
|
|
115
150
|
onVisibilityChange?.(attribute, isVisible);
|
|
@@ -180,6 +215,7 @@ function PostFilterFindApi({
|
|
|
180
215
|
region: regions[0],
|
|
181
216
|
filters,
|
|
182
217
|
text: !requestImages[0] ? query : undefined,
|
|
218
|
+
scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
|
|
183
219
|
})
|
|
184
220
|
.then(response => {
|
|
185
221
|
if (requestId !== postFilterRequestCounter) {
|
|
@@ -264,7 +300,9 @@ function PostFilterFindApi({
|
|
|
264
300
|
<input
|
|
265
301
|
type="checkbox"
|
|
266
302
|
checked={item.isRefined}
|
|
267
|
-
onChange={() =>
|
|
303
|
+
onChange={() =>
|
|
304
|
+
handleFilterClick(item.value, item.isRefined)
|
|
305
|
+
}
|
|
268
306
|
className="cursor-pointer"
|
|
269
307
|
/>
|
|
270
308
|
<span className="text-xs font-normal pl-2 pr-1">
|
|
@@ -10,6 +10,7 @@ import { truncateString } from 'utils/truncateString';
|
|
|
10
10
|
import { twMerge } from 'tailwind-merge';
|
|
11
11
|
import Tooltip from 'components/Tooltip/TooltipComponent';
|
|
12
12
|
import useResultStore from '../../stores/result/resultStore';
|
|
13
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
handleClose?: any;
|
|
@@ -42,6 +43,9 @@ const PreFilterComponent = (props: Props) => {
|
|
|
42
43
|
const setImageAnalysis = useResultStore(state => state.setImageAnalysis);
|
|
43
44
|
const regions = useRequestStore(state => state.regions);
|
|
44
45
|
|
|
46
|
+
const scopeFilterEnabled = !!settings.scopeFilter;
|
|
47
|
+
const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
|
|
48
|
+
|
|
45
49
|
const [keyFilter, setKeyFilter] = useState<Record<string, boolean>>(
|
|
46
50
|
keyFilterState || {},
|
|
47
51
|
);
|
|
@@ -66,7 +70,7 @@ const PreFilterComponent = (props: Props) => {
|
|
|
66
70
|
setLoading(true);
|
|
67
71
|
getDataFilterDesktop();
|
|
68
72
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
-
}, [preFilterList, searchKey]);
|
|
73
|
+
}, [preFilterList, scopeFilterAttributes, searchKey]);
|
|
70
74
|
|
|
71
75
|
useEffect(() => {
|
|
72
76
|
return () => {
|
|
@@ -76,7 +80,11 @@ const PreFilterComponent = (props: Props) => {
|
|
|
76
80
|
};
|
|
77
81
|
}, []);
|
|
78
82
|
|
|
79
|
-
const
|
|
83
|
+
const activeFilterList = scopeFilterEnabled
|
|
84
|
+
? scopeFilterAttributes
|
|
85
|
+
: preFilterList;
|
|
86
|
+
|
|
87
|
+
const matchedPreFilter = activeFilterList.find(
|
|
80
88
|
value =>
|
|
81
89
|
value.key?.toLocaleLowerCase() ===
|
|
82
90
|
settings.visualSearchFilterKey?.toLocaleLowerCase(),
|
|
@@ -105,6 +113,26 @@ const PreFilterComponent = (props: Props) => {
|
|
|
105
113
|
return;
|
|
106
114
|
}
|
|
107
115
|
setLoading(true);
|
|
116
|
+
|
|
117
|
+
if (scopeFilterEnabled) {
|
|
118
|
+
const arrResult = matchedPreFilter?.values || [];
|
|
119
|
+
const lowerValue = String(value).toLocaleLowerCase();
|
|
120
|
+
const res = arrResult.filter(item =>
|
|
121
|
+
String(item).toLocaleLowerCase().includes(lowerValue),
|
|
122
|
+
);
|
|
123
|
+
if (res.length > 0) {
|
|
124
|
+
setResultFilter({ [res[0][0].toLocaleUpperCase()]: res });
|
|
125
|
+
if (res.length <= 20) setColumns(1);
|
|
126
|
+
else if (res.length <= 40) setColumns(2);
|
|
127
|
+
else setColumns(4);
|
|
128
|
+
} else {
|
|
129
|
+
setResultFilter({});
|
|
130
|
+
setColumns(4);
|
|
131
|
+
}
|
|
132
|
+
setLoading(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
const data = await searchFilters(
|
|
109
137
|
preFilterApiKey,
|
|
110
138
|
encodeURIComponent(value),
|
|
@@ -673,7 +673,7 @@ function Product(props: Props) {
|
|
|
673
673
|
</div>
|
|
674
674
|
)}
|
|
675
675
|
|
|
676
|
-
{settings.cart && (
|
|
676
|
+
{settings.cart?.enabled && (
|
|
677
677
|
<div
|
|
678
678
|
style={{
|
|
679
679
|
boxShadow: '-2px 2px 4px rgba(170, 171, 181, 0.5)',
|
|
@@ -717,7 +717,7 @@ function Product(props: Props) {
|
|
|
717
717
|
paddingRight: '8px',
|
|
718
718
|
}}
|
|
719
719
|
>
|
|
720
|
-
Add to cart
|
|
720
|
+
{settings.cart?.buttonText || 'Add to cart'}
|
|
721
721
|
</div>
|
|
722
722
|
<div style={{ width: '16px' }}>
|
|
723
723
|
<Icon
|
|
@@ -456,7 +456,7 @@ function ProductDetailView(props: Props) {
|
|
|
456
456
|
</div>
|
|
457
457
|
)}
|
|
458
458
|
</div>
|
|
459
|
-
{settings.cart && (
|
|
459
|
+
{settings.cart?.enabled && (
|
|
460
460
|
<div
|
|
461
461
|
className="bg-[#2B2C46] rounded mt-2 flex justify-between items-center h-8 desktop:h-12 w-full btn-detail-item"
|
|
462
462
|
style={{
|
|
@@ -472,7 +472,7 @@ function ProductDetailView(props: Props) {
|
|
|
472
472
|
}}
|
|
473
473
|
>
|
|
474
474
|
<div className="text-f16 fw-600 max-line-1 text-white pr-1.5">
|
|
475
|
-
Add to cart
|
|
475
|
+
{settings.cart?.buttonText || 'Add to cart'}
|
|
476
476
|
</div>
|
|
477
477
|
<div style={{ width: '16px' }}>
|
|
478
478
|
<Icon
|
|
@@ -63,6 +63,7 @@ function ProductList({ sendFeedBackAction }: Props): JSX.Element {
|
|
|
63
63
|
}, [productsFromFindApi, query, requestImages]);
|
|
64
64
|
|
|
65
65
|
const postFilteredProducts = useMemo(() => {
|
|
66
|
+
// Find-API post-filters re-query when Algolia is off; do not also client-filter.
|
|
66
67
|
if (!window.settings.algolia?.enabled && window.settings.postFilterOption) {
|
|
67
68
|
return textFilteredProducts;
|
|
68
69
|
}
|
|
@@ -114,6 +115,7 @@ function ProductList({ sendFeedBackAction }: Props): JSX.Element {
|
|
|
114
115
|
);
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Product source follows search backend (Algolia vs Find API), not postFilterOption.
|
|
117
119
|
if (!isAlgoliaEnabled) {
|
|
118
120
|
return baseFindApiProducts;
|
|
119
121
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const ScopeMissing = () => {
|
|
2
|
+
const { supportEmail } = window.settings.auth0;
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
<div className="pl-8 desktop:pl-16 pt-6 desktop:pt-8 flex flex-col gap-4 bg-white h-full">
|
|
6
|
+
<div
|
|
7
|
+
style={{
|
|
8
|
+
fontSize: '20px',
|
|
9
|
+
fontWeight: 'bold',
|
|
10
|
+
color: '#2B2C46',
|
|
11
|
+
}}
|
|
12
|
+
>
|
|
13
|
+
Access Denied
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
style={{
|
|
18
|
+
width: '200px',
|
|
19
|
+
height: '4px',
|
|
20
|
+
borderRadius: '4px',
|
|
21
|
+
}}
|
|
22
|
+
className="bg-red-500"
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
<p style={{ color: '#2B2C46', fontSize: '13px' }}>
|
|
26
|
+
You do not have access to the {window.settings.visualSearchFilterKey}.
|
|
27
|
+
If you believe this is an error, please contact support.
|
|
28
|
+
</p>
|
|
29
|
+
<a
|
|
30
|
+
className="contact-support"
|
|
31
|
+
style={{
|
|
32
|
+
backgroundColor: '#2B2C46',
|
|
33
|
+
padding: '8px 16px 8px 16px',
|
|
34
|
+
color: '#fff',
|
|
35
|
+
border: 'none',
|
|
36
|
+
cursor: 'pointer',
|
|
37
|
+
fontSize: '14px',
|
|
38
|
+
width: 'fit-content',
|
|
39
|
+
}}
|
|
40
|
+
href={`mailto:${
|
|
41
|
+
supportEmail || 'support@nyris.io'
|
|
42
|
+
}?subject=Access Denied&body=`}
|
|
43
|
+
>
|
|
44
|
+
Contact Support
|
|
45
|
+
</a>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default ScopeMissing;
|
|
@@ -18,6 +18,9 @@ export default function SidePanel({ className }: { className?: string }) {
|
|
|
18
18
|
const specificationFilter = useRequestStore(
|
|
19
19
|
state => state.specificationFilter,
|
|
20
20
|
);
|
|
21
|
+
const postFilterSelections = useRequestStore(
|
|
22
|
+
state => state.postFilterSelections,
|
|
23
|
+
);
|
|
21
24
|
|
|
22
25
|
const findApiProducts = useResultStore(state => state.productsFromFindApi);
|
|
23
26
|
|
|
@@ -45,13 +48,20 @@ export default function SidePanel({ className }: { className?: string }) {
|
|
|
45
48
|
|
|
46
49
|
const effectiveGroundingResults = useEffectiveGroundingResults();
|
|
47
50
|
|
|
51
|
+
const hasPostFilterSelections = Object.values(
|
|
52
|
+
postFilterSelections || {},
|
|
53
|
+
).some(values => values?.length > 0);
|
|
54
|
+
|
|
48
55
|
if (!showPostFilter && requestImages.length === 0) {
|
|
49
56
|
return <></>;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
// Keep the panel when post-filters are applied so the user can unselect
|
|
60
|
+
// after a filtered find returns zero results.
|
|
52
61
|
if (
|
|
53
62
|
requestImages.length === 0 &&
|
|
54
63
|
findApiProducts.length === 0 &&
|
|
64
|
+
!hasPostFilterSelections &&
|
|
55
65
|
!window.settings.algolia.enabled
|
|
56
66
|
) {
|
|
57
67
|
return <></>;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import ItemSpecification from './ItemSpecification';
|
|
3
2
|
import { Icon } from '@nyris/nyris-react-components';
|
|
4
3
|
import useRequestStore from 'stores/request/requestStore';
|
|
5
4
|
import useResultStore from 'stores/result/resultStore';
|
|
@@ -19,8 +18,12 @@ function SmartFilter({
|
|
|
19
18
|
const isSmartFiltersEnabled = !!settings.useSmartFilters;
|
|
20
19
|
const smartFilters = useResultStore(state => state.smartFilters);
|
|
21
20
|
const setSmartFilters = useResultStore(state => state.setSmartFilters);
|
|
22
|
-
const productsFromFindApi = useResultStore(
|
|
23
|
-
|
|
21
|
+
const productsFromFindApi = useResultStore(
|
|
22
|
+
state => state.productsFromFindApi,
|
|
23
|
+
);
|
|
24
|
+
const productsFromAlgolia = useResultStore(
|
|
25
|
+
state => state.productsFromAlgolia,
|
|
26
|
+
);
|
|
24
27
|
const firstSearchResults = useResultStore(state => state.firstSearchResults);
|
|
25
28
|
const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
|
|
26
29
|
const setAlgoliaProducts = useResultStore(state => state.setAlgoliaProducts);
|
|
@@ -194,7 +197,10 @@ function SmartFilter({
|
|
|
194
197
|
return;
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
if (
|
|
200
|
+
if (
|
|
201
|
+
rerankedSkus.length === 0 ||
|
|
202
|
+
!applySkuOrderingToCurrentResults(rerankedSkus)
|
|
203
|
+
) {
|
|
198
204
|
setSmartFilters({
|
|
199
205
|
...smartFilters,
|
|
200
206
|
selectedFilterKey: filter.key,
|
|
@@ -214,7 +220,9 @@ function SmartFilter({
|
|
|
214
220
|
errorMessage: null,
|
|
215
221
|
});
|
|
216
222
|
} catch (error: any) {
|
|
217
|
-
const timeout = String(error?.message || '').includes(
|
|
223
|
+
const timeout = String(error?.message || '').includes(
|
|
224
|
+
'SMART_FILTER_TIMEOUT',
|
|
225
|
+
);
|
|
218
226
|
setSmartFilters({
|
|
219
227
|
...smartFilters,
|
|
220
228
|
selectedFilterKey: filter.key,
|
|
@@ -241,25 +249,7 @@ function SmartFilter({
|
|
|
241
249
|
</div>
|
|
242
250
|
<Icon name="smart_filter_indication" className="mt-1" />
|
|
243
251
|
</div>
|
|
244
|
-
|
|
245
|
-
{Object.keys(imageAnalysis?.specification || {})
|
|
246
|
-
.filter(key => key !== 'is_nameplate' && key !== 'prefilter_value')
|
|
247
|
-
.map(key => {
|
|
248
|
-
const value = imageAnalysis?.specification[key];
|
|
249
|
-
if (!value) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
return (
|
|
253
|
-
<ItemSpecification
|
|
254
|
-
key={key}
|
|
255
|
-
attr={key}
|
|
256
|
-
value={value}
|
|
257
|
-
specificationFilter={specificationFilter}
|
|
258
|
-
imageAnalysis={imageAnalysis}
|
|
259
|
-
/>
|
|
260
|
-
);
|
|
261
|
-
})}
|
|
262
|
-
</div>
|
|
252
|
+
|
|
263
253
|
{smartFilters.status === 'loading' && (
|
|
264
254
|
<div className="ml-[30px] mt-3 text-xs text-[#3E36DC]">
|
|
265
255
|
Loading smart filters...
|
|
@@ -270,11 +260,12 @@ function SmartFilter({
|
|
|
270
260
|
{smartFilters.errorMessage || 'Could not generate smart filters.'}
|
|
271
261
|
</div>
|
|
272
262
|
)}
|
|
273
|
-
{smartFilters.status === 'success' &&
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
263
|
+
{smartFilters.status === 'success' &&
|
|
264
|
+
smartFilters.filters.length === 0 && (
|
|
265
|
+
<div className="ml-[30px] mt-3 text-xs text-[#767A9F]">
|
|
266
|
+
No smart filters available for this image.
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
278
269
|
<div
|
|
279
270
|
className={
|
|
280
271
|
variant === 'results'
|
|
@@ -298,47 +289,46 @@ function SmartFilter({
|
|
|
298
289
|
? 'flex flex-row flex-nowrap gap-2 pb-1'
|
|
299
290
|
: 'flex flex-row flex-wrap gap-2 pb-1'
|
|
300
291
|
}
|
|
301
|
-
style={
|
|
292
|
+
style={
|
|
293
|
+
variant === 'results' ? { width: 'max-content' } : { width: 'auto' }
|
|
294
|
+
}
|
|
302
295
|
>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
</button>
|
|
340
|
-
);
|
|
341
|
-
})}
|
|
296
|
+
{smartFilters.filters.map((filter, index) => {
|
|
297
|
+
const isSelected =
|
|
298
|
+
smartFilters.selectedFilterKey === filter.key &&
|
|
299
|
+
smartFilters.selectedFilterValue === filter.value;
|
|
300
|
+
const hasSelection =
|
|
301
|
+
!!smartFilters.selectedFilterKey &&
|
|
302
|
+
!!smartFilters.selectedFilterValue;
|
|
303
|
+
const isRerankLoading = smartFilters.rerankStatus === 'loading';
|
|
304
|
+
const isDisabled = !isSelected && (hasSelection || isRerankLoading);
|
|
305
|
+
return (
|
|
306
|
+
<button
|
|
307
|
+
key={`${filter.key}-${filter.value}-${index}`}
|
|
308
|
+
type="button"
|
|
309
|
+
disabled={isDisabled}
|
|
310
|
+
aria-pressed={isSelected}
|
|
311
|
+
style={variant === 'results' ? { flex: '0 0 auto' } : undefined}
|
|
312
|
+
className={`px-3.5 py-2 rounded-lg border text-sm inline-flex items-center gap-1.5 ${
|
|
313
|
+
isSelected
|
|
314
|
+
? 'bg-[#545987] text-white border-[#545987]'
|
|
315
|
+
: 'bg-[#E7E8F1] text-[#767A9F] border-transparent'
|
|
316
|
+
} ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
|
317
|
+
onClick={() => {
|
|
318
|
+
if (isRerankLoading) return;
|
|
319
|
+
void onFilterClick({
|
|
320
|
+
key: filter.key,
|
|
321
|
+
value: filter.value,
|
|
322
|
+
});
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
<span>{filter.value}</span>
|
|
326
|
+
{isSelected && (
|
|
327
|
+
<Icon name="close" className="w-3 h-3 text-white" />
|
|
328
|
+
)}
|
|
329
|
+
</button>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
342
332
|
</div>
|
|
343
333
|
</div>
|
|
344
334
|
</div>
|
|
@@ -65,13 +65,13 @@ function TextSearch({
|
|
|
65
65
|
|
|
66
66
|
const showPreFilter = useMemo(() => {
|
|
67
67
|
if (settings.shouldUseUserMetadata && user) {
|
|
68
|
-
if (user['/user_metadata']
|
|
68
|
+
if (user['/user_metadata']?.value) {
|
|
69
69
|
setMetaFilter(user['/user_metadata'].value);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
if (settings.shouldUseUserMetadata && user) {
|
|
74
|
-
if (settings.preFilterOption && !user['/user_metadata']
|
|
74
|
+
if (settings.preFilterOption && !user['/user_metadata']?.value) {
|
|
75
75
|
return true;
|
|
76
76
|
}
|
|
77
77
|
return false;
|
|
@@ -103,7 +103,7 @@ function TextSearch({
|
|
|
103
103
|
imageRegion: regions[0],
|
|
104
104
|
text: value,
|
|
105
105
|
settings,
|
|
106
|
-
showFeedback:
|
|
106
|
+
showFeedback: true,
|
|
107
107
|
newSearch: false,
|
|
108
108
|
});
|
|
109
109
|
}
|
|
@@ -204,7 +204,9 @@ function TextSearch({
|
|
|
204
204
|
!isEmpty(preFilter)
|
|
205
205
|
? 'fill-theme-primary'
|
|
206
206
|
: 'fill-[#2B2C46]',
|
|
207
|
-
|
|
207
|
+
!isEmpty(preFilter)
|
|
208
|
+
? 'desktop:fill-white'
|
|
209
|
+
: 'desktop:fill-[#2B2C46]',
|
|
208
210
|
)}
|
|
209
211
|
/>
|
|
210
212
|
</div>
|
|
@@ -20,8 +20,12 @@ import { useGroundedSearch } from './useGroundedSearch';
|
|
|
20
20
|
import { generateSmartFilters } from '../services/vizo';
|
|
21
21
|
import { cropImage } from 'utils/cropImageToBase64';
|
|
22
22
|
import { invalidatePostFilterRequests } from 'components/PostFilter/PostFilterFindApi';
|
|
23
|
+
import { derivePostFilterFacetsFromResults } from 'utils/derivePostFilterFacets';
|
|
24
|
+
import { useAuth0 } from '@auth0/auth0-react';
|
|
23
25
|
|
|
24
26
|
export const useImageSearch = () => {
|
|
27
|
+
const { user } = useAuth0();
|
|
28
|
+
|
|
25
29
|
const setRegions = useRequestStore(state => state.setRegions);
|
|
26
30
|
const query = useRequestStore(state => state.query);
|
|
27
31
|
|
|
@@ -69,7 +73,9 @@ export const useImageSearch = () => {
|
|
|
69
73
|
state => state.setGroundingFilterResult,
|
|
70
74
|
);
|
|
71
75
|
const setSmartFilters = useResultStore(state => state.setSmartFilters);
|
|
72
|
-
const setPostFilterFacets = useResultStore(
|
|
76
|
+
const setPostFilterFacets = useResultStore(
|
|
77
|
+
state => state.setPostFilterFacets,
|
|
78
|
+
);
|
|
73
79
|
const setInitialFindSearchSnapshot = useResultStore(
|
|
74
80
|
state => state.setInitialFindSearchSnapshot,
|
|
75
81
|
);
|
|
@@ -208,6 +214,7 @@ export const useImageSearch = () => {
|
|
|
208
214
|
];
|
|
209
215
|
|
|
210
216
|
let smartFilterPromise: Promise<void> | null = null;
|
|
217
|
+
|
|
211
218
|
if (isSmartFiltersEnabled && requestImage && (newSearch || imageRegion)) {
|
|
212
219
|
setSmartFilters({
|
|
213
220
|
status: 'loading',
|
|
@@ -287,17 +294,20 @@ export const useImageSearch = () => {
|
|
|
287
294
|
});
|
|
288
295
|
}
|
|
289
296
|
|
|
297
|
+
const usesFindApiPostFilters =
|
|
298
|
+
!settings.algolia?.enabled && settings.postFilterOption;
|
|
299
|
+
// Crop/region finds replace results without sending postFilterSelections;
|
|
300
|
+
// clear stale selections so checkboxes match the new unfiltered grid.
|
|
290
301
|
const shouldResetPostFilters =
|
|
291
302
|
clearPostFilter ||
|
|
292
|
-
(
|
|
293
|
-
|
|
294
|
-
settings.postFilterOption);
|
|
303
|
+
(Boolean(imageRegion) && usesFindApiPostFilters) ||
|
|
304
|
+
(!requestImage && settings.postFilterOption);
|
|
295
305
|
|
|
296
306
|
if (shouldResetPostFilters) {
|
|
297
307
|
invalidatePostFilterRequests();
|
|
298
308
|
refine();
|
|
299
309
|
clearPostFilterSelections();
|
|
300
|
-
if (clearPostFilter) {
|
|
310
|
+
if (clearPostFilter || !requestImage) {
|
|
301
311
|
setPostFilterFacets([]);
|
|
302
312
|
setInitialFindSearchSnapshot(null);
|
|
303
313
|
}
|
|
@@ -324,16 +334,40 @@ export const useImageSearch = () => {
|
|
|
324
334
|
? query
|
|
325
335
|
: text
|
|
326
336
|
: undefined,
|
|
337
|
+
scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
|
|
327
338
|
});
|
|
328
339
|
useResultStore.getState().resetGrounding();
|
|
329
340
|
|
|
330
341
|
setFindApiProducts(res?.results);
|
|
331
|
-
|
|
332
|
-
|
|
342
|
+
|
|
343
|
+
// Image: API facets if present, else derive from result.filters.
|
|
344
|
+
// Text: API facets only.
|
|
345
|
+
// Empty results with no facet payload may keep prior facet UI for unselect,
|
|
346
|
+
// but the search snapshot always updates to this response.
|
|
347
|
+
const isImageSearch = Boolean(requestImage);
|
|
348
|
+
const results = res?.results || [];
|
|
349
|
+
const apiFacets = Array.isArray(res?.facets) ? res.facets : [];
|
|
350
|
+
const nextFacets = isImageSearch
|
|
351
|
+
? apiFacets.length > 0
|
|
352
|
+
? apiFacets
|
|
353
|
+
: derivePostFilterFacetsFromResults(results)
|
|
354
|
+
: apiFacets;
|
|
355
|
+
const previousFacets = useResultStore.getState().postFilterFacets;
|
|
356
|
+
const shouldPreserveFacets =
|
|
357
|
+
results.length === 0 &&
|
|
358
|
+
nextFacets.length === 0 &&
|
|
359
|
+
Array.isArray(previousFacets) &&
|
|
360
|
+
previousFacets.length > 0;
|
|
361
|
+
|
|
362
|
+
// Facet UI may stay for recovery; snapshot always matches this search.
|
|
363
|
+
if (!shouldPreserveFacets) {
|
|
364
|
+
setPostFilterFacets(nextFacets);
|
|
365
|
+
}
|
|
333
366
|
setInitialFindSearchSnapshot({
|
|
334
|
-
results
|
|
335
|
-
postFilterFacets:
|
|
367
|
+
results,
|
|
368
|
+
postFilterFacets: nextFacets,
|
|
336
369
|
});
|
|
370
|
+
|
|
337
371
|
setImageAnalysis(res?.image_analysis);
|
|
338
372
|
setSessionId(res?.session);
|
|
339
373
|
setRequestId(res?.id);
|
|
@@ -421,6 +455,7 @@ export const useImageSearch = () => {
|
|
|
421
455
|
setSmartFilters,
|
|
422
456
|
setPostFilterFacets,
|
|
423
457
|
setInitialFindSearchSnapshot,
|
|
458
|
+
user,
|
|
424
459
|
],
|
|
425
460
|
);
|
|
426
461
|
|
|
@@ -339,14 +339,14 @@ Evaluate if these results match the image. If they do (same type of product), ro
|
|
|
339
339
|
|
|
340
340
|
// Enforce ≤5 rule: if initial results are ≤5, strip filter options, keep only text_search_prompt
|
|
341
341
|
if (activeProducts.length <= 5 && activeProducts.length > 0) {
|
|
342
|
-
mappedOptions = [
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
];
|
|
342
|
+
// mappedOptions = [
|
|
343
|
+
// {
|
|
344
|
+
// label: "Describe what you're looking for",
|
|
345
|
+
// action_type: 'text_search_prompt' as const,
|
|
346
|
+
// key: 'text_search',
|
|
347
|
+
// value: 'text_search',
|
|
348
|
+
// },
|
|
349
|
+
// ];
|
|
350
350
|
|
|
351
351
|
if (
|
|
352
352
|
window.settings.vizo?.findRelatedParts &&
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import ReactDOM from 'react-dom/client';
|
|
2
2
|
import './index.css';
|
|
3
3
|
import App from './App';
|
|
4
|
+
import { bootstrapAuth0ApiKeyGuard } from 'utils/nyrisApiKey';
|
|
5
|
+
|
|
6
|
+
// Clear settings.js apiKey before any React tree can issue Nyris requests (Auth0 mode).
|
|
7
|
+
bootstrapAuth0ApiKeyGuard();
|
|
4
8
|
|
|
5
9
|
const root = ReactDOM.createRoot(
|
|
6
10
|
document.getElementById('root') as HTMLElement,
|