@nyris/nyris-webapp 0.3.91 → 0.3.92

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 (106) hide show
  1. package/build/asset-manifest.json +6 -6
  2. package/build/data/related-parts.json +83 -0
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +3 -0
  5. package/build/static/css/main.5ea01690.css +4 -0
  6. package/build/static/css/main.5ea01690.css.map +1 -0
  7. package/build/static/js/main.36b77705.js +3 -0
  8. package/build/static/js/{main.f2255597.js.map → main.36b77705.js.map} +1 -1
  9. package/package.json +4 -3
  10. package/public/data/related-parts.json +83 -0
  11. package/public/js/settings.example.js +3 -0
  12. package/src/App.test.tsx +0 -1
  13. package/src/App.tsx +0 -1
  14. package/src/assets/arrow_down_expanded.svg +3 -0
  15. package/src/assets/arrow_enter.svg +3 -0
  16. package/src/assets/camera.svg +3 -0
  17. package/src/assets/close.svg +3 -0
  18. package/src/assets/enter.svg +3 -0
  19. package/src/assets/refresh.svg +3 -0
  20. package/src/assets/vizo_avatar.svg +16 -0
  21. package/src/components/Cadenas/CadenasWebViewer.tsx +1 -1
  22. package/src/components/Cart.tsx +48 -36
  23. package/src/components/ChatAssistant/ChatAssistant.tsx +289 -0
  24. package/src/components/ChatAssistant/MobileChatAssistant.tsx +291 -0
  25. package/src/components/ChatAssistant/OptionChip.tsx +78 -0
  26. package/src/components/ChatAssistant/index.ts +3 -0
  27. package/src/components/ChatAssistant/useChatAssistantLogic.ts +745 -0
  28. package/src/components/CurrentRefinements.tsx +2 -2
  29. package/src/components/CustomCameraDrawer.tsx +56 -13
  30. package/src/components/DragDropFile.tsx +5 -5
  31. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +1 -1
  32. package/src/components/Header.tsx +116 -96
  33. package/src/components/Hint.tsx +1 -2
  34. package/src/components/HitsPerPage.tsx +9 -3
  35. package/src/components/ImagePreview.tsx +32 -17
  36. package/src/components/ImageUpload.tsx +16 -8
  37. package/src/components/Inquiry/InquiryBanner.tsx +1 -1
  38. package/src/components/Inquiry/InquiryModal.tsx +35 -29
  39. package/src/components/ItemSpecification.tsx +58 -126
  40. package/src/components/LocationInfoPopup.tsx +33 -33
  41. package/src/components/MatchNotificationBanner.tsx +90 -36
  42. package/src/components/PostFilter/PostFilter.tsx +1 -1
  43. package/src/components/PostFilter/PostFilterComponent.tsx +0 -1
  44. package/src/components/PostFilter/PostFilterFindApi.tsx +0 -1
  45. package/src/components/PoweredBy.tsx +1 -1
  46. package/src/components/PreFilter/PreFilter.tsx +14 -3
  47. package/src/components/PreFilter/PreFilterModal.tsx +0 -1
  48. package/src/components/Product/Product.tsx +15 -11
  49. package/src/components/Product/ProductAttribute.tsx +4 -5
  50. package/src/components/Product/ProductDetailViewModal.tsx +2 -4
  51. package/src/components/Product/ProductList.tsx +26 -13
  52. package/src/components/Rfq/RfqModal.tsx +1 -1
  53. package/src/components/SidePanel.tsx +124 -91
  54. package/src/components/SmartFilter.tsx +320 -0
  55. package/src/components/TextSearch.tsx +134 -70
  56. package/src/components/UploadDisclaimer.tsx +1 -1
  57. package/src/hooks/useBadResultsRecovery.ts +407 -0
  58. package/src/hooks/useEffectiveGroundingResults.ts +54 -0
  59. package/src/hooks/useGoodResultsChat.ts +651 -0
  60. package/src/hooks/useGroundedSearch.ts +88 -0
  61. package/src/hooks/useImageSearch.ts +139 -187
  62. package/src/hooks/useResultEvaluator.ts +417 -0
  63. package/src/index.css +1 -1
  64. package/src/index.tsx +0 -1
  65. package/src/layouts/AppLayout.tsx +53 -2
  66. package/src/pages/Home.tsx +11 -52
  67. package/src/pages/Login.tsx +1 -2
  68. package/src/pages/Logout.tsx +1 -1
  69. package/src/pages/Result.tsx +198 -200
  70. package/src/providers/AuthProvider.tsx +0 -1
  71. package/src/services/Feedback.ts +1 -1
  72. package/src/services/visualSearch.ts +0 -21
  73. package/src/services/vizo.ts +192 -4
  74. package/src/stores/chat/chatStore.ts +150 -0
  75. package/src/stores/chat/conversationStore.ts +300 -0
  76. package/src/stores/request/Misc/misc.slice.ts +2 -2
  77. package/src/stores/request/filter/filter.slice.ts +8 -8
  78. package/src/stores/request/query/query.slice.ts +2 -2
  79. package/src/stores/request/requestImage/requestImage.slice.ts +6 -6
  80. package/src/stores/request/specifications/specifications.slice.ts +10 -7
  81. package/src/stores/result/detectedRegions/detectedRegions.slice.ts +1 -1
  82. package/src/stores/result/prodcuts/products.initialState.ts +12 -0
  83. package/src/stores/result/prodcuts/products.slice.ts +28 -8
  84. package/src/stores/result/session/session.slice.ts +2 -2
  85. package/src/stores/smartFilters/smartFiltersStore.ts +270 -0
  86. package/src/stores/types.ts +41 -0
  87. package/src/stores/ui/ai/ai.initialState.ts +5 -0
  88. package/src/stores/ui/ai/ai.slice.ts +15 -0
  89. package/src/stores/ui/banner/banner.initialState.ts +6 -0
  90. package/src/stores/ui/banner/banner.slice.ts +14 -0
  91. package/src/stores/ui/feedback/feedback.slice.ts +1 -1
  92. package/src/stores/ui/loading/loading.slice.ts +4 -4
  93. package/src/stores/ui/uiStore.ts +7 -1
  94. package/src/styles/product.scss +0 -2
  95. package/src/types.ts +3 -7
  96. package/src/utils/cropImageToBase64.ts +32 -0
  97. package/src/utils/fetchProductImage.ts +109 -0
  98. package/src/utils/imageConverters.ts +124 -0
  99. package/src/utils/relatedParts.ts +35 -0
  100. package/src/utils/specificationFilter.ts +1 -5
  101. package/tailwind.config.js +3 -2
  102. package/build/static/css/main.734b52e1.css +0 -4
  103. package/build/static/css/main.734b52e1.css.map +0 -1
  104. package/build/static/js/main.f2255597.js +0 -3
  105. package/src/utils/addAssets.ts +0 -40
  106. /package/build/static/js/{main.f2255597.js.LICENSE.txt → main.36b77705.js.LICENSE.txt} +0 -0
@@ -0,0 +1,291 @@
1
+ import { memo, useState, useRef, useCallback } from 'react';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { Sparkles, Send, Loader2, AlertCircle, Globe } from 'lucide-react';
4
+
5
+ import { Icon } from '@nyris/nyris-react-components';
6
+ import useUiStore from 'stores/ui/uiStore';
7
+ import { useTranslation } from 'react-i18next';
8
+ import CustomCamera from 'components/CustomCameraDrawer';
9
+ import ImageUpload from 'components/ImageUpload';
10
+ import OptionChip, { ChatOption } from './OptionChip';
11
+ import { useChatAssistantLogic } from './useChatAssistantLogic';
12
+
13
+ function MobileChatAssistant() {
14
+ const { t } = useTranslation();
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+
17
+ const {
18
+ inputValue,
19
+ setInputValue,
20
+ messages,
21
+ isLoading,
22
+ isEvaluated,
23
+ error,
24
+ hasApiKey,
25
+ p7Running,
26
+ p7Done,
27
+ catalogSearching,
28
+ messagesEndRef,
29
+ imageInputRef,
30
+ handleSendMessage,
31
+ handleOptionClick,
32
+ handleImageUpload,
33
+ } = useChatAssistantLogic();
34
+
35
+ const setIsAiModeOpen = useUiStore(state => state.setIsAiModeOpen);
36
+
37
+ const [isOpenModalCamera, setOpenModalCamera] = useState<boolean>(false);
38
+
39
+ // Focus input on text_search_prompt option click
40
+ const wrappedHandleOptionClick = useCallback(
41
+ async (option: ChatOption) => {
42
+ if (option.action_type === 'text_search_prompt') {
43
+ inputRef.current?.focus();
44
+ }
45
+ if (option.action_type === 'related_part') {
46
+ inputRef.current?.focus();
47
+ }
48
+ await handleOptionClick(option);
49
+ },
50
+ [handleOptionClick],
51
+ );
52
+
53
+ return (
54
+ <div className="fixed inset-x-0 bottom-0 z-[50] flex flex-col justify-end max-h-[50vh]">
55
+ {/* Chat container — anchored to bottom, content-based height, max 50vh */}
56
+ <div
57
+ className={twMerge(
58
+ 'relative flex flex-col max-h-[50vh] overflow-hidden bg-white/40 backdrop-blur-[8px] rounded-t-[24px] ',
59
+ 'shadow-[0px_0px_32px_0px_rgba(152,155,183,1)]',
60
+ )}
61
+ >
62
+ {/* Close / collapse chevron */}
63
+ <div className="flex items-center justify-end px-4 pt-3 pb-1 absolute top-4 right-4">
64
+ <button
65
+ type="button"
66
+ className="w-8 h-8 flex items-center justify-center rounded-lg bg-[#FAFAFA]"
67
+ onClick={() => setIsAiModeOpen(false)}
68
+ >
69
+ <Icon name="arrow_down" className="fill-[#3B3E5F] w-4 h-4" />
70
+ </button>
71
+ </div>
72
+
73
+ {error && (
74
+ <div className="flex-shrink-0 mx-4 mt-1 p-2.5 bg-red-50 border border-red-200 rounded-lg flex items-center gap-2">
75
+ <AlertCircle size={12} className="text-red-500 flex-shrink-0" />
76
+ <p className="text-[11px] text-red-700">{error}</p>
77
+ </div>
78
+ )}
79
+
80
+ {/* Messages area */}
81
+ <div className="flex-1 min-h-0 overflow-y-auto py-3 space-y-3 px-4">
82
+ {messages.length === 0 && !isLoading ? (
83
+ <div className="flex flex-col items-center justify-center flex-grow text-center px-4 py-10">
84
+ <div className="w-12 h-12 rounded-full bg-[#F0EFFF] flex items-center justify-center mb-3">
85
+ <Sparkles size={20} className="text-[#3E36DC]" />
86
+ </div>
87
+ <p className="text-sm font-semibold text-[#2B2C46] mb-1">
88
+ Hi! I can help you find parts faster. What are you looking for?
89
+ </p>
90
+ <p className="text-[11px] text-[#999] leading-relaxed max-w-[200px]">
91
+ Upload an image to get started, or search for a product
92
+ </p>
93
+ </div>
94
+ ) : (
95
+ messages.map((msg, i) => (
96
+ <div
97
+ key={msg.id || i}
98
+ className={twMerge(
99
+ 'flex',
100
+ msg.role === 'user' ? 'justify-end' : 'justify-start',
101
+ )}
102
+ >
103
+ <div className="max-w-[85%]">
104
+ <div
105
+ className={twMerge(
106
+ 'py-3 px-4 text-sm leading-relaxed',
107
+ msg.role === 'user'
108
+ ? 'bg-[#3E36DC] text-white rounded-tl-[16px] rounded-tr-[8px] rounded-br-[16px] rounded-bl-[16px] px-4 py-3 font-medium'
109
+ : msg?.content?.startsWith('[P7')
110
+ ? 'bg-[#EEF2FF] border border-[#C7D2FE] text-[#2B2C46] rounded-2xl rounded-bl-sm'
111
+ : 'bg-white text-[#2B2C46] rounded-2xl rounded-bl-sm shadow-sm',
112
+ )}
113
+ >
114
+ {msg.imageUrl && (
115
+ <img
116
+ src={msg.imageUrl}
117
+ alt="Uploaded"
118
+ className="w-[48px] h-[48px] rounded-lg object-cover mb-2"
119
+ />
120
+ )}
121
+ {msg?.content?.startsWith('[P7') && (
122
+ <div className="flex items-center gap-1 mb-1.5 text-indigo-600 font-semibold text-[10px]">
123
+ <Globe size={10} /> Google Grounding
124
+ </div>
125
+ )}
126
+ <pre className="whitespace-pre-wrap font-sans text-sm leading-5 tracking-[0.16px]">
127
+ {msg?.content?.startsWith('[P7')
128
+ ? msg.content.replace(/^\[P7[^\]]*\]\s*/, '')
129
+ : msg.content}
130
+ </pre>
131
+ </div>
132
+
133
+ {msg.role === 'assistant' &&
134
+ msg.options &&
135
+ msg.options.length > 0 && (
136
+ <div className="mt-2 space-y-2.5">
137
+ <div className="flex flex-wrap gap-1.5">
138
+ {msg.options
139
+ .filter(o => o.action_type !== 'text_search_prompt')
140
+ .map((option, optIdx) => (
141
+ <OptionChip
142
+ key={optIdx}
143
+ option={option}
144
+ onClick={wrappedHandleOptionClick}
145
+ disabled={
146
+ isLoading || i !== messages.length - 1
147
+ }
148
+ />
149
+ ))}
150
+ </div>
151
+ </div>
152
+ )}
153
+ </div>
154
+ </div>
155
+ ))
156
+ )}
157
+
158
+ {isLoading && (
159
+ <div className="flex justify-start">
160
+ <div className="flex items-center gap-2 px-3 py-2.5 bg-white/80 rounded-2xl rounded-bl-sm shadow-sm">
161
+ <Loader2 size={12} className="animate-spin text-[#3E36DC]" />
162
+ <span className="text-xs text-[#666]">
163
+ {!isEvaluated
164
+ ? 'Analyzing results...'
165
+ : catalogSearching
166
+ ? 'Refining your search…'
167
+ : 'Thinking...'}
168
+ </span>
169
+ </div>
170
+ </div>
171
+ )}
172
+
173
+ <div ref={messagesEndRef} />
174
+ </div>
175
+
176
+ {/* P7 Grounding status strip */}
177
+ {(p7Running || p7Done) && (
178
+ <div className="flex-shrink-0 px-4 py-1.5 bg-white/60">
179
+ {p7Running ? (
180
+ <span className="flex items-center gap-1.5 text-[10px] text-blue-600">
181
+ <Globe size={10} className="animate-pulse" /> Running Google
182
+ Grounding...
183
+ </span>
184
+ ) : (
185
+ <span className="flex items-center gap-1.5 text-[10px] text-blue-500">
186
+ <Globe size={10} /> Google Grounding complete
187
+ </span>
188
+ )}
189
+ </div>
190
+ )}
191
+
192
+ {/* Bottom input bar */}
193
+ <div className="flex-shrink-0 px-3 pb-4 pt-2">
194
+ <form
195
+ onSubmit={e => {
196
+ e.preventDefault();
197
+ handleSendMessage();
198
+ }}
199
+ className="flex items-center gap-2"
200
+ >
201
+ <div
202
+ className={twMerge(
203
+ 'flex items-center gap-2 flex-1 h-14',
204
+ 'rounded-[32px] border-[2px] border-solid border-[#655EE3] bg-[#F3F4F8] p-2',
205
+ )}
206
+ >
207
+ <button
208
+ type="button"
209
+ className="flex items-center justify-center p-2 rounded-full flex-shrink-0"
210
+ onClick={() => setIsAiModeOpen(false)}
211
+ >
212
+ <Icon
213
+ name="search_ai"
214
+ width={20}
215
+ height={20}
216
+ className="text-[#3B3E5F]"
217
+ />
218
+ </button>
219
+ <input
220
+ ref={inputRef}
221
+ className="flex-1 min-w-0 bg-transparent outline-none text-sm text-[#2B2C46] placeholder-[#989BB7]"
222
+ placeholder={isLoading ? 'Please wait...' : t('Ask Vizo!')}
223
+ disabled={isLoading}
224
+ value={inputValue}
225
+ onChange={e => setInputValue(e.target.value)}
226
+ onKeyDown={e => {
227
+ if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
228
+ e.preventDefault();
229
+ handleSendMessage();
230
+ }
231
+ }}
232
+ />
233
+ <button
234
+ type="submit"
235
+ className="flex items-center justify-center w-10 h-10 rounded-full flex-shrink-0 bg-[#FAFAFA]"
236
+ disabled={!inputValue.trim() || isLoading || !hasApiKey}
237
+ >
238
+ <Send
239
+ size={16}
240
+ className={twMerge(
241
+ inputValue.trim() && !isLoading
242
+ ? 'text-[#655EE3]'
243
+ : 'text-[#BBBDCF]',
244
+ )}
245
+ />
246
+ </button>
247
+ </div>
248
+
249
+ <div
250
+ className={twMerge(
251
+ 'h-14 min-w-14 flex items-center justify-center rounded-full bg-[#FFFFFF] border border-solid border-[#DDDEE7]',
252
+ 'shadow-ds-2',
253
+ )}
254
+ >
255
+ <ImageUpload
256
+ onCameraClick={() => {
257
+ setOpenModalCamera(true);
258
+ }}
259
+ disableDisclaimer={true}
260
+ />
261
+ </div>
262
+ </form>
263
+
264
+ {/* Hidden file input for image upload */}
265
+ <input
266
+ ref={imageInputRef}
267
+ type="file"
268
+ accept="image/*"
269
+ className="hidden"
270
+ onChange={e => {
271
+ const file = e.target.files?.[0];
272
+ if (file) {
273
+ handleImageUpload(file);
274
+ e.target.value = '';
275
+ }
276
+ }}
277
+ />
278
+ </div>
279
+ </div>
280
+ <CustomCamera
281
+ show={isOpenModalCamera}
282
+ onClose={() => {
283
+ setOpenModalCamera(s => !s);
284
+ }}
285
+ handleImageUpload={handleImageUpload}
286
+ />
287
+ </div>
288
+ );
289
+ }
290
+
291
+ export default memo(MobileChatAssistant);
@@ -0,0 +1,78 @@
1
+ import { RotateCcw, Search, Wrench } from 'lucide-react';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ import Tooltip from 'components/Tooltip/TooltipComponent';
5
+ import { Icon } from '@nyris/nyris-react-components';
6
+
7
+ export type ChatOption = {
8
+ action_type: string;
9
+ label: string;
10
+ value?: string;
11
+ };
12
+
13
+ type OptionChipProps = {
14
+ option: ChatOption;
15
+ onClick: (option: ChatOption) => void | Promise<void>;
16
+ disabled?: boolean;
17
+ };
18
+
19
+ function OptionChip({ option, onClick, disabled = false }: OptionChipProps) {
20
+ const isRestoreOriginal = option.action_type === 'restore_original';
21
+ const isFilter = option.action_type === 'filter';
22
+ const isRelatedPart =
23
+ option.action_type === 'related_part' ||
24
+ option.action_type === 'find_related_parts';
25
+ const isCatalogSearchImage = option.action_type === 'catalog_search_image';
26
+ const isCatalogSearchText = option.action_type === 'catalog_search_text';
27
+ const displayText =
28
+ isFilter && option.value?.trim() ? option.value : option.label;
29
+
30
+ return (
31
+ <Tooltip content={displayText} disabled={displayText.length <= 32}>
32
+ <button
33
+ onClick={() => onClick(option)}
34
+ disabled={disabled}
35
+ className={twMerge(
36
+ 'flex items-center',
37
+ 'px-2 py-1.5 text-xs font-medium rounded-full',
38
+ 'transition-colors',
39
+ disabled && 'opacity-50 cursor-not-allowed',
40
+ 'bg-[#FFFFFF] font-semibold',
41
+ 'border border-[#DDDEE7] text-[#767A9F] hover:bg-[#545987] hover:text-[#F3F4F8]',
42
+ 'h-8',
43
+ isRelatedPart &&
44
+ 'border border-[#E67E22] text-[#E67E22] bg-[#FFF8F0] hover:bg-[#E67E22] hover:text-white',
45
+ (isRestoreOriginal || isCatalogSearchImage || isCatalogSearchText) &&
46
+ 'border border-[#C7D2FE] bg-[#EEF2FF] text-[#3E36DC] hover:bg-[#E0E7FF] hover:text-[#2F29B7]',
47
+ )}
48
+ >
49
+ {isCatalogSearchImage && (
50
+ <div className="pr-1 inline ">
51
+ <Icon
52
+ name="camera_simple"
53
+ className="inline shrink-0 w-[12px] h-[12px]"
54
+ />
55
+ </div>
56
+ )}
57
+ {isCatalogSearchText && (
58
+ <div className="pr-1 inline ">
59
+ <Search size={11} className="inline shrink-0" />
60
+ </div>
61
+ )}
62
+ {isRestoreOriginal && (
63
+ <div className="pr-1 inline">
64
+ <RotateCcw size={11} className="inline shrink-0" />
65
+ </div>
66
+ )}
67
+ {isRelatedPart && (
68
+ <div className="inline pr-1">
69
+ <Wrench size={11} className="inline shrink-0" />
70
+ </div>
71
+ )}
72
+ <span className="max-line-1 text-start">{displayText}</span>
73
+ </button>
74
+ </Tooltip>
75
+ );
76
+ }
77
+
78
+ export default OptionChip;
@@ -0,0 +1,3 @@
1
+ export { default as ChatAssistant } from './ChatAssistant';
2
+ export { default as MobileChatAssistant } from './MobileChatAssistant';
3
+ export { useChatAssistantLogic } from './useChatAssistantLogic';