@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
@@ -14,6 +14,11 @@ import useConversationStore, {
14
14
  } from 'stores/chat/conversationStore';
15
15
  import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
16
16
  import { chatAnalysis } from 'services/vizo';
17
+ import {
18
+ getChatAttributeFrequency,
19
+ getChatProductAttributes,
20
+ getChatProductMetadata,
21
+ } from 'utils/chatProductMetadata';
17
22
  import useRequestStore from 'stores/request/requestStore';
18
23
  import { canvasToBase64, cropImage } from 'utils/cropImageToBase64';
19
24
  import {
@@ -47,10 +52,18 @@ export function useResultEvaluator() {
47
52
  async ({
48
53
  imageAnalysis,
49
54
  activeProducts,
55
+ query,
50
56
  }: {
51
57
  imageAnalysis: any;
52
58
  activeProducts: any[];
59
+ query: string;
53
60
  }) => {
61
+ // Bind this run to the current session; a newer search calls
62
+ // startNewSession, which makes any in-flight evaluation stale.
63
+ const runSessionId = useConversationStore.getState().sessionId;
64
+ const isStale = () =>
65
+ useConversationStore.getState().sessionId !== runSessionId;
66
+
54
67
  setIsLoading(true);
55
68
  setError(null);
56
69
  setRelatedPartsTableSuggestions([]);
@@ -66,29 +79,8 @@ export function useResultEvaluator() {
66
79
  const resultsSummary = activeProducts
67
80
  .slice(0, 30)
68
81
  .map((p: any, i: number) => {
69
- // Extract all potentially useful metadata
70
- const metadata: Record<string, any> = {};
71
- const metadataFields = [
72
- 'voltage',
73
- 'size',
74
- 'brand',
75
- 'manufacturer',
76
- 'material',
77
- 'type',
78
- 'category',
79
- 'color',
80
- 'dimensions',
81
- 'power',
82
- 'current',
83
- 'connector',
84
- 'model',
85
- 'series',
86
- 'product_line',
87
- ];
88
-
89
- metadataFields.forEach(field => {
90
- if (p[field]) metadata[field] = p[field];
91
- });
82
+ const fullMetadata = getChatProductMetadata(p);
83
+ const metadata = getChatProductAttributes(fullMetadata);
92
84
 
93
85
  // Extract from title/description using common patterns
94
86
  const title = p.title || p.name || '';
@@ -173,11 +165,8 @@ export function useResultEvaluator() {
173
165
 
174
166
  return {
175
167
  rank: i + 1,
176
- title: title || 'Untitled',
177
- description: (p.description || '').substring(0, 100),
178
- category: p.category || p.type || 'Unknown',
179
- score: p.score || p.similarity || 0,
180
- metadata: Object.keys(metadata).length > 0 ? metadata : null,
168
+ metadata: fullMetadata,
169
+ attributes: metadata,
181
170
  };
182
171
  });
183
172
 
@@ -195,39 +184,37 @@ export function useResultEvaluator() {
195
184
  });
196
185
 
197
186
  // Analyze common attributes across results for filter suggestions
198
- const attributeFrequency: Record<string, Record<string, number>> = {};
199
- resultsSummary.forEach(r => {
200
- if (r.metadata) {
201
- Object.entries(r.metadata).forEach(([key, value]) => {
202
- if (!attributeFrequency[key]) attributeFrequency[key] = {};
203
- const strValue = String(value);
204
- attributeFrequency[key][strValue] =
205
- (attributeFrequency[key][strValue] || 0) + 1;
206
- });
207
- }
208
- });
187
+ const attributeFrequency = getChatAttributeFrequency(
188
+ resultsSummary.map(result => ({ metadata: result.attributes })),
189
+ );
190
+
191
+ const requestImage = useRequestStore.getState().requestImages;
192
+ const regions = useRequestStore.getState().regions;
193
+ const hasRequestImage = requestImage && requestImage[0];
194
+
195
+ const evaluationTarget = hasRequestImage
196
+ ? 'the image'
197
+ : 'the user query (keyword input)';
209
198
 
210
199
  const userPrompt = `Evaluate these search results:
211
200
 
212
- IMAGE DESCRIPTION: ${imageDescription}
201
+ user query: ${query || 'N/A'}
202
+
203
+ IMAGE DESCRIPTION: ${hasRequestImage ? imageDescription : 'N/A'}
213
204
 
214
205
  SPECIFICATION (from image analysis):
215
- ${JSON.stringify(p4Specification, null, 2)}
206
+ ${hasRequestImage ? JSON.stringify(p4Specification, null, 2) : 'N/A'}
216
207
 
217
- TOP 20 RESULTS WITH METADATA:
218
- ${resultsSummary
219
- .map(
220
- r =>
221
- `${r.rank}. ${r.title}${r.description ? ` - ${r.description}` : ''} (Score: ${r.score})${r.metadata ? `\n Metadata: ${JSON.stringify(r.metadata)}` : ''}`,
222
- )
223
- .join('\n')}
224
208
 
225
209
  COMMON ATTRIBUTES FOUND IN RESULTS (for filter suggestions):
226
210
  ${JSON.stringify(attributeFrequency, null, 2)}
227
211
 
228
212
  TOTAL RESULTS: ${resultsSummary.length}
229
213
 
230
- Evaluate if these results match the image. If they do (same type of product), route to P3 and suggest filter options based on the COMMON ATTRIBUTES found above (like voltage, size, brand). Do NOT suggest the obvious category match.`;
214
+ Evaluate if these results match ${evaluationTarget}. If they do (same type of product), route to P3 and suggest filter options based on the COMMON ATTRIBUTES found above (like voltage, size, brand). Do NOT suggest the obvious category match.
215
+
216
+ ${hasRequestImage ? 'The request image is available, use it for evaluation.' : 'The request image is not available, rely solely on the user query for evaluation. Rest of the image information is only result/response images.'}.
217
+ `;
231
218
 
232
219
  const productsForImages = activeProducts.slice(0, 30);
233
220
  const imageResults = await Promise.all(
@@ -250,14 +237,9 @@ Evaluate if these results match the image. If they do (same type of product), ro
250
237
  mimeType?: string;
251
238
  }> = [];
252
239
 
253
- resultsSummary.forEach((result, i) => {
254
- const originalProduct = productsForImages[i];
255
- const tags = originalProduct?._tags;
256
- const tagStr = tags
257
- ? ` | TAGS: type=${tags.productType}, color=${tags.color}, shape=${tags.shape}, material=${tags.material}`
258
- : '';
259
-
260
- const metaLine = `${result.rank}. ${result.title}${result.description ? ` - ${result.description}` : ''} (Score: ${result.score})${result.metadata ? ` | Metadata: ${JSON.stringify(result.metadata)}` : ''}${tagStr}`;
240
+ resultsSummary.forEach(({ rank, metadata }, i) => {
241
+ const result = { rank, metadata };
242
+ const metaLine = `${JSON.stringify(getChatProductMetadata(result))}. Result_images.`;
261
243
 
262
244
  if (hasVisualContext && imageResults[i]?.base64) {
263
245
  productImageParts.push({
@@ -265,27 +247,32 @@ Evaluate if these results match the image. If they do (same type of product), ro
265
247
  image: imageResults[i].base64 || '',
266
248
  mimeType: 'image/jpeg',
267
249
  });
250
+ } else {
251
+ productImageParts.push({ metadata: metaLine });
268
252
  }
269
253
  });
270
254
 
271
- const requestImage = useRequestStore.getState().requestImages;
272
- const regions = useRequestStore.getState().regions;
273
- const croppedCanvas = cropImage(requestImage[0], regions[0]);
274
- const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
255
+ const multimodalInputs: any[] = [
256
+ {
257
+ metadata: userPrompt,
258
+ },
259
+ ];
260
+
261
+ if (hasRequestImage) {
262
+ const croppedCanvas = cropImage(requestImage[0], regions[0]);
263
+ const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
264
+ multimodalInputs.push({
265
+ metadata: 'request_image',
266
+ mimeType: 'image/jpeg',
267
+ image: base64Image,
268
+ });
269
+ }
270
+
271
+ multimodalInputs.push(...productImageParts);
275
272
 
276
273
  const response = await chatAnalysis({
277
274
  promptKey: 'result_evaluator_prompt',
278
- multimodalInputs: [
279
- {
280
- metadata: userPrompt,
281
- },
282
- {
283
- metadata: 'request_image',
284
- mimeType: 'image/jpeg',
285
- image: base64Image,
286
- },
287
- ...productImageParts,
288
- ],
275
+ multimodalInputs,
289
276
  });
290
277
 
291
278
  const evaluation: P2EvaluationResult = response;
@@ -315,6 +302,8 @@ Evaluate if these results match the image. If they do (same type of product), ro
315
302
  // Check lookup table for related parts on fresh image searches and store globally.
316
303
  const table = await loadRelatedPartsTable();
317
304
 
305
+ if (isStale()) return;
306
+
318
307
  const tableSuggestions = lookupRelatedParts(
319
308
  table,
320
309
  imageAnalysis?.specification?.product_category || '',
@@ -371,6 +360,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
371
360
  );
372
361
  } catch (err: any) {
373
362
  console.error('result_evaluator_prompt', err);
363
+ if (isStale()) return;
374
364
  setRelatedPartsTableSuggestions([]);
375
365
  setError(err.message || 'Failed to evaluate results');
376
366
 
@@ -390,7 +380,7 @@ Evaluate if these results match the image. If they do (same type of product), ro
390
380
  ],
391
381
  );
392
382
  } finally {
393
- setIsLoading(false);
383
+ if (!isStale()) setIsLoading(false);
394
384
  }
395
385
  },
396
386
  // eslint-disable-next-line react-hooks/exhaustive-deps
package/src/index.css CHANGED
@@ -283,7 +283,7 @@ h2 {
283
283
 
284
284
  h1 {
285
285
  font-size: 25px;
286
- margin: 0 auto 25px;
286
+ margin: 0 auto;
287
287
  }
288
288
 
289
289
  h1.entry-title a {
@@ -1,4 +1,4 @@
1
- import { memo, useCallback, useEffect, useMemo } from 'react';
1
+ import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
2
2
  import { Outlet, useLocation } from 'react-router';
3
3
 
4
4
  import { useAuth0 } from '@auth0/auth0-react';
@@ -27,7 +27,9 @@ import {
27
27
  isNyrisApiKeyReady,
28
28
  useAuthApiKeyStore,
29
29
  } from 'utils/nyrisApiKey';
30
- import useScopeStore from 'stores/scope/scopeStore';
30
+ import useScopeStore, { hasActiveMachineScope } from 'stores/scope/scopeStore';
31
+ import useConversationStore from 'stores/chat/conversationStore';
32
+ import { useResultEvaluator } from 'hooks/useResultEvaluator';
31
33
 
32
34
  i18n.use(initReactI18next).init({
33
35
  resources: translations,
@@ -66,9 +68,13 @@ function AppLayout(): JSX.Element {
66
68
  const requestImages = useRequestStore(state => state.requestImages);
67
69
  const showLoading = useRequestStore(state => state.showLoading);
68
70
 
69
- const { status } = useInstantSearch();
71
+ const { status, results } = useInstantSearch();
70
72
  const { items } = useHits();
71
73
 
74
+ const isAiModeOpen = useUiStore(state => state.isAiModeOpen);
75
+ const { evaluateResults } = useResultEvaluator();
76
+ const lastAlgoliaEvaluationRef = useRef<string | null>(null);
77
+
72
78
  useEffect(() => {
73
79
  i18n.changeLanguage(window.settings.language);
74
80
  console.log('App version:', packageJson.version);
@@ -97,6 +103,40 @@ function AppLayout(): JSX.Element {
97
103
  // eslint-disable-next-line react-hooks/exhaustive-deps
98
104
  }, [items, scopeItemKeys]);
99
105
 
106
+ // Algolia text search never goes through singleImageSearch, so run P2 here
107
+ // on the Algolia hits once AI mode is open for the current query.
108
+ // Deliberately not keyed on the conversation session: singleImageSearch
109
+ // calls startNewSession before requestImages is set, and re-running on that
110
+ // session change would start a competing evaluation of stale Algolia hits.
111
+ useEffect(() => {
112
+ if (requestImages.length > 0 || hasActiveMachineScope()) {
113
+ // An image search or machine scope owns P2. Algolia hits are unscoped.
114
+ lastAlgoliaEvaluationRef.current = null;
115
+ return;
116
+ }
117
+
118
+ if (
119
+ !window.settings.algolia?.enabled ||
120
+ !window.settings.vizo?.chat ||
121
+ !isAiModeOpen ||
122
+ isFindApiLoading ||
123
+ !query ||
124
+ status !== 'idle' ||
125
+ results?.query !== query
126
+ ) {
127
+ return;
128
+ }
129
+
130
+ if (lastAlgoliaEvaluationRef.current === query) {
131
+ return;
132
+ }
133
+
134
+ useConversationStore.getState().startNewSession();
135
+ lastAlgoliaEvaluationRef.current = query;
136
+ evaluateResults({ activeProducts: items, imageAnalysis: undefined, query });
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [isAiModeOpen, isFindApiLoading, query, status, results, requestImages]);
139
+
100
140
  useEffect(() => {
101
141
  if (status === 'loading' || status === 'stalled') {
102
142
  setIsAlgoliaLoading(true);
@@ -1,5 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { twMerge } from 'tailwind-merge';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ArrowRight } from 'lucide-react';
3
5
 
4
6
  import DragDropFile from 'components/DragDropFile';
5
7
  import MobileCameraCTA from 'components/MobileCameraCTA';
@@ -15,6 +17,7 @@ import ImageUpload from 'components/ImageUpload';
15
17
 
16
18
  function Home() {
17
19
  const settings = window.settings;
20
+ const { t } = useTranslation();
18
21
  const { experienceVisualSearch, experienceVisualSearchImages, geoLocation } =
19
22
  settings;
20
23
  const navigate = useNavigate();
@@ -88,32 +91,75 @@ function Home() {
88
91
  ])}
89
92
  >
90
93
  {geoLocation && <LocationInfoPopup />}
91
- <div className="relative flex items-center justify-center w-full gap-2">
94
+ <div className="relative flex w-full flex-col items-center gap-6">
92
95
  {settings.headerText && (
93
96
  <div
94
97
  className={twMerge([
95
98
  'absolute',
96
- 'bottom-[49px]',
99
+ 'bottom-full',
100
+ 'mb-[25px]',
101
+ 'flex',
102
+ 'flex-col',
103
+ 'items-center',
104
+ 'gap-3',
97
105
  'text-primary',
98
106
  'font-bold',
99
107
  ])}
100
108
  >
101
- <h1 className="text-[25px]">{settings.headerText}</h1>
109
+ <h1 className="text-[25px] m-0 mb-0">{settings.headerText}</h1>
110
+ {settings.scopes?.scopeID &&
111
+ Boolean(settings.scopes?.typePlateStepOne?.trim()) &&
112
+ Boolean(settings.scopes?.typePlateStepTwo?.trim()) && (
113
+ <div
114
+ className="flex w-full items-center justify-center"
115
+ aria-label="Search flow"
116
+ >
117
+ <div className="flex items-center gap-1.5 shrink-0">
118
+ <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[#F3F4F8] border border-solid border-[#2B2C46] text-[#2B2C46] font-bold text-xs leading-none">
119
+ 1
120
+ </span>
121
+ <span className="font-semibold text-[14px] leading-none text-[#2B2C46] whitespace-nowrap">
122
+ {t(settings.scopes!.typePlateStepOne!)}
123
+ </span>
124
+ </div>
125
+
126
+ <div
127
+ className="flex items-center flex-1 min-w-[20px] max-w-[60px] mx-2.5"
128
+ aria-hidden="true"
129
+ >
130
+ <div className="h-[1.5px] flex-1 bg-[#2B2C46]" />
131
+ <ArrowRight
132
+ className="h-3.5 w-3.5 -ml-1 text-[#2B2C46] shrink-0"
133
+ strokeWidth={2}
134
+ />
135
+ </div>
136
+
137
+ <div className="flex items-center gap-1.5 shrink-0">
138
+ <span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[#2B2C46] text-white font-bold text-xs leading-none">
139
+ 2
140
+ </span>
141
+ <span className="font-semibold text-[14px] leading-none text-[#2B2C46] whitespace-nowrap">
142
+ {t(settings.scopes!.typePlateStepTwo!)}
143
+ </span>
144
+ </div>
145
+ </div>
146
+ )}
102
147
  </div>
103
148
  )}
104
-
105
- <TextSearch className="h-[56px]" showImageUpload={false} />
106
- <div
107
- className={twMerge(
108
- 'h-14 w-14 flex items-center justify-center rounded-full bg-[#FFFFFF] hover:bg-[#F3F4F8] border border-solid border-[#DDDEE7]',
109
- 'shadow-ds-2',
110
- )}
111
- >
112
- <ImageUpload />
149
+ <div className="flex items-center justify-center w-full gap-2">
150
+ <TextSearch className="h-[56px]" showImageUpload={false} />
151
+ <div
152
+ className={twMerge(
153
+ 'h-14 w-14 flex items-center justify-center rounded-full bg-[#FFFFFF] hover:bg-[#F3F4F8] border border-solid border-[#DDDEE7]',
154
+ 'shadow-ds-2',
155
+ )}
156
+ >
157
+ <ImageUpload />
158
+ </div>
113
159
  </div>
114
160
  </div>
115
161
  <div className="max-w-[532px] relative w-full">
116
- <DragDropFile />
162
+ {!settings.scopes?.scopeID && <DragDropFile />}
117
163
  {settings.experienceVisualSearch && (
118
164
  <ExperienceVisualSearchTrigger
119
165
  experienceVisualSearchBlobs={experienceVisualSearchBlobs}
@@ -148,7 +194,7 @@ function Home() {
148
194
  />
149
195
  )}
150
196
 
151
- <div className="flex desktop:hidden w-full fixed bottom-12 px-2 gap-2">
197
+ <div className="flex desktop:hidden w-full fixed bottom-12 px-2 flex-col gap-4">
152
198
  <TextSearch className="flex md:hidden w-full " />
153
199
  </div>
154
200
  </div>