@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.
Files changed (97) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +42 -0
  4. package/build/static/css/main.a7ff7a7f.css +4 -0
  5. package/build/static/css/main.a7ff7a7f.css.map +1 -0
  6. package/build/static/js/main.916c157c.js +3 -0
  7. package/build/static/js/{main.93fdbd6e.js.map → main.916c157c.js.map} +1 -1
  8. package/build/static/media/plate_info.6beaea36d35053be3a45.png +0 -0
  9. package/package.json +3 -3
  10. package/public/js/settings.example.js +42 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/Cart.tsx +41 -0
  13. package/src/components/ChatAssistant/ChatAssistant.test.tsx +213 -0
  14. package/src/components/ChatAssistant/ChatAssistant.tsx +18 -1
  15. package/src/components/ChatAssistant/MobileChatAssistant.tsx +18 -1
  16. package/src/components/ChatAssistant/OptionChip.tsx +23 -1
  17. package/src/components/ChatAssistant/useChatAssistantLogic.ts +35 -14
  18. package/src/components/GoBackButton.tsx +29 -2
  19. package/src/components/Header.tsx +3 -0
  20. package/src/components/HitsPerPage.tsx +55 -18
  21. package/src/components/ImagePreview.tsx +31 -1
  22. package/src/components/ImageUpload.tsx +12 -1
  23. package/src/components/Inquiry/InquiryModal.tsx +61 -26
  24. package/src/components/Pagination.tsx +33 -6
  25. package/src/components/PostFilter/PostFilter.tsx +8 -1
  26. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  27. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  28. package/src/components/PreFilter/PreFilter.tsx +10 -2
  29. package/src/components/Product/Product.tsx +60 -10
  30. package/src/components/Product/ProductDetailView.tsx +23 -1
  31. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  32. package/src/components/Product/ProductList.tsx +51 -18
  33. package/src/components/Rfq/RfqModal.tsx +3 -0
  34. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  35. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  36. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  37. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  38. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  39. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  41. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  43. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  44. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  45. package/src/components/SidePanel.tsx +33 -0
  46. package/src/components/TextSearch.tsx +77 -11
  47. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  48. package/src/components/TypePlateUpload.tsx +132 -0
  49. package/src/hooks/chatProductMetadata.test.ts +307 -0
  50. package/src/hooks/useBadResultsRecovery.ts +26 -49
  51. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  52. package/src/hooks/useGoodResultsChat.ts +28 -39
  53. package/src/hooks/useImageSearch.ts +290 -12
  54. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  55. package/src/hooks/useResultEvaluator.ts +63 -73
  56. package/src/index.css +1 -1
  57. package/src/layouts/AppLayout.tsx +43 -3
  58. package/src/pages/Home.tsx +60 -14
  59. package/src/pages/Result.tsx +194 -14
  60. package/src/services/Feedback.ts +225 -34
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +1 -1
  63. package/src/services/initialMachinePartsText.payload.test.ts +61 -0
  64. package/src/services/initialMachinePartsText.test.ts +33 -0
  65. package/src/services/initialMachinePartsText.ts +11 -0
  66. package/src/services/machinePartsPage.test.ts +74 -0
  67. package/src/services/machinePartsPage.ts +120 -0
  68. package/src/services/normalizeChatResponse.ts +26 -0
  69. package/src/services/scopeLookup.test.ts +67 -0
  70. package/src/services/scopeLookup.ts +39 -0
  71. package/src/services/visualSearch.listingOptions.test.ts +79 -0
  72. package/src/services/visualSearch.ts +71 -3
  73. package/src/services/vizo.ts +4 -3
  74. package/src/stores/chat/conversationStore.test.ts +133 -0
  75. package/src/stores/chat/conversationStore.ts +55 -14
  76. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  77. package/src/stores/request/filter/filter.initialState.ts +1 -0
  78. package/src/stores/request/filter/filter.slice.ts +2 -0
  79. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  80. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  81. package/src/stores/scope/scopeStore.ts +67 -4
  82. package/src/stores/types.ts +19 -14
  83. package/src/translations.ts +213 -0
  84. package/src/types.ts +41 -0
  85. package/src/utils/chatProductMetadata.test.ts +130 -0
  86. package/src/utils/chatProductMetadata.ts +93 -0
  87. package/src/utils/compressImage.ts +3 -3
  88. package/src/utils/fetchProductImage.ts +1 -0
  89. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  90. package/src/utils/productCardImageUrl.test.ts +41 -0
  91. package/src/utils/productCardImageUrl.ts +49 -0
  92. package/src/utils/textSearchFilter.test.ts +41 -0
  93. package/src/utils/textSearchFilter.ts +13 -6
  94. package/build/static/css/main.81216c5d.css +0 -4
  95. package/build/static/css/main.81216c5d.css.map +0 -1
  96. package/build/static/js/main.93fdbd6e.js +0 -3
  97. /package/build/static/js/{main.93fdbd6e.js.LICENSE.txt → main.916c157c.js.LICENSE.txt} +0 -0
@@ -0,0 +1,249 @@
1
+ import {
2
+ act,
3
+ fireEvent,
4
+ render,
5
+ screen,
6
+ waitFor,
7
+ } from '@testing-library/react';
8
+ import ProductList from 'components/Product/ProductList';
9
+ import { find } from 'services/visualSearch';
10
+ import PostFilterFindApi from './PostFilterFindApi';
11
+
12
+ let mockResultState: any;
13
+ let mockRequestState: any;
14
+ const mockSetIsFindApiLoading = jest.fn();
15
+
16
+ jest.mock('components/Accordion', () => {
17
+ const React = require('react');
18
+ const Container = ({ children }: any) =>
19
+ React.createElement('div', null, children);
20
+ return {
21
+ AccordionContent: Container,
22
+ AccordionItem: Container,
23
+ AccordionTrigger: Container,
24
+ };
25
+ });
26
+ jest.mock('@nyris/nyris-react-components', () => ({ Icon: () => null }));
27
+ jest.mock('@auth0/auth0-react', () => ({ useAuth0: () => ({}) }));
28
+ jest.mock('react-i18next', () => ({
29
+ useTranslation: () => ({ t: (value: string) => value }),
30
+ }));
31
+ jest.mock('stores/result/resultStore', () => ({
32
+ __esModule: true,
33
+ default: Object.assign((selector: any) => selector(mockResultState), {
34
+ getState: () => mockResultState,
35
+ }),
36
+ }));
37
+ jest.mock('stores/request/requestStore', () => ({
38
+ __esModule: true,
39
+ default: Object.assign((selector: any) => selector(mockRequestState), {
40
+ getState: () => mockRequestState,
41
+ }),
42
+ }));
43
+ jest.mock('stores/ui/uiStore', () => ({
44
+ __esModule: true,
45
+ default: (selector: any) =>
46
+ selector({ setIsFindApiLoading: mockSetIsFindApiLoading }),
47
+ }));
48
+ jest.mock('stores/scope/scopeStore', () => ({
49
+ __esModule: true,
50
+ default: (selector: any) => selector({ filterAttributes: [] }),
51
+ }));
52
+ jest.mock('services/visualSearch', () => ({ find: jest.fn() }));
53
+ jest.mock('services/Feedback', () => ({ trackClick: jest.fn() }));
54
+ jest.mock('hooks/useImageSearch', () => ({
55
+ useImageSearch: () => ({ singleImageSearch: jest.fn() }),
56
+ }));
57
+ jest.mock('hooks/useEffectiveGroundingResults', () => ({
58
+ useEffectiveGroundingResults: () => [],
59
+ }));
60
+ jest.mock('components/Product/Product', () => ({
61
+ __esModule: true,
62
+ default: ({ dataItem }: any) => (
63
+ <div data-testid="product">{dataItem.sku}</div>
64
+ ),
65
+ }));
66
+
67
+ beforeEach(() => {
68
+ jest.clearAllMocks();
69
+ window.settings = {
70
+ algolia: { enabled: false },
71
+ postFilterOption: true,
72
+ scopeFilter: false,
73
+ } as typeof window.settings;
74
+ mockResultState = {
75
+ productsFromAlgolia: [],
76
+ productsFromFindApi: [
77
+ { sku: '1', filters: { brand: ['Alpha'], color: ['Red'] } },
78
+ { sku: '2', filters: { brand: ['Alpha'], color: ['Blue'] } },
79
+ { sku: '3', filters: { brand: ['Beta'], color: ['Red'] } },
80
+ { sku: '4' },
81
+ ],
82
+ postFilterFacets: [
83
+ { name: 'BRAND', values: [{ value: 'Facet only', count: 99 }] },
84
+ ],
85
+ groundingFilterResult: [],
86
+ showingGroundingFilterResult: false,
87
+ setFindApiProducts: jest.fn(),
88
+ };
89
+ mockRequestState = {
90
+ postFilterSelections: {},
91
+ query: 'search',
92
+ requestImages: [],
93
+ regions: [],
94
+ preFilter: {},
95
+ preFilterList: [],
96
+ setPostFilterSelections: jest.fn(),
97
+ togglePostFilterSelection: jest.fn(),
98
+ };
99
+ (find as jest.Mock).mockResolvedValue({ results: [] });
100
+ });
101
+
102
+ test('builds text-search options and counts from product filters, not facets', () => {
103
+ render(
104
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
105
+ );
106
+
107
+ expect(screen.getByLabelText(/Alpha\s*\(2\)/)).toBeInTheDocument();
108
+ expect(screen.getByLabelText(/Beta\s*\(1\)/)).toBeInTheDocument();
109
+ expect(screen.queryByText('Facet only')).not.toBeInTheDocument();
110
+ });
111
+
112
+ test.each([undefined, false])(
113
+ 'toggles selections locally when API mode is %s',
114
+ postFilterUseApi => {
115
+ window.settings.postFilterUseApi = postFilterUseApi;
116
+ render(
117
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
118
+ );
119
+
120
+ fireEvent.click(screen.getByLabelText(/Alpha\s*\(2\)/));
121
+
122
+ expect(mockRequestState.togglePostFilterSelection).toHaveBeenCalledWith(
123
+ 'brand',
124
+ 'Alpha',
125
+ );
126
+ expect(find).not.toHaveBeenCalled();
127
+ expect(mockResultState.setFindApiProducts).not.toHaveBeenCalled();
128
+ expect(mockSetIsFindApiLoading).not.toHaveBeenCalled();
129
+ },
130
+ );
131
+
132
+ test('uses metadata facets and requests filtered results when API mode is enabled', async () => {
133
+ window.settings.postFilterUseApi = true;
134
+ render(
135
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
136
+ );
137
+
138
+ expect(screen.queryByText('Alpha')).not.toBeInTheDocument();
139
+ fireEvent.click(screen.getByLabelText(/Facet only\s*\(99\)/));
140
+ expect(mockRequestState.setPostFilterSelections).toHaveBeenCalledWith({
141
+ brand: ['Facet only'],
142
+ });
143
+ expect(find).toHaveBeenCalledWith(
144
+ expect.objectContaining({
145
+ text: 'search',
146
+ filters: [{ key: 'BRAND', values: ['Facet only'] }],
147
+ }),
148
+ );
149
+ await waitFor(() => {
150
+ expect(mockResultState.setFindApiProducts).toHaveBeenCalledWith([]);
151
+ });
152
+ });
153
+
154
+ test('does not client-filter server results when API mode is enabled', () => {
155
+ window.settings.postFilterUseApi = true;
156
+ mockRequestState.postFilterSelections = { brand: ['Facet only'] };
157
+ render(<ProductList />);
158
+
159
+ expect(screen.getAllByTestId('product')).toHaveLength(4);
160
+ });
161
+
162
+ test('restores initial results and ignores pending API responses when filters clear', async () => {
163
+ window.settings.postFilterUseApi = true;
164
+ const initialResults = mockResultState.productsFromFindApi;
165
+ mockResultState.initialFindSearchSnapshot = { results: initialResults };
166
+ mockRequestState.setPostFilterSelections.mockImplementation(
167
+ (selections: any) => {
168
+ mockRequestState.postFilterSelections = selections;
169
+ },
170
+ );
171
+ let resolveFind!: (response: any) => void;
172
+ (find as jest.Mock).mockReturnValue(
173
+ new Promise(resolve => {
174
+ resolveFind = resolve;
175
+ }),
176
+ );
177
+ const { rerender } = render(
178
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
179
+ );
180
+
181
+ fireEvent.click(screen.getByLabelText(/Facet only\s*\(99\)/));
182
+ rerender(
183
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
184
+ );
185
+ fireEvent.click(screen.getByLabelText(/Facet only\s*\(99\)/));
186
+ expect(mockRequestState.postFilterSelections).toEqual({});
187
+ expect(mockResultState.setFindApiProducts).toHaveBeenCalledWith(
188
+ initialResults,
189
+ );
190
+ expect(mockSetIsFindApiLoading).toHaveBeenLastCalledWith(false);
191
+
192
+ await act(async () => {
193
+ resolveFind({ results: [{ sku: 'stale' }] });
194
+ });
195
+ expect(find).toHaveBeenCalledTimes(1);
196
+ expect(mockResultState.setFindApiProducts).toHaveBeenCalledTimes(1);
197
+ });
198
+
199
+ test.each([[[]], [['image']]])(
200
+ 'updates counts under other selections with request images %j',
201
+ requestImages => {
202
+ mockRequestState.requestImages = requestImages;
203
+ mockRequestState.query = '';
204
+ mockRequestState.postFilterSelections = { color: ['Red'] };
205
+ render(
206
+ <PostFilterFindApi attribute="brand" label="Brand" searchable={false} />,
207
+ );
208
+
209
+ expect(screen.getByLabelText(/Alpha\s*\(1\)/)).toBeInTheDocument();
210
+ expect(screen.getByLabelText(/Beta\s*\(1\)/)).toBeInTheDocument();
211
+ fireEvent.click(screen.getByLabelText(/Alpha\s*\(1\)/));
212
+ expect(mockRequestState.togglePostFilterSelection).toHaveBeenCalledWith(
213
+ 'brand',
214
+ 'Alpha',
215
+ );
216
+ expect(find).not.toHaveBeenCalled();
217
+ },
218
+ );
219
+
220
+ test('filters existing text-search results and restores them when selections clear', () => {
221
+ const sourceProducts = mockResultState.productsFromFindApi;
222
+ const { rerender } = render(<ProductList />);
223
+ const displayedSkus = () =>
224
+ screen.queryAllByTestId('product').map(product => product.textContent);
225
+
226
+ expect(displayedSkus()).toEqual(['1', '2', '3', '4']);
227
+
228
+ mockRequestState.postFilterSelections = { brand: ['Alpha', 'Beta'] };
229
+ rerender(<ProductList />);
230
+ expect(displayedSkus()).toEqual(['1', '2', '3']);
231
+
232
+ mockRequestState.postFilterSelections = {
233
+ brand: ['Alpha'],
234
+ color: ['Red'],
235
+ };
236
+ rerender(<ProductList />);
237
+ expect(displayedSkus()).toEqual(['1']);
238
+
239
+ mockRequestState.postFilterSelections = { brand: ['Missing'] };
240
+ rerender(<ProductList />);
241
+ expect(displayedSkus()).toEqual([]);
242
+
243
+ mockRequestState.postFilterSelections = {};
244
+ rerender(<ProductList />);
245
+ expect(displayedSkus()).toEqual(['1', '2', '3', '4']);
246
+ expect(mockResultState.productsFromFindApi).toBe(sourceProducts);
247
+ expect(mockResultState.setFindApiProducts).not.toHaveBeenCalled();
248
+ expect(find).not.toHaveBeenCalled();
249
+ });
@@ -6,13 +6,16 @@ import {
6
6
  } from 'components/Accordion';
7
7
  import { Icon } from '@nyris/nyris-react-components';
8
8
  import { useTranslation } from 'react-i18next';
9
+ import { useAuth0 } from '@auth0/auth0-react';
9
10
  import { isEmpty } from 'lodash';
10
11
  import useResultStore from 'stores/result/resultStore';
11
12
  import useRequestStore from 'stores/request/requestStore';
12
13
  import useUiStore from 'stores/ui/uiStore';
13
14
  import useScopeStore from 'stores/scope/scopeStore';
14
15
  import { find } from 'services/visualSearch';
15
- import { useAuth0 } from '@auth0/auth0-react';
16
+ import { trackClick } from 'services/Feedback';
17
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
18
+ import { filterProductsByText } from 'utils/textSearchFilter';
16
19
 
17
20
  let postFilterRequestCounter = 0;
18
21
 
@@ -34,15 +37,26 @@ function PostFilterFindApi({
34
37
  isLastVisible?: boolean;
35
38
  }) {
36
39
  const { user } = useAuth0();
37
-
38
40
  const [itemsLimit, setItemsLimit] = useState(10);
39
41
  const [searchInput, setSearchInput] = useState<string>('');
40
42
 
43
+ const productsFromFindApi = useResultStore(
44
+ state => state.productsFromFindApi,
45
+ );
46
+ const groundingFilterResult = useResultStore(
47
+ state => state.groundingFilterResult,
48
+ );
49
+ const showingGroundingFilterResult = useResultStore(
50
+ state => state.showingGroundingFilterResult,
51
+ );
41
52
  const postFilterFacets = useResultStore(state => state.postFilterFacets);
42
53
  const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
43
54
  const setPostFilterSelections = useRequestStore(
44
55
  state => state.setPostFilterSelections,
45
56
  );
57
+ const togglePostFilterSelection = useRequestStore(
58
+ state => state.togglePostFilterSelection,
59
+ );
46
60
  const postFilterSelections = useRequestStore(
47
61
  state => state.postFilterSelections,
48
62
  );
@@ -55,6 +69,9 @@ function PostFilterFindApi({
55
69
  const setIsFindApiLoading = useUiStore(state => state.setIsFindApiLoading);
56
70
  const { t } = useTranslation();
57
71
  const settings = window.settings;
72
+ const isImageSearch = requestImages?.length > 0;
73
+ const useApiPostFilters =
74
+ !settings.algolia?.enabled && settings.postFilterUseApi === true;
58
75
 
59
76
  const scopeFilterEnabled = settings.scopeFilter;
60
77
  const scopeFilterAttributes = useScopeStore(state => state.filterAttributes);
@@ -73,35 +90,86 @@ function PostFilterFindApi({
73
90
  );
74
91
  }, [scopeFilterEnabled, scopeFilterAttributes]);
75
92
 
76
- const matchedFacet = useMemo(() => {
77
- const facets = Array.isArray(postFilterFacets) ? postFilterFacets : [];
78
- return facets.find(
79
- (entry: { name?: string }) =>
80
- entry.name?.toLocaleLowerCase() === attribute.toLocaleLowerCase(),
93
+ const productsForFilters = useMemo(() => {
94
+ if (!isImageSearch) {
95
+ return productsFromFindApi;
96
+ }
97
+
98
+ let baseProducts = productsFromFindApi;
99
+
100
+ if (query?.trim()) {
101
+ baseProducts = filterProductsByText(query, productsFromFindApi);
102
+ }
103
+
104
+ if (showingGroundingFilterResult && groundingFilterResult?.length > 0) {
105
+ const grounded = baseProducts.filter((product: any) =>
106
+ groundingFilterResult.some((result: any) => product.sku === result.sku),
107
+ );
108
+ if (grounded.length > 0) {
109
+ return grounded;
110
+ }
111
+ }
112
+
113
+ return baseProducts;
114
+ }, [
115
+ isImageSearch,
116
+ productsFromFindApi,
117
+ query,
118
+ showingGroundingFilterResult,
119
+ groundingFilterResult,
120
+ ]);
121
+
122
+ const filteredProductsForCounts = useMemo(() => {
123
+ const selections = postFilterSelections || {};
124
+ const activeSelections = Object.entries(selections).filter(
125
+ ([attr, values]) =>
126
+ attr !== attribute && Array.isArray(values) && values.length > 0,
81
127
  );
82
- }, [attribute, postFilterFacets]);
83
128
 
84
- const preFilterApiKey =
85
- preFilterList.find(
86
- filter =>
87
- filter.key?.toLocaleLowerCase() ===
88
- settings.visualSearchFilterKey?.toLocaleLowerCase(),
89
- )?.key ?? settings.visualSearchFilterKey;
129
+ if (!activeSelections.length) {
130
+ return productsForFilters;
131
+ }
132
+
133
+ return productsForFilters.filter((product: any) =>
134
+ activeSelections.every(([attr, values]) => {
135
+ const filterValues = product.filters?.[attr];
136
+ if (!Array.isArray(filterValues) || filterValues.length === 0) {
137
+ return false;
138
+ }
139
+ return values.some(value => filterValues.includes(value));
140
+ }),
141
+ );
142
+ }, [attribute, postFilterSelections, productsForFilters]);
90
143
 
91
144
  const valueCounts = useMemo(() => {
92
145
  const counts: Record<string, number> = {};
93
146
 
94
- (matchedFacet?.values || []).forEach(
95
- (entry: { value?: string; count?: number }) => {
96
- const value = entry?.value;
97
- if (value) {
98
- counts[value] = entry.count ?? 0;
99
- }
100
- },
101
- );
147
+ if (useApiPostFilters) {
148
+ const facet = postFilterFacets?.find(
149
+ entry =>
150
+ entry.name?.toLocaleLowerCase() === attribute.toLocaleLowerCase(),
151
+ );
152
+ (facet?.values || []).forEach(
153
+ (entry: { value?: string; count?: number }) => {
154
+ if (entry.value) {
155
+ counts[entry.value] = entry.count ?? 0;
156
+ }
157
+ },
158
+ );
159
+ return counts;
160
+ }
161
+
162
+ filteredProductsForCounts?.forEach((product: any) => {
163
+ const filterValues = product.filters?.[attribute];
164
+ if (Array.isArray(filterValues)) {
165
+ filterValues.forEach((val: string) => {
166
+ if (val) counts[val] = (counts[val] || 0) + 1;
167
+ });
168
+ }
169
+ });
102
170
 
103
171
  return counts;
104
- }, [matchedFacet]);
172
+ }, [attribute, filteredProductsForCounts, postFilterFacets, useApiPostFilters]);
105
173
 
106
174
  const items = useMemo(() => {
107
175
  const selectedValues = postFilterSelections?.[attribute] || [];
@@ -151,63 +219,63 @@ function PostFilterFindApi({
151
219
  }, [attribute, isVisible, onVisibilityChange]);
152
220
 
153
221
  const handleFilterClick = (value: string, isRefined: boolean) => {
222
+ if ((!isImageSearch || useApiPostFilters) && !isRefined) {
223
+ trackClick(CLICK_SUBTYPE.FILTER_APPLIED);
224
+ }
225
+
226
+ if (!useApiPostFilters) {
227
+ togglePostFilterSelection(attribute, value);
228
+ return;
229
+ }
230
+
154
231
  const currentSelections =
155
232
  useRequestStore.getState().postFilterSelections || {};
156
- const currentAttributeValues = currentSelections[attribute] || [];
157
- const nextAttributeValues = isRefined
158
- ? currentAttributeValues.filter(item => item !== value)
159
- : [...currentAttributeValues, value];
160
-
233
+ const currentValues = currentSelections[attribute] || [];
234
+ const nextValues = isRefined
235
+ ? currentValues.filter(item => item !== value)
236
+ : [...currentValues, value];
161
237
  const nextSelections = { ...currentSelections };
162
- if (nextAttributeValues.length > 0) {
163
- nextSelections[attribute] = nextAttributeValues;
238
+ if (nextValues.length > 0) {
239
+ nextSelections[attribute] = nextValues;
164
240
  } else {
165
241
  delete nextSelections[attribute];
166
242
  }
167
-
168
243
  setPostFilterSelections(nextSelections);
169
244
 
170
- const hasAnySelection = Object.values(nextSelections).some(
171
- values => values.length > 0,
172
- );
173
-
174
- if (!hasAnySelection) {
245
+ const requestId = ++postFilterRequestCounter;
246
+ if (!Object.values(nextSelections).some(values => values.length > 0)) {
175
247
  const snapshot = useResultStore.getState().initialFindSearchSnapshot;
176
248
  if (snapshot) {
177
249
  setFindApiProducts(snapshot.results);
178
250
  }
251
+ setIsFindApiLoading(false);
179
252
  return;
180
253
  }
181
254
 
182
- const facets = useResultStore.getState().postFilterFacets || [];
255
+ const preFilterApiKey =
256
+ preFilterList.find(
257
+ filter =>
258
+ filter.key?.toLocaleLowerCase() ===
259
+ settings.visualSearchFilterKey?.toLocaleLowerCase(),
260
+ )?.key ?? settings.visualSearchFilterKey;
183
261
  const filters = [];
184
-
185
262
  if (!isEmpty(preFilter)) {
186
263
  filters.push({
187
264
  key: preFilterApiKey,
188
265
  values: Object.keys(preFilter).filter(key => preFilter[key]),
189
266
  });
190
267
  } else if (metaFilter) {
191
- filters.push({
192
- key: preFilterApiKey,
193
- values: [metaFilter],
194
- });
268
+ filters.push({ key: preFilterApiKey, values: [metaFilter] });
195
269
  }
196
-
197
270
  Object.entries(nextSelections).forEach(([attr, values]) => {
198
271
  if (values.length > 0) {
199
- const facet = facets.find(
200
- (entry: { name?: string }) =>
201
- entry.name?.toLocaleLowerCase() === attr.toLocaleLowerCase(),
272
+ const facet = useResultStore.getState().postFilterFacets?.find(
273
+ entry => entry.name?.toLocaleLowerCase() === attr.toLocaleLowerCase(),
202
274
  );
203
- filters.push({
204
- key: facet?.name ?? attr,
205
- values,
206
- });
275
+ filters.push({ key: facet?.name ?? attr, values });
207
276
  }
208
277
  });
209
278
 
210
- const requestId = ++postFilterRequestCounter;
211
279
  setIsFindApiLoading(true);
212
280
  void find({
213
281
  image: requestImages[0],
@@ -215,14 +283,22 @@ function PostFilterFindApi({
215
283
  region: regions[0],
216
284
  filters,
217
285
  text: !requestImages[0] ? query : undefined,
218
- scopeIds: window.settings?.scopeFilter ? user?.email : undefined,
286
+ scopeIds: settings.scopeFilter ? user?.email : undefined,
219
287
  })
220
288
  .then(response => {
221
289
  if (requestId !== postFilterRequestCounter) {
222
290
  return;
223
291
  }
224
292
  setFindApiProducts(response?.results || []);
293
+ if (response?.id) {
294
+ const session = useResultStore.getState();
295
+ session.setRequestId(response.id);
296
+ if (response.session) {
297
+ session.setSessionId(response.session);
298
+ }
299
+ }
225
300
  })
301
+ .catch(() => undefined)
226
302
  .finally(() => {
227
303
  if (requestId === postFilterRequestCounter) {
228
304
  setIsFindApiLoading(false);
@@ -10,7 +10,9 @@ 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
+ import useScopeStore, { hasActiveMachineScope } from 'stores/scope/scopeStore';
14
+ import { trackClick } from 'services/Feedback';
15
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
14
16
 
15
17
  interface Props {
16
18
  handleClose?: any;
@@ -172,6 +174,9 @@ const PreFilterComponent = (props: Props) => {
172
174
  setPreFilter(preFilter);
173
175
 
174
176
  const preFilterValues = Object.keys(preFilter) as string[];
177
+ if (preFilterValues.length > 0) {
178
+ trackClick(CLICK_SUBTYPE.PRE_FILTER_APPLIED);
179
+ }
175
180
  const filter =
176
181
  preFilterValues.length > 0
177
182
  ? preFilterValues
@@ -192,7 +197,10 @@ const PreFilterComponent = (props: Props) => {
192
197
 
193
198
  handleClose();
194
199
  const query = useRequestStore.getState().query;
195
- if (!window.settings?.algolia.enabled && requestImages.length === 0) {
200
+ if (
201
+ requestImages.length === 0 &&
202
+ (hasActiveMachineScope() || !window.settings?.algolia.enabled)
203
+ ) {
196
204
  singleImageSearch({
197
205
  image: requestImages[0],
198
206
  imageRegion: regions[0],