@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,133 @@
1
+ import useConversationStore from './conversationStore';
2
+
3
+ beforeEach(() => {
4
+ window.settings = {
5
+ vizo: { requestSupport: true },
6
+ support: { emailInquiry: true, emailTemplateId: 'support-template' },
7
+ } as typeof window.settings;
8
+ useConversationStore.getState().reset();
9
+ });
10
+
11
+ const sendUserMessages = (count: number) => {
12
+ for (let index = 0; index < count; index += 1) {
13
+ useConversationStore.getState().addMessage({
14
+ role: 'user',
15
+ content: `Still searching ${index}`,
16
+ });
17
+ }
18
+ };
19
+
20
+ const addApiResponse = () => {
21
+ useConversationStore.getState().addMessage(
22
+ {
23
+ role: 'assistant',
24
+ content: 'Let us keep looking.',
25
+ options: [{ label: 'Continue search', action_type: 'filter' }],
26
+ },
27
+ { isApiResponse: true },
28
+ );
29
+ };
30
+
31
+ test('offers support within the third API reply, once per session', () => {
32
+ sendUserMessages(2);
33
+ addApiResponse();
34
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
35
+
36
+ sendUserMessages(1);
37
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
38
+ addApiResponse();
39
+ const state = useConversationStore.getState();
40
+ expect(state.userMessageCount).toBe(3);
41
+ expect(state.messages).toHaveLength(5);
42
+ expect(state.messages[state.messages.length - 1]).toMatchObject({
43
+ role: 'assistant',
44
+ content: 'Let us keep looking.',
45
+ options: [
46
+ { label: 'Continue search', action_type: 'filter' },
47
+ { label: 'Request support', action_type: 'request_support' },
48
+ ],
49
+ });
50
+
51
+ sendUserMessages(2);
52
+ addApiResponse();
53
+ expect(
54
+ useConversationStore
55
+ .getState()
56
+ .messages.filter(message =>
57
+ message.options?.some(
58
+ option => option.action_type === 'request_support',
59
+ ),
60
+ ),
61
+ ).toHaveLength(1);
62
+ });
63
+
64
+ test('waits for a response and excludes assistant and empty user messages', () => {
65
+ const store = useConversationStore.getState();
66
+ store.addMessage({ role: 'assistant', content: 'How can I help?' });
67
+ store.addMessage({ role: 'user', content: ' ' });
68
+ expect(useConversationStore.getState().userMessageCount).toBe(0);
69
+ sendUserMessages(3);
70
+ store.setIsLoading(true);
71
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
72
+ store.setIsLoading(false);
73
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
74
+ store.addMessage({ role: 'assistant', content: 'Something went wrong.' });
75
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
76
+ addApiResponse();
77
+ expect(useConversationStore.getState().supportPromptShown).toBe(true);
78
+ });
79
+
80
+ test('does not offer support after a successful outcome', () => {
81
+ sendUserMessages(3);
82
+ useConversationStore.getState().setSearchResolved(true);
83
+ addApiResponse();
84
+ expect(useConversationStore.getState().supportPromptShown).toBe(false);
85
+ });
86
+
87
+ test('offers support on the third reply when recovery exits without another API call', () => {
88
+ sendUserMessages(3);
89
+ useConversationStore.getState().addMessage({
90
+ role: 'assistant',
91
+ content: 'I was not able to find a match.',
92
+ action: { type: 'exit' },
93
+ });
94
+ const state = useConversationStore.getState();
95
+ expect(state.supportPromptShown).toBe(true);
96
+ expect(state.messages[state.messages.length - 1].options).toContainEqual({
97
+ label: 'Request support',
98
+ action_type: 'request_support',
99
+ });
100
+ });
101
+
102
+ test('a new session resets message counting and allows another support prompt', () => {
103
+ sendUserMessages(3);
104
+ addApiResponse();
105
+ useConversationStore.getState().startNewSession();
106
+ expect(useConversationStore.getState()).toMatchObject({
107
+ userMessageCount: 0,
108
+ supportPromptShown: false,
109
+ searchResolved: false,
110
+ });
111
+ sendUserMessages(3);
112
+ addApiResponse();
113
+ expect(useConversationStore.getState().supportPromptShown).toBe(true);
114
+ });
115
+
116
+ test('does not offer support when vizo.requestSupport is disabled', () => {
117
+ window.settings.vizo!.requestSupport = false;
118
+ sendUserMessages(3);
119
+ addApiResponse();
120
+ const state = useConversationStore.getState();
121
+ expect(state.supportPromptShown).toBe(false);
122
+ expect(state.messages[state.messages.length - 1].options).not.toContainEqual(
123
+ expect.objectContaining({ action_type: 'request_support' }),
124
+ );
125
+ });
126
+
127
+ test('offers support even when the inquiry banner and email template are disabled', () => {
128
+ window.settings.support.emailInquiry = false;
129
+ window.settings.support.emailTemplateId = '';
130
+ sendUserMessages(3);
131
+ addApiResponse();
132
+ expect(useConversationStore.getState().supportPromptShown).toBe(true);
133
+ });
@@ -39,7 +39,8 @@ export interface QuickReplyOption {
39
39
  | 'restore_previous'
40
40
  | 'related_part'
41
41
  | 'find_related_parts'
42
- | 'restore_original';
42
+ | 'restore_original'
43
+ | 'request_support';
43
44
  value?: string;
44
45
  key?: string;
45
46
  }
@@ -72,7 +73,7 @@ export interface RelatedPartSuggestion {
72
73
  interface ConversationState {
73
74
  // Session state
74
75
  sessionId: string;
75
- isEvaluated: boolean; // P2 has fired
76
+ isEvaluated: boolean; // P2 has fired
76
77
  // Routing
77
78
  route: ConversationRoute;
78
79
  quality: ResultQuality;
@@ -84,6 +85,9 @@ interface ConversationState {
84
85
  // Conversation
85
86
  messages: ChatMessage[];
86
87
  activeFilters: ActiveFilter[];
88
+ userMessageCount: number;
89
+ supportPromptShown: boolean;
90
+ searchResolved: boolean;
87
91
 
88
92
  // UI state
89
93
  isOpen: boolean;
@@ -126,8 +130,12 @@ interface ConversationActions {
126
130
  ) => void;
127
131
 
128
132
  // Messages
129
- addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'>) => void;
133
+ addMessage: (
134
+ message: Omit<ChatMessage, 'id' | 'timestamp'>,
135
+ options?: { isApiResponse?: boolean },
136
+ ) => void;
130
137
  clearMessages: () => void;
138
+ setSearchResolved: (resolved: boolean) => void;
131
139
 
132
140
  // Filters (for P3)
133
141
  addFilter: (key: string, value: string) => void;
@@ -172,6 +180,9 @@ const initialState: ConversationState = {
172
180
  attemptCounter: 0,
173
181
  messages: [],
174
182
  activeFilters: [],
183
+ userMessageCount: 0,
184
+ supportPromptShown: false,
185
+ searchResolved: false,
175
186
  isOpen: false, // Start closed so left panel is open by default
176
187
  hasNotification: false,
177
188
  isLoading: false,
@@ -240,20 +251,50 @@ const useConversationStore = create<ConversationState & ConversationActions>()(
240
251
  }));
241
252
  },
242
253
 
243
- addMessage: message =>
244
- set(state => ({
245
- messages: [
246
- ...state.messages,
247
- {
248
- ...message,
249
- id: generateId(),
250
- timestamp: Date.now(),
251
- },
252
- ],
253
- })),
254
+ addMessage: (message, { isApiResponse = false } = {}) =>
255
+ set(state => {
256
+ const shouldOfferSupport =
257
+ window.settings?.vizo?.requestSupport &&
258
+ (isApiResponse || message.action?.type === 'exit') &&
259
+ message.role === 'assistant' &&
260
+ state.userMessageCount > 2 &&
261
+ !state.supportPromptShown &&
262
+ !state.searchResolved;
263
+
264
+ return {
265
+ userMessageCount:
266
+ state.userMessageCount +
267
+ (message.role === 'user' &&
268
+ (message.content.trim() || message.imageUrl)
269
+ ? 1
270
+ : 0),
271
+ supportPromptShown: state.supportPromptShown || shouldOfferSupport,
272
+ messages: [
273
+ ...state.messages,
274
+ {
275
+ ...message,
276
+ id: generateId(),
277
+ timestamp: Date.now(),
278
+ ...(shouldOfferSupport
279
+ ? {
280
+ options: [
281
+ ...(message.options || []),
282
+ {
283
+ label: 'Request support',
284
+ action_type: 'request_support' as const,
285
+ },
286
+ ],
287
+ }
288
+ : {}),
289
+ },
290
+ ],
291
+ };
292
+ }),
254
293
 
255
294
  clearMessages: () => set({ messages: [] }),
256
295
 
296
+ setSearchResolved: searchResolved => set({ searchResolved }),
297
+
257
298
  addFilter: (key, value) =>
258
299
  set(state => ({
259
300
  activeFilters: [
@@ -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;
@@ -8,4 +8,5 @@ export const initialState: FilterState = {
8
8
  specificationFilter: {},
9
9
  postFilterSelections: {},
10
10
  preFilterLoading: false,
11
+ typePlatePreFilterValues: [],
11
12
  };
@@ -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) =>
@@ -31,6 +31,7 @@ export const initialState: ProductsState = {
31
31
  rerankStatus: 'idle',
32
32
  errorMessage: null,
33
33
  },
34
+ requestQuality: null,
34
35
  postFilterFacets: [],
35
36
  initialFindSearchSnapshot: null,
36
37
  };
@@ -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: (itemKeys: string[], filterAttributes: Filter[]) => void;
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
- resetScope: () => set(initialState),
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;
@@ -71,20 +71,21 @@ export interface ProductsState {
71
71
  groundingError: string | null;
72
72
  firstAlgoliaProducts: any[];
73
73
  fullResultPool: any[];
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
- };
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 {