@nyris/nyris-webapp 0.3.98 → 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.
- package/build/asset-manifest.json +7 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +40 -0
- package/build/static/css/main.5cb72c09.css +4 -0
- package/build/static/css/main.5cb72c09.css.map +1 -0
- package/build/static/js/main.2339271b.js +3 -0
- package/build/static/js/{main.93fdbd6e.js.map → main.2339271b.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 +40 -0
- package/src/common/assets/images/plate_info.png +0 -0
- package/src/components/Cart.tsx +41 -0
- 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 +3 -0
- 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/useEffectiveGroundingResults.ts +8 -6
- package/src/hooks/useGoodResultsChat.ts +8 -22
- package/src/hooks/useImageSearch.ts +290 -12
- package/src/hooks/useMachinePartsListingPagination.ts +95 -0
- package/src/hooks/useResultEvaluator.ts +43 -19
- 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 +223 -35
- 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/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 +40 -0
- 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.2339271b.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getScopes } from 'services/filter';
|
|
2
|
+
import { lookupScopeById } from './scopeLookup';
|
|
3
|
+
|
|
4
|
+
jest.mock('services/filter', () => ({
|
|
5
|
+
getScopes: jest.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
jest.mock('utils/nyrisApiKey', () => ({
|
|
9
|
+
isNyrisApiKeyReady: () => true,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const getScopesMock = getScopes as jest.MockedFunction<typeof getScopes>;
|
|
13
|
+
|
|
14
|
+
describe('lookupScopeById', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
getScopesMock.mockReset();
|
|
17
|
+
window.settings = {
|
|
18
|
+
scopes: { scopeID: true },
|
|
19
|
+
apiKey: 'test-key',
|
|
20
|
+
} as typeof window.settings;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('sends the serial number unchanged', async () => {
|
|
24
|
+
getScopesMock.mockResolvedValue({
|
|
25
|
+
itemKeys: ['sku-1'],
|
|
26
|
+
filterAttributes: [],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const result = await lookupScopeById('Ab-12/c 9');
|
|
30
|
+
|
|
31
|
+
expect(getScopesMock).toHaveBeenCalledWith(
|
|
32
|
+
'Ab-12/c 9',
|
|
33
|
+
window.settings,
|
|
34
|
+
);
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
status: 'found',
|
|
37
|
+
itemKeys: ['sku-1'],
|
|
38
|
+
filterAttributes: [],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not call the scopes API for a blank id', async () => {
|
|
43
|
+
const result = await lookupScopeById(' ');
|
|
44
|
+
|
|
45
|
+
expect(getScopesMock).not.toHaveBeenCalled();
|
|
46
|
+
expect(result).toEqual({ status: 'not_found' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns empty when the machine has no item keys', async () => {
|
|
50
|
+
getScopesMock.mockResolvedValue({
|
|
51
|
+
itemKeys: [],
|
|
52
|
+
filterAttributes: [],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await expect(lookupScopeById('2234GH')).resolves.toEqual({
|
|
56
|
+
status: 'empty',
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns not found when the scopes request fails', async () => {
|
|
61
|
+
getScopesMock.mockRejectedValue(new Error('unavailable'));
|
|
62
|
+
|
|
63
|
+
await expect(lookupScopeById('2234GH')).resolves.toEqual({
|
|
64
|
+
status: 'not_found',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getScopes } from 'services/filter';
|
|
2
|
+
import { isNyrisApiKeyReady } from 'utils/nyrisApiKey';
|
|
3
|
+
import { TypePlateLookupResult } from 'components/ScopeSelect/identifyTypePlateImage';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /v1/scopes?scopeIds=... using the existing client encoding.
|
|
7
|
+
* Does not write the scope store. Callers apply a machine only after confirm.
|
|
8
|
+
* The id is sent unchanged; trimming only rejects a blank value.
|
|
9
|
+
*/
|
|
10
|
+
export async function lookupScopeById(
|
|
11
|
+
scopeId: string,
|
|
12
|
+
): Promise<TypePlateLookupResult> {
|
|
13
|
+
const settings = window.settings;
|
|
14
|
+
if (!settings.scopes?.scopeID) {
|
|
15
|
+
return { status: 'unavailable' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!isNyrisApiKeyReady()) {
|
|
19
|
+
return { status: 'unavailable' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (scopeId.trim().length === 0 || scopeId.trim() === 'NOT_FOUND') {
|
|
23
|
+
return { status: 'not_found' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const scopesResponse = await getScopes(scopeId, settings);
|
|
28
|
+
const itemKeys = (scopesResponse.itemKeys || []).filter(Boolean);
|
|
29
|
+
const filterAttributes = scopesResponse.filterAttributes || [];
|
|
30
|
+
|
|
31
|
+
if (itemKeys.length === 0) {
|
|
32
|
+
return { status: 'empty' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { status: 'found', itemKeys, filterAttributes };
|
|
36
|
+
} catch {
|
|
37
|
+
return { status: 'not_found' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { machinePartsPageText } from './machinePartsPage';
|
|
2
|
+
import { machinePartsListingXOptions } from './visualSearch';
|
|
3
|
+
|
|
4
|
+
const X_OPTIONS = 'hybrid_search.text.reranking=false';
|
|
5
|
+
|
|
6
|
+
function keys(count: number) {
|
|
7
|
+
return Array.from({ length: count }, (_, index) => `ITEM-${index + 1}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const listing = {
|
|
11
|
+
hasSearchImage: false,
|
|
12
|
+
hasFilters: false,
|
|
13
|
+
scopeId: 'TestScope2',
|
|
14
|
+
itemKeys: keys(15),
|
|
15
|
+
keyword: '',
|
|
16
|
+
pageSize: 10,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('machinePartsListingXOptions', () => {
|
|
20
|
+
it('sends the override for each scope-only item key page', () => {
|
|
21
|
+
expect(
|
|
22
|
+
machinePartsListingXOptions({
|
|
23
|
+
...listing,
|
|
24
|
+
pageIndex: 0,
|
|
25
|
+
text: machinePartsPageText(listing.itemKeys, 0, 10),
|
|
26
|
+
}),
|
|
27
|
+
).toBe(X_OPTIONS);
|
|
28
|
+
expect(
|
|
29
|
+
machinePartsListingXOptions({
|
|
30
|
+
...listing,
|
|
31
|
+
pageIndex: 1,
|
|
32
|
+
text: machinePartsPageText(listing.itemKeys, 1, 10),
|
|
33
|
+
}),
|
|
34
|
+
).toBe(X_OPTIONS);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('omits the override for a keyword, image, filters, or another page', () => {
|
|
38
|
+
const firstPage = machinePartsPageText(listing.itemKeys, 0, 10);
|
|
39
|
+
expect(
|
|
40
|
+
machinePartsListingXOptions({
|
|
41
|
+
...listing,
|
|
42
|
+
pageIndex: 1,
|
|
43
|
+
text: firstPage,
|
|
44
|
+
}),
|
|
45
|
+
).toBeUndefined();
|
|
46
|
+
expect(
|
|
47
|
+
machinePartsListingXOptions({
|
|
48
|
+
...listing,
|
|
49
|
+
pageIndex: 0,
|
|
50
|
+
text: 'pump',
|
|
51
|
+
keyword: 'pump',
|
|
52
|
+
}),
|
|
53
|
+
).toBeUndefined();
|
|
54
|
+
expect(
|
|
55
|
+
machinePartsListingXOptions({
|
|
56
|
+
...listing,
|
|
57
|
+
pageIndex: 0,
|
|
58
|
+
text: firstPage,
|
|
59
|
+
hasSearchImage: true,
|
|
60
|
+
}),
|
|
61
|
+
).toBeUndefined();
|
|
62
|
+
expect(
|
|
63
|
+
machinePartsListingXOptions({
|
|
64
|
+
...listing,
|
|
65
|
+
pageIndex: 0,
|
|
66
|
+
text: firstPage,
|
|
67
|
+
hasFilters: true,
|
|
68
|
+
}),
|
|
69
|
+
).toBeUndefined();
|
|
70
|
+
expect(
|
|
71
|
+
machinePartsListingXOptions({
|
|
72
|
+
...listing,
|
|
73
|
+
pageIndex: 0,
|
|
74
|
+
text: firstPage,
|
|
75
|
+
scopeId: null,
|
|
76
|
+
}),
|
|
77
|
+
).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -10,6 +10,45 @@ import { isEqual } from 'lodash';
|
|
|
10
10
|
import { DEFAULT_REGION } from '../constants';
|
|
11
11
|
import { getUserLocation } from 'helpers/getGeoLocation';
|
|
12
12
|
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
13
|
+
import useRequestStore from 'stores/request/requestStore';
|
|
14
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
15
|
+
import { machinePartsPageText } from './machinePartsPage';
|
|
16
|
+
import useMachinePartsListingStore from 'stores/machinePartsListing/machinePartsListingStore';
|
|
17
|
+
|
|
18
|
+
const MACHINE_PARTS_LISTING_X_OPTIONS = 'hybrid_search.text.reranking=false';
|
|
19
|
+
|
|
20
|
+
/** Scope-only listing: generated item-key text, no keyword, image, or filters. */
|
|
21
|
+
export function machinePartsListingXOptions({
|
|
22
|
+
text,
|
|
23
|
+
hasSearchImage,
|
|
24
|
+
hasFilters,
|
|
25
|
+
scopeId,
|
|
26
|
+
itemKeys,
|
|
27
|
+
keyword,
|
|
28
|
+
pageIndex,
|
|
29
|
+
pageSize,
|
|
30
|
+
}: {
|
|
31
|
+
text?: string;
|
|
32
|
+
hasSearchImage: boolean;
|
|
33
|
+
hasFilters: boolean;
|
|
34
|
+
scopeId: string | null;
|
|
35
|
+
itemKeys: readonly string[];
|
|
36
|
+
keyword: string;
|
|
37
|
+
pageIndex: number;
|
|
38
|
+
pageSize: number;
|
|
39
|
+
}): string | undefined {
|
|
40
|
+
if (!scopeId || hasSearchImage || hasFilters) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const listingText = machinePartsPageText(itemKeys, pageIndex, pageSize);
|
|
44
|
+
if (!listingText || text !== listingText) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (keyword.trim().length > 0) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return MACHINE_PARTS_LISTING_X_OPTIONS;
|
|
51
|
+
}
|
|
13
52
|
|
|
14
53
|
export interface Filter {
|
|
15
54
|
key?: string;
|
|
@@ -53,6 +92,18 @@ export const findRegions = async (
|
|
|
53
92
|
};
|
|
54
93
|
};
|
|
55
94
|
|
|
95
|
+
/** Validated machine scope id. Item keys from /v1/scopes are not used. */
|
|
96
|
+
function appliedMachineScopeId(): string | null {
|
|
97
|
+
if (!window.settings?.scopes?.scopeID) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const { scopeId } = useScopeStore.getState();
|
|
101
|
+
if (typeof scopeId !== 'string' || scopeId.trim().length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return scopeId;
|
|
105
|
+
}
|
|
106
|
+
|
|
56
107
|
export const find = async ({
|
|
57
108
|
image,
|
|
58
109
|
settings,
|
|
@@ -69,7 +120,8 @@ export const find = async ({
|
|
|
69
120
|
scopeIds?: string;
|
|
70
121
|
}) => {
|
|
71
122
|
const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
|
|
72
|
-
let options: ImageSearchOptions =
|
|
123
|
+
let options: ImageSearchOptions =
|
|
124
|
+
typeof text === 'string' && text.length > 0 ? { text } : {};
|
|
73
125
|
|
|
74
126
|
if (window.settings?.geoLocation) {
|
|
75
127
|
try {
|
|
@@ -85,13 +137,29 @@ export const find = async ({
|
|
|
85
137
|
if (region) {
|
|
86
138
|
options = { ...options, cropRect: region };
|
|
87
139
|
}
|
|
140
|
+
const machineScopeId = appliedMachineScopeId();
|
|
141
|
+
const listingPage = useMachinePartsListingStore.getState();
|
|
142
|
+
const xOptions = machinePartsListingXOptions({
|
|
143
|
+
text,
|
|
144
|
+
hasSearchImage: Boolean(image),
|
|
145
|
+
hasFilters: Boolean(filters && filters.length > 0),
|
|
146
|
+
scopeId: machineScopeId,
|
|
147
|
+
itemKeys: useScopeStore.getState().itemKeys || [],
|
|
148
|
+
keyword: useRequestStore.getState().query || '',
|
|
149
|
+
pageIndex: listingPage.pageIndex,
|
|
150
|
+
pageSize: listingPage.pageSize,
|
|
151
|
+
});
|
|
88
152
|
return nyrisApi.find(
|
|
89
153
|
options,
|
|
90
154
|
image,
|
|
91
155
|
filters,
|
|
92
156
|
undefined,
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
xOptions,
|
|
158
|
+
machineScopeId
|
|
159
|
+
? [machineScopeId]
|
|
160
|
+
: scopeIds
|
|
161
|
+
? [scopeIds]
|
|
162
|
+
: [],
|
|
95
163
|
);
|
|
96
164
|
};
|
|
97
165
|
|
package/src/services/vizo.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { RectCoords } from '@nyris/nyris-api';
|
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import { cropImage } from 'utils/cropImageToBase64';
|
|
4
4
|
import { getEffectiveApiKey } from 'utils/nyrisApiKey';
|
|
5
|
+
import { normalizeChatResponse } from './normalizeChatResponse';
|
|
5
6
|
|
|
6
7
|
function resolveAiOrNyrisApiKey(explicitApiKey?: string): string {
|
|
7
8
|
if (explicitApiKey) {
|
|
@@ -330,20 +331,20 @@ export async function chatAnalysis({
|
|
|
330
331
|
}: ChatAnalysisParams): Promise<any> {
|
|
331
332
|
try {
|
|
332
333
|
const response = await axios.post(
|
|
333
|
-
|
|
334
|
+
`${window.settings.baseUrl}/api/ai-analysis/chat`,
|
|
334
335
|
{
|
|
335
336
|
promptKey,
|
|
336
337
|
multimodalInputs,
|
|
337
338
|
},
|
|
338
339
|
{
|
|
339
340
|
headers: {
|
|
340
|
-
'x-api-key': apiKey
|
|
341
|
+
'x-api-key': resolveAiOrNyrisApiKey(apiKey),
|
|
341
342
|
'Content-Type': 'application/json',
|
|
342
343
|
},
|
|
343
344
|
},
|
|
344
345
|
);
|
|
345
346
|
|
|
346
|
-
return response.data;
|
|
347
|
+
return normalizeChatResponse(response.data);
|
|
347
348
|
} catch (error: any) {
|
|
348
349
|
if (error.response) {
|
|
349
350
|
throw new Error(`API error: ${error.response.statusText}`);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MACHINE_PARTS_PAGE_SIZE,
|
|
4
|
+
machinePartsPageText,
|
|
5
|
+
} from 'services/machinePartsPage';
|
|
6
|
+
import useScopeStore from 'stores/scope/scopeStore';
|
|
7
|
+
|
|
8
|
+
interface MachinePartsListingState {
|
|
9
|
+
pageSize: number;
|
|
10
|
+
pageIndex: number;
|
|
11
|
+
setPageSize: (pageSize: number) => void;
|
|
12
|
+
setPageIndex: (pageIndex: number) => void;
|
|
13
|
+
resetPage: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const useMachinePartsListingStore = create<MachinePartsListingState>()(set => ({
|
|
17
|
+
pageSize: DEFAULT_MACHINE_PARTS_PAGE_SIZE,
|
|
18
|
+
pageIndex: 0,
|
|
19
|
+
setPageSize: pageSize => set({ pageSize, pageIndex: 0 }),
|
|
20
|
+
setPageIndex: pageIndex => set({ pageIndex }),
|
|
21
|
+
resetPage: () => set({ pageIndex: 0 }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
/** Scope-only listing restarts at page 1 and keeps the selected page size. */
|
|
25
|
+
export function firstMachinePartsListingText(): string | undefined {
|
|
26
|
+
useMachinePartsListingStore.getState().resetPage();
|
|
27
|
+
const { pageSize } = useMachinePartsListingStore.getState();
|
|
28
|
+
return machinePartsPageText(
|
|
29
|
+
useScopeStore.getState().itemKeys || [],
|
|
30
|
+
0,
|
|
31
|
+
pageSize,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default useMachinePartsListingStore;
|
|
@@ -13,6 +13,8 @@ const filterSlice: StateCreator<FilterState & FilterAction> = set => ({
|
|
|
13
13
|
setPreFilterList: filter => set(() => ({ preFilterList: filter })),
|
|
14
14
|
setPreFilterLoading: isLoading =>
|
|
15
15
|
set(() => ({ preFilterLoading: isLoading })),
|
|
16
|
+
setTypePlatePreFilterValues: values =>
|
|
17
|
+
set(() => ({ typePlatePreFilterValues: values })),
|
|
16
18
|
setPostFilterSelections: filters =>
|
|
17
19
|
set(() => ({ postFilterSelections: filters })),
|
|
18
20
|
togglePostFilterSelection: (attribute, value) =>
|
|
@@ -61,6 +61,10 @@ const productsSlice: StateCreator<ProductsState & ProductsAction> = set => ({
|
|
|
61
61
|
set(() => ({
|
|
62
62
|
smartFilters,
|
|
63
63
|
})),
|
|
64
|
+
setRequestQuality: requestQuality =>
|
|
65
|
+
set(() => ({
|
|
66
|
+
requestQuality,
|
|
67
|
+
})),
|
|
64
68
|
setPostFilterFacets: facets =>
|
|
65
69
|
set(() => ({
|
|
66
70
|
postFilterFacets: facets,
|
|
@@ -1,35 +1,98 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { Filter } from '@nyris/nyris-api';
|
|
3
3
|
|
|
4
|
+
export type ScopeSource = 'manual' | 'image';
|
|
5
|
+
|
|
6
|
+
export type AppliedScopeInput =
|
|
7
|
+
| { source: 'manual'; value: string }
|
|
8
|
+
| { source: 'image'; fileKey: string };
|
|
9
|
+
|
|
4
10
|
interface ScopeState {
|
|
5
11
|
itemKeys: string[];
|
|
6
12
|
filterAttributes: Filter[];
|
|
13
|
+
scopeId: string | null;
|
|
7
14
|
isLoading: boolean;
|
|
8
15
|
hasFetched: boolean;
|
|
16
|
+
formResetKey: number;
|
|
17
|
+
manualScopeId: string;
|
|
18
|
+
pendingFile: File | null;
|
|
19
|
+
previewUrl: string | null;
|
|
20
|
+
activeSource: ScopeSource | null;
|
|
21
|
+
lastAppliedInput: AppliedScopeInput | null;
|
|
22
|
+
lastExtractedFromImage: string;
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
interface ScopeAction {
|
|
12
|
-
setScope: (
|
|
26
|
+
setScope: (
|
|
27
|
+
itemKeys: string[],
|
|
28
|
+
filterAttributes: Filter[],
|
|
29
|
+
scopeId?: string | null,
|
|
30
|
+
) => void;
|
|
13
31
|
setScopeLoading: (isLoading: boolean) => void;
|
|
14
32
|
setScopeFetched: (hasFetched: boolean) => void;
|
|
33
|
+
setManualScopeId: (manualScopeId: string) => void;
|
|
34
|
+
setPendingFile: (pendingFile: File | null) => void;
|
|
35
|
+
setPreviewUrl: (previewUrl: string | null) => void;
|
|
36
|
+
setActiveSource: (activeSource: ScopeSource | null) => void;
|
|
37
|
+
setLastAppliedInput: (lastAppliedInput: AppliedScopeInput | null) => void;
|
|
38
|
+
setLastExtractedFromImage: (lastExtractedFromImage: string) => void;
|
|
15
39
|
resetScope: () => void;
|
|
16
40
|
}
|
|
17
41
|
|
|
18
42
|
const initialState: ScopeState = {
|
|
19
43
|
itemKeys: [],
|
|
20
44
|
filterAttributes: [],
|
|
45
|
+
scopeId: null,
|
|
21
46
|
isLoading: false,
|
|
22
47
|
hasFetched: false,
|
|
48
|
+
formResetKey: 0,
|
|
49
|
+
manualScopeId: '',
|
|
50
|
+
pendingFile: null,
|
|
51
|
+
previewUrl: null,
|
|
52
|
+
activeSource: null,
|
|
53
|
+
lastAppliedInput: null,
|
|
54
|
+
lastExtractedFromImage: '',
|
|
23
55
|
};
|
|
24
56
|
|
|
25
57
|
const useScopeStore = create<ScopeState & ScopeAction>()(set => ({
|
|
26
58
|
...initialState,
|
|
27
59
|
|
|
28
|
-
setScope: (itemKeys, filterAttributes) =>
|
|
29
|
-
set({ itemKeys, filterAttributes, hasFetched: true }),
|
|
60
|
+
setScope: (itemKeys, filterAttributes, scopeId = null) =>
|
|
61
|
+
set({ itemKeys, filterAttributes, scopeId, hasFetched: true }),
|
|
30
62
|
setScopeLoading: isLoading => set({ isLoading }),
|
|
31
63
|
setScopeFetched: hasFetched => set({ hasFetched }),
|
|
32
|
-
|
|
64
|
+
setManualScopeId: manualScopeId => set({ manualScopeId }),
|
|
65
|
+
setPendingFile: pendingFile => set({ pendingFile }),
|
|
66
|
+
setPreviewUrl: previewUrl => set({ previewUrl }),
|
|
67
|
+
setActiveSource: activeSource => set({ activeSource }),
|
|
68
|
+
setLastAppliedInput: lastAppliedInput => set({ lastAppliedInput }),
|
|
69
|
+
setLastExtractedFromImage: lastExtractedFromImage =>
|
|
70
|
+
set({ lastExtractedFromImage }),
|
|
71
|
+
resetScope: () =>
|
|
72
|
+
set(state => {
|
|
73
|
+
if (state.previewUrl) {
|
|
74
|
+
try {
|
|
75
|
+
URL.revokeObjectURL(state.previewUrl);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...initialState,
|
|
80
|
+
formResetKey: state.formResetKey + 1,
|
|
81
|
+
};
|
|
82
|
+
}),
|
|
33
83
|
}));
|
|
34
84
|
|
|
85
|
+
/** Machine scope that should constrain the results currently on screen. */
|
|
86
|
+
export function hasActiveMachineScope(): boolean {
|
|
87
|
+
if (!window.settings?.scopes?.scopeID) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const { scopeId, itemKeys } = useScopeStore.getState();
|
|
91
|
+
return (
|
|
92
|
+
typeof scopeId === 'string' &&
|
|
93
|
+
scopeId.trim().length > 0 &&
|
|
94
|
+
itemKeys.length > 0
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
35
98
|
export default useScopeStore;
|
package/src/stores/types.ts
CHANGED
|
@@ -71,20 +71,21 @@ export interface ProductsState {
|
|
|
71
71
|
groundingError: string | null;
|
|
72
72
|
firstAlgoliaProducts: any[];
|
|
73
73
|
fullResultPool: any[];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
smartFilters: {
|
|
75
|
+
status: 'idle' | 'loading' | 'success' | 'error';
|
|
76
|
+
filters: Array<{
|
|
77
|
+
key: string;
|
|
78
|
+
label: string;
|
|
79
|
+
value: string;
|
|
80
|
+
selected?: boolean;
|
|
81
|
+
}>;
|
|
82
|
+
selectedFilterKey: string | null;
|
|
83
|
+
selectedFilterValue: string | null;
|
|
84
|
+
rerankedSkus: string[];
|
|
85
|
+
rerankStatus: 'idle' | 'loading' | 'success' | 'error';
|
|
86
|
+
errorMessage: string | null;
|
|
87
|
+
};
|
|
88
|
+
requestQuality: string | null;
|
|
88
89
|
postFilterFacets: any[];
|
|
89
90
|
initialFindSearchSnapshot: {
|
|
90
91
|
results: any[];
|
|
@@ -110,6 +111,7 @@ export interface ProductsAction {
|
|
|
110
111
|
setFullResultPool: (products: any[]) => void;
|
|
111
112
|
resetGrounding: () => void;
|
|
112
113
|
setSmartFilters: (smartFilters: ProductsState['smartFilters']) => void;
|
|
114
|
+
setRequestQuality: (requestQuality: string | null) => void;
|
|
113
115
|
setPostFilterFacets: (facets: any[]) => void;
|
|
114
116
|
setInitialFindSearchSnapshot: (
|
|
115
117
|
snapshot: ProductsState['initialFindSearchSnapshot'],
|
|
@@ -132,6 +134,8 @@ export interface FilterState {
|
|
|
132
134
|
specificationFilter: Record<string, string>;
|
|
133
135
|
postFilterSelections: Record<string, string[]>;
|
|
134
136
|
preFilterLoading: boolean;
|
|
137
|
+
// Values applied from type-plate upload; cleared independently of manual picks.
|
|
138
|
+
typePlatePreFilterValues: string[];
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
export interface FilterAction {
|
|
@@ -144,6 +148,7 @@ export interface FilterAction {
|
|
|
144
148
|
togglePostFilterSelection: (attribute: string, value: string) => void;
|
|
145
149
|
clearPostFilterSelections: () => void;
|
|
146
150
|
setPreFilterLoading: (isLoading: boolean) => void;
|
|
151
|
+
setTypePlatePreFilterValues: (values: string[]) => void;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
export interface MiscState {
|