@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
@@ -8,11 +8,20 @@ import { Icon } from '@nyris/nyris-react-components';
8
8
  import { useImageSearch } from 'hooks/useImageSearch';
9
9
  import PreFilterModal from './PreFilter/PreFilterModal';
10
10
  import useRequestStore from 'stores/request/requestStore';
11
+ import { hasActiveMachineScope } from 'stores/scope/scopeStore';
12
+ import { firstMachinePartsListingText } from 'stores/machinePartsListing/machinePartsListingStore';
11
13
  import Tooltip from './Tooltip/TooltipComponent';
12
14
  import { useMediaQuery } from 'react-responsive';
13
15
  import ImageUpload from './ImageUpload';
16
+ import { TypePlateUpload } from './TypePlateUpload';
14
17
  import useUiStore from 'stores/ui/uiStore';
15
18
  import useConversationStore from 'stores/chat/conversationStore';
19
+ import { trackClick } from 'services/Feedback';
20
+ import { CLICK_SUBTYPE } from 'services/feedbackSubtypes';
21
+
22
+ function machinePartsListingText() {
23
+ return firstMachinePartsListingText();
24
+ }
16
25
 
17
26
  function TextSearch({
18
27
  className,
@@ -63,15 +72,19 @@ function TextSearch({
63
72
  useState<boolean>(false);
64
73
  const [isInputFocused, setIsInputFocused] = useState(false);
65
74
 
75
+ const isScopeMode = !!settings.scopes?.scopeID;
66
76
  const showPreFilter = useMemo(() => {
77
+ if (settings.scopes?.scopeID) {
78
+ return false;
79
+ }
67
80
  if (settings.shouldUseUserMetadata && user) {
68
- if (user['/user_metadata'].value) {
81
+ if (user['/user_metadata']?.value) {
69
82
  setMetaFilter(user['/user_metadata'].value);
70
83
  }
71
84
  }
72
85
 
73
86
  if (settings.shouldUseUserMetadata && user) {
74
- if (settings.preFilterOption && !user['/user_metadata'].value) {
87
+ if (settings.preFilterOption && !user['/user_metadata']?.value) {
75
88
  return true;
76
89
  }
77
90
  return false;
@@ -81,29 +94,44 @@ function TextSearch({
81
94
  }, [
82
95
  setMetaFilter,
83
96
  settings.preFilterOption,
97
+ settings.scopes?.scopeID,
84
98
  settings.shouldUseUserMetadata,
85
99
  user,
86
100
  ]);
87
101
 
88
102
  const visualSearch = useMemo(() => requestImages.length > 0, [requestImages]);
89
- const { singleImageSearch } = useImageSearch();
103
+ const { singleImageSearch, searchMachinePartsListing } = useImageSearch();
90
104
 
91
105
  // eslint-disable-next-line react-hooks/exhaustive-deps
92
106
  const searchOrRedirect = useCallback(
93
107
  debounce((value: any) => {
94
108
  setQuery(value);
95
- if (requestImages.length === 0 && value === '' && !specifications) {
96
- navigate('/');
97
- return;
109
+ if (requestImages.length === 0 && value === '') {
110
+ if (hasActiveMachineScope()) {
111
+ searchMachinePartsListing({
112
+ text: machinePartsListingText() || '',
113
+ showFeedback: true,
114
+ clearPostFilters: true,
115
+ });
116
+ navigate('/result');
117
+ return;
118
+ }
119
+ if (!specifications) {
120
+ navigate('/');
121
+ return;
122
+ }
98
123
  }
99
124
 
100
- if (!window.settings?.algolia.enabled && requestImages.length === 0) {
125
+ if (
126
+ requestImages.length === 0 &&
127
+ (hasActiveMachineScope() || !window.settings?.algolia.enabled)
128
+ ) {
101
129
  singleImageSearch({
102
130
  image: requestImages[0],
103
131
  imageRegion: regions[0],
104
132
  text: value,
105
133
  settings,
106
- showFeedback: false,
134
+ showFeedback: true,
107
135
  newSearch: false,
108
136
  });
109
137
  }
@@ -114,10 +142,28 @@ function TextSearch({
114
142
  );
115
143
 
116
144
  const onChangeText = (event: any) => {
117
- setValueInput(event.currentTarget.value);
118
- searchOrRedirect(event.currentTarget.value);
145
+ const nextValue = event.currentTarget.value;
146
+ setValueInput(nextValue);
147
+
148
+ if (
149
+ nextValue === '' &&
150
+ requestImages.length === 0 &&
151
+ hasActiveMachineScope()
152
+ ) {
153
+ searchOrRedirect.cancel();
154
+ setQuery('');
155
+ searchMachinePartsListing({
156
+ text: machinePartsListingText() || '',
157
+ showFeedback: true,
158
+ clearPostFilters: true,
159
+ });
160
+ navigate('/result');
161
+ return;
162
+ }
119
163
 
120
- if (event.currentTarget.value === '' && !specifications) {
164
+ searchOrRedirect(nextValue);
165
+
166
+ if (nextValue === '' && !specifications) {
121
167
  setValueInput('');
122
168
  setQuery('');
123
169
  }
@@ -179,6 +225,11 @@ function TextSearch({
179
225
  showPreFilter ? setToggleModalFilterDesktop(true) : false
180
226
  }
181
227
  >
228
+ {isScopeMode && (
229
+ <TypePlateUpload
230
+ iconOnly={location.pathname === '/result'}
231
+ />
232
+ )}
182
233
  {showPreFilter && (
183
234
  <Tooltip
184
235
  content={
@@ -342,7 +393,7 @@ function TextSearch({
342
393
  {nameplateNotificationText}
343
394
  </div>
344
395
  )}
345
- {!showPreFilter && (
396
+ {!isScopeMode && !showPreFilter && (
346
397
  <div className="p-2 hidden desktop:block">
347
398
  <Icon name="search" width={16} height={16} />
348
399
  </div>
@@ -390,7 +441,7 @@ function TextSearch({
390
441
  location.pathname === '/result' &&
391
442
  'desktop:bg-[#E7E8F1] focus:bg-[#FFFFFF]',
392
443
  )}
393
- placeholder={t('Search')}
444
+ placeholder={t('Search part names, SKU, or description...')}
394
445
  value={valueInput}
395
446
  onChange={onChangeText}
396
447
  onFocus={() => setIsInputFocused(true)}
@@ -419,6 +470,7 @@ function TextSearch({
419
470
  event.preventDefault();
420
471
  }}
421
472
  onClick={() => {
473
+ searchOrRedirect.cancel();
422
474
  if (visualSearch) {
423
475
  navigate('/result');
424
476
  setValueInput('');
@@ -438,6 +490,17 @@ function TextSearch({
438
490
  }
439
491
  return;
440
492
  }
493
+ if (hasActiveMachineScope()) {
494
+ setQuery('');
495
+ setValueInput('');
496
+ searchMachinePartsListing({
497
+ text: machinePartsListingText() || '',
498
+ showFeedback: false,
499
+ clearPostFilters: true,
500
+ });
501
+ navigate('/result');
502
+ return;
503
+ }
441
504
  setSpecifications(null);
442
505
  setQuery('');
443
506
  setValueInput('');
@@ -451,7 +514,7 @@ function TextSearch({
451
514
  {showImageUpload && <ImageUpload onCameraClick={onCameraClick} />}
452
515
  {aiMode &&
453
516
  window.settings.vizo?.chat &&
454
- requestImages.length !== 0 &&
517
+ (requestImages.length !== 0 || valueInput) &&
455
518
  !(isMobile && isInputFocused) && (
456
519
  <div className="relative">
457
520
  <button
@@ -467,6 +530,9 @@ function TextSearch({
467
530
  )}
468
531
  onClick={() => {
469
532
  const nextIsAiModeOpen = !isAiModeOpen;
533
+ if (nextIsAiModeOpen) {
534
+ trackClick(CLICK_SUBTYPE.CHAT_WITH_VIZO);
535
+ }
470
536
  toggleAiMode();
471
537
  setIsChatOpen(nextIsAiModeOpen);
472
538
  }}
@@ -13,20 +13,28 @@ function Tooltip({
13
13
  disabled,
14
14
  sideOffset,
15
15
  delayDuration = 80,
16
+ open,
17
+ side = 'top',
16
18
  }: {
17
- content: string;
19
+ content: React.ReactNode;
18
20
  children: React.ReactNode;
19
21
  disabled?: boolean;
20
22
  sideOffset?: number;
21
23
  delayDuration?: number;
24
+ open?: boolean;
25
+ side?: 'top' | 'right' | 'bottom' | 'left';
22
26
  }) {
23
27
  return (
24
28
  <TooltipProvider delayDuration={delayDuration}>
25
- <RadixTooltip open={disabled ? false : undefined}>
29
+ <RadixTooltip open={disabled ? false : open}>
26
30
  <TooltipTrigger asChild>{children}</TooltipTrigger>
27
- <TooltipContent sideOffset={sideOffset}>
31
+ <TooltipContent side={side} sideOffset={sideOffset}>
28
32
  <TooltipArrow />
29
- <p className="text-white max-w-[350px]">{content}</p>
33
+ {typeof content === 'string' ? (
34
+ <p className="text-white max-w-[350px]">{content}</p>
35
+ ) : (
36
+ content
37
+ )}
30
38
  </TooltipContent>
31
39
  </RadixTooltip>
32
40
  </TooltipProvider>
@@ -0,0 +1,132 @@
1
+ import { useState } from 'react';
2
+ import { Icon } from '@nyris/nyris-react-components';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { twMerge } from 'tailwind-merge';
5
+ import useScopeStore from 'stores/scope/scopeStore';
6
+ import Tooltip from './Tooltip/TooltipComponent';
7
+ import { ScopeSelectModal } from './ScopeSelect/ScopeSelectModal';
8
+
9
+ export interface TypePlateUploadProps {
10
+ className?: string;
11
+ disabledTooltip?: boolean;
12
+ alternativeView?: boolean;
13
+ /** When false, the button only opens a modal owned by the parent. */
14
+ showModal?: boolean;
15
+ open?: boolean;
16
+ onOpenChange?: (open: boolean) => void;
17
+ /** Results-page text search: icon, tooltip, and active-scope marker. */
18
+ iconOnly?: boolean;
19
+ }
20
+
21
+ export function TypePlateUpload({
22
+ className,
23
+ alternativeView = false,
24
+ showModal = true,
25
+ open,
26
+ onOpenChange,
27
+ iconOnly = false,
28
+ }: TypePlateUploadProps = {}) {
29
+ const { t } = useTranslation();
30
+ const scopeId = useScopeStore(state => state.scopeId);
31
+ const scopeItemKeys = useScopeStore(state => state.itemKeys);
32
+ const hasActiveScope = Boolean(scopeId) && scopeItemKeys.length > 0;
33
+ const [internalOpen, setInternalOpen] = useState(false);
34
+ const isControlled = open !== undefined;
35
+ const isOpen = isControlled ? open : internalOpen;
36
+ const setIsOpen = (next: boolean) => {
37
+ if (!isControlled) {
38
+ setInternalOpen(next);
39
+ }
40
+ onOpenChange?.(next);
41
+ };
42
+
43
+ if (!window.settings.scopes?.scopeID) return null;
44
+
45
+ const buttonLabel =
46
+ window.settings.scopes?.typePlateButtonLabel ||
47
+ window.settings.scopes?.typePlateButtonText ||
48
+ 'Scan type plate';
49
+ const tooltipText =
50
+ iconOnly && hasActiveScope
51
+ ? t(
52
+ 'Searching within parts for {{scopeId}}. You may scan another type plate.',
53
+ { scopeId },
54
+ )
55
+ : t('Scan Type plate');
56
+ const accessibleName = iconOnly ? tooltipText : t(buttonLabel);
57
+
58
+ const button = (
59
+ <button
60
+ type="button"
61
+ className={twMerge(
62
+ 'p-2 desktop:p-3 flex items-center gap-2 rounded-full border border-solid cursor-pointer',
63
+ alternativeView
64
+ ? 'bg-theme-primary border-theme-primary'
65
+ : 'bg-[#f3f3f5] border-[#2B2C46] desktop:bg-[#FAFAFA]',
66
+ className,
67
+ )}
68
+ aria-label={accessibleName}
69
+ onClick={() => setIsOpen(true)}
70
+ >
71
+ <Icon
72
+ name="typeplate"
73
+ width={16}
74
+ height={16}
75
+ fill={alternativeView ? '#FFFFFF' : '#2B2C46'}
76
+ />
77
+ {!iconOnly && (
78
+ <span
79
+ className={twMerge(
80
+ 'whitespace-nowrap pr-1 text-[14px] leading-none',
81
+ alternativeView ? 'text-white' : 'text-[#2B2C46]',
82
+ )}
83
+ >
84
+ {t(buttonLabel)}
85
+ </span>
86
+ )}
87
+ </button>
88
+ );
89
+
90
+ return (
91
+ <>
92
+ {iconOnly ? (
93
+ <div className="relative">
94
+ <Tooltip content={tooltipText}>{button}</Tooltip>
95
+ {hasActiveScope && (
96
+ <div
97
+ className="pointer-events-none top-2 left-8 desktop:top-[1px] desktop:left-[26px]"
98
+ style={{
99
+ position: 'absolute',
100
+ display: 'flex',
101
+ justifyContent: 'center',
102
+ alignItems: 'center',
103
+ background: 'white',
104
+ width: '12px',
105
+ height: '12px',
106
+ borderRadius: '100%',
107
+ }}
108
+ >
109
+ <div
110
+ style={{
111
+ width: '8px',
112
+ height: '8px',
113
+ background: window.settings.theme?.primaryColor,
114
+ borderRadius: '100%',
115
+ strokeWidth: '2px',
116
+ }}
117
+ />
118
+ </div>
119
+ )}
120
+ </div>
121
+ ) : (
122
+ button
123
+ )}
124
+ {showModal && (
125
+ <ScopeSelectModal
126
+ openModal={isOpen}
127
+ handleClose={() => setIsOpen(false)}
128
+ />
129
+ )}
130
+ </>
131
+ );
132
+ }
@@ -26,12 +26,14 @@ export function useEffectiveGroundingResults() {
26
26
  const postEntries = Object.entries(postFilterSelections || {}).filter(
27
27
  ([, values]) => values?.length,
28
28
  );
29
- if (
30
- postEntries.length > 0 &&
31
- !(
32
- !window.settings.algolia?.enabled && window.settings.postFilterOption
33
- )
34
- ) {
29
+ // Text-only find-API post-filters re-query; skip client-side narrowing.
30
+ // Image search uses the old client-side post-filter behavior.
31
+ const isTextOnlyFindApiPostFilter =
32
+ !window.settings.algolia?.enabled &&
33
+ window.settings.postFilterOption &&
34
+ !(requestImages?.length > 0);
35
+
36
+ if (postEntries.length > 0 && !isTextOnlyFindApiPostFilter) {
35
37
  filteredProducts = filteredProducts.filter((product: any) => {
36
38
  const productFilters = product?.filters || {};
37
39
  return postEntries.every(([attribute, selectedValues]) => {
@@ -7,10 +7,7 @@
7
7
  */
8
8
 
9
9
  import { useCallback, useRef } from 'react';
10
- import useConversationStore, {
11
- QuickReplyOption,
12
- ActionType,
13
- } from 'stores/chat/conversationStore';
10
+ import useConversationStore from 'stores/chat/conversationStore';
14
11
  import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
15
12
  import useResultStore from 'stores/result/resultStore';
16
13
  import useRequestStore from 'stores/request/requestStore';
@@ -19,6 +16,10 @@ import {
19
16
  fetchProductImageBase64,
20
17
  getProductImageUrl,
21
18
  } from 'utils/fetchProductImage';
19
+ import {
20
+ normalizeChatAssistantResponse,
21
+ P3Response,
22
+ } from 'utils/normalizeChatAssistantResponse';
22
23
 
23
24
  /**
24
25
  * Extracts search keywords from a user's natural language description.
@@ -138,18 +139,6 @@ function extractSearchKeywords(description: string): string {
138
139
  );
139
140
  }
140
141
 
141
- interface P3Response {
142
- message: string;
143
- action: {
144
- type: ActionType;
145
- key?: string;
146
- value?: string;
147
- matched_ids?: string[];
148
- matched_indices?: number[]; // Legacy fallback
149
- };
150
- options: QuickReplyOption[];
151
- }
152
-
153
142
  export function useGoodResultsChat() {
154
143
  const {
155
144
  messages,
@@ -374,7 +363,7 @@ USER MESSAGE: ${userMessage}
374
363
  Respond to the user's message with an appropriate action and follow-up options.`,
375
364
  });
376
365
 
377
- const response: P3Response = await chatAnalysis({
366
+ const rawResponse = await chatAnalysis({
378
367
  promptKey: 'chat_assistant_prompt',
379
368
  multimodalInputs: userParts.map(part => ({
380
369
  metadata: part.text,
@@ -382,11 +371,8 @@ Respond to the user's message with an appropriate action and follow-up options.`
382
371
  mimeType: part.inlineData?.mimeType,
383
372
  })),
384
373
  });
385
-
386
- const responseText = response;
387
-
388
- // Parse the JSON response
389
- const p3Response: P3Response = responseText;
374
+ const p3Response: P3Response =
375
+ normalizeChatAssistantResponse(rawResponse);
390
376
 
391
377
  // Silent mode: only suggest filters, never apply actions
392
378
  if (options?.silent && p3Response.action) {