@nyris/nyris-webapp 0.3.97 → 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.
Files changed (94) hide show
  1. package/build/asset-manifest.json +7 -6
  2. package/build/index.html +1 -1
  3. package/build/js/settings.example.js +41 -0
  4. package/build/static/css/main.5cb72c09.css +4 -0
  5. package/build/static/css/main.5cb72c09.css.map +1 -0
  6. package/build/static/js/main.2339271b.js +3 -0
  7. package/build/static/js/{main.9e7fb38a.js.map → main.2339271b.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 +41 -0
  11. package/src/common/assets/images/plate_info.png +0 -0
  12. package/src/components/AuthApiKeySync.tsx +34 -0
  13. package/src/components/Cart.tsx +41 -0
  14. package/src/components/ChatAssistant/useChatAssistantLogic.ts +3 -2
  15. package/src/components/GoBackButton.tsx +29 -2
  16. package/src/components/Header.tsx +3 -0
  17. package/src/components/HitsPerPage.tsx +55 -18
  18. package/src/components/ImagePreview.tsx +31 -1
  19. package/src/components/ImageUpload.tsx +14 -3
  20. package/src/components/Inquiry/InquiryModal.tsx +3 -0
  21. package/src/components/LogoutModal.tsx +2 -0
  22. package/src/components/Pagination.tsx +33 -6
  23. package/src/components/PostFilter/PostFilter.tsx +8 -1
  24. package/src/components/PostFilter/PostFilterComponent.tsx +8 -22
  25. package/src/components/PostFilter/PostFilterFindApi.test.tsx +249 -0
  26. package/src/components/PostFilter/PostFilterFindApi.tsx +128 -52
  27. package/src/components/PreFilter/PreFilter.tsx +10 -2
  28. package/src/components/Product/Product.tsx +60 -10
  29. package/src/components/Product/ProductDetailView.tsx +23 -1
  30. package/src/components/Product/ProductDetailViewModal.tsx +5 -1
  31. package/src/components/Product/ProductList.tsx +51 -16
  32. package/src/components/Rfq/RfqModal.tsx +3 -0
  33. package/src/components/ScopeSelect/ScopeSelectForm.tsx +514 -0
  34. package/src/components/ScopeSelect/ScopeSelectImageActions.tsx +62 -0
  35. package/src/components/ScopeSelect/ScopeSelectImagePreview.tsx +141 -0
  36. package/src/components/ScopeSelect/ScopeSelectInfo.tsx +68 -0
  37. package/src/components/ScopeSelect/ScopeSelectInput.tsx +123 -0
  38. package/src/components/ScopeSelect/ScopeSelectModal.tsx +310 -0
  39. package/src/components/ScopeSelect/identifyTypePlateImage.test.ts +129 -0
  40. package/src/components/ScopeSelect/identifyTypePlateImage.ts +83 -0
  41. package/src/components/ScopeSelect/parseScopeIdResponse.test.ts +103 -0
  42. package/src/components/ScopeSelect/parseScopeIdResponse.ts +72 -0
  43. package/src/components/ScopeSelect/scopeSelectHelpers.ts +54 -0
  44. package/src/components/SidePanel.tsx +43 -0
  45. package/src/components/TextSearch.tsx +80 -14
  46. package/src/components/Tooltip/TooltipComponent.tsx +12 -4
  47. package/src/components/TypePlateUpload.tsx +132 -0
  48. package/src/hooks/useEffectiveGroundingResults.ts +8 -6
  49. package/src/hooks/useGoodResultsChat.ts +8 -22
  50. package/src/hooks/useImageSearch.ts +325 -20
  51. package/src/hooks/useMachinePartsListingPagination.ts +95 -0
  52. package/src/hooks/useResultEvaluator.ts +43 -19
  53. package/src/index.css +1 -1
  54. package/src/index.tsx +4 -0
  55. package/src/layouts/AppLayout.tsx +95 -9
  56. package/src/pages/Home.tsx +60 -14
  57. package/src/pages/Logout.tsx +2 -0
  58. package/src/pages/Result.tsx +206 -20
  59. package/src/providers/AuthProvider.tsx +5 -0
  60. package/src/services/Feedback.ts +224 -35
  61. package/src/services/feedbackSubtypes.ts +63 -0
  62. package/src/services/filter.ts +4 -3
  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 +76 -7
  73. package/src/services/vizo.ts +17 -5
  74. package/src/stores/machinePartsListing/machinePartsListingStore.ts +35 -0
  75. package/src/stores/request/filter/filter.initialState.ts +1 -0
  76. package/src/stores/request/filter/filter.slice.ts +2 -0
  77. package/src/stores/result/prodcuts/products.initialState.ts +1 -0
  78. package/src/stores/result/prodcuts/products.slice.ts +4 -0
  79. package/src/stores/scope/scopeStore.ts +67 -4
  80. package/src/stores/types.ts +19 -14
  81. package/src/translations.ts +213 -0
  82. package/src/types.ts +40 -0
  83. package/src/utils/derivePostFilterFacets.ts +68 -0
  84. package/src/utils/fetchProductImage.ts +1 -0
  85. package/src/utils/normalizeChatAssistantResponse.ts +227 -0
  86. package/src/utils/nyrisApiKey.ts +115 -0
  87. package/src/utils/productCardImageUrl.test.ts +41 -0
  88. package/src/utils/productCardImageUrl.ts +49 -0
  89. package/src/utils/textSearchFilter.test.ts +41 -0
  90. package/src/utils/textSearchFilter.ts +13 -6
  91. package/build/static/css/main.7c7f5447.css +0 -4
  92. package/build/static/css/main.7c7f5447.css.map +0 -1
  93. package/build/static/js/main.9e7fb38a.js +0 -3
  94. /package/build/static/js/{main.9e7fb38a.js.LICENSE.txt → main.2339271b.js.LICENSE.txt} +0 -0
@@ -9,6 +9,46 @@ import { isEqual } from 'lodash';
9
9
 
10
10
  import { DEFAULT_REGION } from '../constants';
11
11
  import { getUserLocation } from 'helpers/getGeoLocation';
12
+ import { getNyrisSettings } from 'utils/nyrisApiKey';
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
+ }
12
52
 
13
53
  export interface Filter {
14
54
  key?: string;
@@ -41,7 +81,7 @@ export const findRegions = async (
41
81
  image: HTMLCanvasElement,
42
82
  settings: NyrisAPISettings,
43
83
  ) => {
44
- const nyrisApi = new NyrisAPI(settings);
84
+ const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
45
85
  let regions = await nyrisApi.findRegions(image);
46
86
  const selectedRegion = getRegionByMaxConfidence(regions);
47
87
  return {
@@ -52,6 +92,18 @@ export const findRegions = async (
52
92
  };
53
93
  };
54
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
+
55
107
  export const find = async ({
56
108
  image,
57
109
  settings,
@@ -67,8 +119,9 @@ export const find = async ({
67
119
  text?: string;
68
120
  scopeIds?: string;
69
121
  }) => {
70
- const nyrisApi = new NyrisAPI(settings);
71
- let options: ImageSearchOptions = text ? { text } : {};
122
+ const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
123
+ let options: ImageSearchOptions =
124
+ typeof text === 'string' && text.length > 0 ? { text } : {};
72
125
 
73
126
  if (window.settings?.geoLocation) {
74
127
  try {
@@ -84,13 +137,29 @@ export const find = async ({
84
137
  if (region) {
85
138
  options = { ...options, cropRect: region };
86
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
+ });
87
152
  return nyrisApi.find(
88
153
  options,
89
154
  image,
90
155
  filters,
91
156
  undefined,
92
- undefined,
93
- scopeIds ? [scopeIds] : [],
157
+ xOptions,
158
+ machineScopeId
159
+ ? [machineScopeId]
160
+ : scopeIds
161
+ ? [scopeIds]
162
+ : [],
94
163
  );
95
164
  };
96
165
 
@@ -103,7 +172,7 @@ export const findCad = ({
103
172
  settings: NyrisAPISettings;
104
173
  filters?: Filter[];
105
174
  }) => {
106
- const nyrisApi = new NyrisAPI(settings);
175
+ const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
107
176
 
108
177
  return nyrisApi.findByCad(file, {}, filters);
109
178
  };
@@ -115,7 +184,7 @@ export const getRequestImage = ({
115
184
  url: string;
116
185
  settings: NyrisAPISettings;
117
186
  }) => {
118
- const nyrisApi = new NyrisAPI(settings);
187
+ const nyrisApi = new NyrisAPI(getNyrisSettings(settings));
119
188
 
120
189
  return nyrisApi.getCadRequestImage(url);
121
190
  };
@@ -1,6 +1,18 @@
1
1
  import { RectCoords } from '@nyris/nyris-api';
2
2
  import axios from 'axios';
3
3
  import { cropImage } from 'utils/cropImageToBase64';
4
+ import { getEffectiveApiKey } from 'utils/nyrisApiKey';
5
+ import { normalizeChatResponse } from './normalizeChatResponse';
6
+
7
+ function resolveAiOrNyrisApiKey(explicitApiKey?: string): string {
8
+ if (explicitApiKey) {
9
+ return explicitApiKey;
10
+ }
11
+ if (window.settings.aiApiKey) {
12
+ return window.settings.aiApiKey;
13
+ }
14
+ return getEffectiveApiKey();
15
+ }
4
16
 
5
17
  export interface ChatMultimodalInput {
6
18
  metadata: string;
@@ -111,7 +123,7 @@ export async function rerankSmartFilters({
111
123
  formData,
112
124
  {
113
125
  headers: {
114
- 'x-api-key': apiKey || window.settings.aiApiKey || window.settings.apiKey,
126
+ 'x-api-key': resolveAiOrNyrisApiKey(apiKey),
115
127
  },
116
128
  },
117
129
  );
@@ -156,7 +168,7 @@ export async function generateSmartFilters({
156
168
  formData,
157
169
  {
158
170
  headers: {
159
- 'x-api-key': window.settings.aiApiKey || window.settings.apiKey,
171
+ 'x-api-key': resolveAiOrNyrisApiKey(),
160
172
  },
161
173
  },
162
174
  );
@@ -319,20 +331,20 @@ export async function chatAnalysis({
319
331
  }: ChatAnalysisParams): Promise<any> {
320
332
  try {
321
333
  const response = await axios.post(
322
- 'https://api.nyris.io/api/ai-analysis/chat',
334
+ `${window.settings.baseUrl}/api/ai-analysis/chat`,
323
335
  {
324
336
  promptKey,
325
337
  multimodalInputs,
326
338
  },
327
339
  {
328
340
  headers: {
329
- 'x-api-key': apiKey || window.settings.aiApiKey || '',
341
+ 'x-api-key': resolveAiOrNyrisApiKey(apiKey),
330
342
  'Content-Type': 'application/json',
331
343
  },
332
344
  },
333
345
  );
334
346
 
335
- return response.data;
347
+ return normalizeChatResponse(response.data);
336
348
  } catch (error: any) {
337
349
  if (error.response) {
338
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;
@@ -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 {