@nyris/nyris-webapp 0.3.90 → 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.
- package/build/asset-manifest.json +6 -6
- package/build/data/related-parts.json +83 -0
- package/build/index.html +1 -1
- package/build/js/settings.example.js +3 -0
- package/build/static/css/main.5ea01690.css +4 -0
- package/build/static/css/main.5ea01690.css.map +1 -0
- package/build/static/js/main.36b77705.js +3 -0
- package/build/static/js/{main.cede3ae1.js.map → main.36b77705.js.map} +1 -1
- package/package.json +4 -3
- package/public/data/related-parts.json +83 -0
- package/public/js/settings.example.js +3 -0
- package/src/App.test.tsx +0 -1
- package/src/App.tsx +0 -1
- package/src/assets/arrow_down_expanded.svg +3 -0
- package/src/assets/arrow_enter.svg +3 -0
- package/src/assets/camera.svg +3 -0
- package/src/assets/close.svg +3 -0
- package/src/assets/enter.svg +3 -0
- package/src/assets/refresh.svg +3 -0
- package/src/assets/vizo_avatar.svg +16 -0
- package/src/components/Cadenas/CadenasWebViewer.tsx +1 -1
- package/src/components/Cart.tsx +48 -36
- package/src/components/ChatAssistant/ChatAssistant.tsx +289 -0
- package/src/components/ChatAssistant/MobileChatAssistant.tsx +291 -0
- package/src/components/ChatAssistant/OptionChip.tsx +78 -0
- package/src/components/ChatAssistant/index.ts +3 -0
- package/src/components/ChatAssistant/useChatAssistantLogic.ts +745 -0
- package/src/components/CurrentRefinements.tsx +2 -2
- package/src/components/CustomCameraDrawer.tsx +56 -13
- package/src/components/DragDropFile.tsx +5 -5
- package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +1 -1
- package/src/components/Header.tsx +116 -96
- package/src/components/Hint.tsx +1 -2
- package/src/components/HitsPerPage.tsx +9 -3
- package/src/components/ImagePreview.tsx +32 -17
- package/src/components/ImageUpload.tsx +16 -8
- package/src/components/Inquiry/InquiryBanner.tsx +1 -1
- package/src/components/Inquiry/InquiryModal.tsx +35 -29
- package/src/components/ItemSpecification.tsx +58 -126
- package/src/components/LocationInfoPopup.tsx +33 -33
- package/src/components/MatchNotificationBanner.tsx +90 -36
- package/src/components/PostFilter/PostFilter.tsx +1 -1
- package/src/components/PostFilter/PostFilterComponent.tsx +0 -1
- package/src/components/PostFilter/PostFilterFindApi.tsx +0 -1
- package/src/components/PoweredBy.tsx +1 -1
- package/src/components/PreFilter/PreFilter.tsx +14 -3
- package/src/components/PreFilter/PreFilterModal.tsx +0 -1
- package/src/components/Product/Product.tsx +15 -11
- package/src/components/Product/ProductAttribute.tsx +4 -5
- package/src/components/Product/ProductDetailViewModal.tsx +2 -4
- package/src/components/Product/ProductList.tsx +26 -13
- package/src/components/Rfq/RfqModal.tsx +1 -1
- package/src/components/SidePanel.tsx +124 -91
- package/src/components/SmartFilter.tsx +320 -0
- package/src/components/TextSearch.tsx +134 -70
- package/src/components/UploadDisclaimer.tsx +1 -1
- package/src/hooks/useBadResultsRecovery.ts +407 -0
- package/src/hooks/useEffectiveGroundingResults.ts +54 -0
- package/src/hooks/useGoodResultsChat.ts +651 -0
- package/src/hooks/useGroundedSearch.ts +88 -0
- package/src/hooks/useImageSearch.ts +139 -187
- package/src/hooks/useResultEvaluator.ts +417 -0
- package/src/index.css +1 -1
- package/src/index.tsx +0 -1
- package/src/layouts/AppLayout.tsx +53 -2
- package/src/pages/Home.tsx +11 -52
- package/src/pages/Login.tsx +1 -2
- package/src/pages/Logout.tsx +1 -1
- package/src/pages/Result.tsx +198 -200
- package/src/providers/AuthProvider.tsx +0 -1
- package/src/services/Feedback.ts +1 -1
- package/src/services/visualSearch.ts +0 -21
- package/src/services/vizo.ts +192 -4
- package/src/stores/chat/chatStore.ts +150 -0
- package/src/stores/chat/conversationStore.ts +300 -0
- package/src/stores/request/Misc/misc.slice.ts +2 -2
- package/src/stores/request/filter/filter.slice.ts +8 -8
- package/src/stores/request/query/query.slice.ts +2 -2
- package/src/stores/request/requestImage/requestImage.slice.ts +6 -6
- package/src/stores/request/specifications/specifications.slice.ts +10 -7
- package/src/stores/result/detectedRegions/detectedRegions.slice.ts +1 -1
- package/src/stores/result/prodcuts/products.initialState.ts +12 -0
- package/src/stores/result/prodcuts/products.slice.ts +28 -8
- package/src/stores/result/session/session.slice.ts +2 -2
- package/src/stores/smartFilters/smartFiltersStore.ts +270 -0
- package/src/stores/types.ts +41 -0
- package/src/stores/ui/ai/ai.initialState.ts +5 -0
- package/src/stores/ui/ai/ai.slice.ts +15 -0
- package/src/stores/ui/banner/banner.initialState.ts +6 -0
- package/src/stores/ui/banner/banner.slice.ts +14 -0
- package/src/stores/ui/feedback/feedback.slice.ts +1 -1
- package/src/stores/ui/loading/loading.slice.ts +4 -4
- package/src/stores/ui/uiStore.ts +7 -1
- package/src/styles/product.scss +0 -2
- package/src/types.ts +3 -7
- package/src/utils/cropImageToBase64.ts +32 -0
- package/src/utils/fetchProductImage.ts +109 -0
- package/src/utils/imageConverters.ts +124 -0
- package/src/utils/relatedParts.ts +35 -0
- package/src/utils/specificationFilter.ts +1 -5
- package/tailwind.config.js +3 -2
- package/build/static/css/main.734b52e1.css +0 -4
- package/build/static/css/main.734b52e1.css.map +0 -1
- package/build/static/js/main.cede3ae1.js +0 -3
- package/src/utils/addAssets.ts +0 -40
- /package/build/static/js/{main.cede3ae1.js.LICENSE.txt → main.36b77705.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P2 - Result Evaluator
|
|
3
|
+
*
|
|
4
|
+
* Fires once when results are returned from the API.
|
|
5
|
+
* Evaluates whether results match the uploaded image.
|
|
6
|
+
* Routes to P3 (good results) or P5 (bad results recovery).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useRef } from 'react';
|
|
10
|
+
import useConversationStore, {
|
|
11
|
+
QuickReplyOption,
|
|
12
|
+
ConversationRoute,
|
|
13
|
+
ResultQuality,
|
|
14
|
+
} from 'stores/chat/conversationStore';
|
|
15
|
+
import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
|
|
16
|
+
import { chatAnalysis } from 'services/vizo';
|
|
17
|
+
import useRequestStore from 'stores/request/requestStore';
|
|
18
|
+
import { canvasToBase64, cropImage } from 'utils/cropImageToBase64';
|
|
19
|
+
import {
|
|
20
|
+
fetchProductImageBase64,
|
|
21
|
+
getProductImageUrl,
|
|
22
|
+
} from 'utils/fetchProductImage';
|
|
23
|
+
import { loadRelatedPartsTable, lookupRelatedParts } from 'utils/relatedParts';
|
|
24
|
+
|
|
25
|
+
interface P2EvaluationResult {
|
|
26
|
+
quality: ResultQuality;
|
|
27
|
+
route: ConversationRoute;
|
|
28
|
+
opening_message: string;
|
|
29
|
+
options: QuickReplyOption[];
|
|
30
|
+
keyword?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useResultEvaluator() {
|
|
34
|
+
const {
|
|
35
|
+
isEvaluated,
|
|
36
|
+
relatedPartsUsed,
|
|
37
|
+
setEvaluationResult,
|
|
38
|
+
setIsLoading,
|
|
39
|
+
setError,
|
|
40
|
+
setRelatedPartsTableSuggestions,
|
|
41
|
+
} = useConversationStore();
|
|
42
|
+
const { filters } = useSmartFiltersStore();
|
|
43
|
+
|
|
44
|
+
const evaluatingRef = useRef(false);
|
|
45
|
+
|
|
46
|
+
const evaluateResults = useCallback(
|
|
47
|
+
async ({
|
|
48
|
+
imageAnalysis,
|
|
49
|
+
activeProducts,
|
|
50
|
+
}: {
|
|
51
|
+
imageAnalysis: any;
|
|
52
|
+
activeProducts: any[];
|
|
53
|
+
}) => {
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
setRelatedPartsTableSuggestions([]);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Build context for P2
|
|
60
|
+
const specification = imageAnalysis?.specification || {};
|
|
61
|
+
const imageDescription =
|
|
62
|
+
imageAnalysis?.imageDescription || 'No description available';
|
|
63
|
+
|
|
64
|
+
// Get results (up to 30) with DETAILED metadata for finding differentiating attributes
|
|
65
|
+
|
|
66
|
+
const resultsSummary = activeProducts
|
|
67
|
+
.slice(0, 30)
|
|
68
|
+
.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
|
+
});
|
|
92
|
+
|
|
93
|
+
// Extract from title/description using common patterns
|
|
94
|
+
const title = p.title || p.name || '';
|
|
95
|
+
const titleDesc = `${title} ${p.description || ''}`;
|
|
96
|
+
|
|
97
|
+
// Extract brand from title - often first word(s) in CAPS or before asterisk
|
|
98
|
+
if (!metadata.brand && !metadata.manufacturer) {
|
|
99
|
+
// Pattern 1: First word in all caps (e.g., "MANDRINO*...")
|
|
100
|
+
const capsMatch = title.match(/^([A-Z][A-Z0-9]+)(?:\*|\s)/);
|
|
101
|
+
if (capsMatch) {
|
|
102
|
+
metadata.brand = capsMatch[1];
|
|
103
|
+
}
|
|
104
|
+
// Pattern 2: Text before asterisk (e.g., "MANDRINO*TOOL")
|
|
105
|
+
const asteriskMatch = title.match(/^([^*]+)\*/);
|
|
106
|
+
if (asteriskMatch && asteriskMatch[1].length <= 20) {
|
|
107
|
+
metadata.brand = asteriskMatch[1].trim();
|
|
108
|
+
}
|
|
109
|
+
// Pattern 3: Generic all-caps token in title/description
|
|
110
|
+
const tokenMatch = titleDesc.match(/\b([A-Z][A-Z0-9]{2,})\b/);
|
|
111
|
+
if (tokenMatch) {
|
|
112
|
+
metadata.brand = tokenMatch[1].toUpperCase();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Extract dimensions like "382 mm x 260 mm x 3 mm" or "344 x 221 x 3"
|
|
117
|
+
const dimensionMatch = titleDesc.match(
|
|
118
|
+
/(\d+)\s*(?:mm)?\s*[xX×]\s*(\d+)\s*(?:mm)?\s*(?:[xX×]\s*(\d+))?/i,
|
|
119
|
+
);
|
|
120
|
+
if (dimensionMatch && !metadata.dimensions) {
|
|
121
|
+
const dims = dimensionMatch[3]
|
|
122
|
+
? `${dimensionMatch[1]}x${dimensionMatch[2]}x${dimensionMatch[3]}mm`
|
|
123
|
+
: `${dimensionMatch[1]}x${dimensionMatch[2]}mm`;
|
|
124
|
+
metadata.dimensions = dims;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract voltage - multiple patterns for different formats
|
|
128
|
+
// Patterns: "24V DC", "12 V", "DC 24V", "24VDC", "AC 230V"
|
|
129
|
+
const voltagePatterns = [
|
|
130
|
+
/(\d+)\s*V\s*(DC|AC)?/i, // "24V DC", "12 V", "24V"
|
|
131
|
+
/(DC|AC)\s*(\d+)\s*V/i, // "DC 24V", "AC 230V"
|
|
132
|
+
/(\d+)\s*(VDC|VAC)/i, // "24VDC", "230VAC"
|
|
133
|
+
];
|
|
134
|
+
for (const pattern of voltagePatterns) {
|
|
135
|
+
const voltageMatch = titleDesc.match(pattern);
|
|
136
|
+
if (voltageMatch && !metadata.voltage) {
|
|
137
|
+
// Normalize to format like "24V" or "24V DC"
|
|
138
|
+
const fullMatch = voltageMatch[0]
|
|
139
|
+
.toUpperCase()
|
|
140
|
+
.replace(/\s+/g, ' ')
|
|
141
|
+
.trim();
|
|
142
|
+
// Extract just the number and V
|
|
143
|
+
const numMatch = fullMatch.match(/(\d+)/);
|
|
144
|
+
if (numMatch) {
|
|
145
|
+
const voltage = numMatch[1];
|
|
146
|
+
const dcAc = fullMatch.includes('DC')
|
|
147
|
+
? ' DC'
|
|
148
|
+
: fullMatch.includes('AC')
|
|
149
|
+
? ' AC'
|
|
150
|
+
: '';
|
|
151
|
+
metadata.voltage = `${voltage}V${dcAc}`;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Extract generic product type keywords from title/description
|
|
158
|
+
if (!metadata.product_type) {
|
|
159
|
+
const typeMatch = titleDesc.match(
|
|
160
|
+
/\b(bearing|ring|relay|sensor|motor|fan|contactor|valve|pump|faucet|thermostat|filter|housing)\b/i,
|
|
161
|
+
);
|
|
162
|
+
if (typeMatch) {
|
|
163
|
+
metadata.product_type = typeMatch[1].toUpperCase();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Extract material
|
|
168
|
+
const materialMatch = titleDesc.match(
|
|
169
|
+
/\b(steel|aluminum|aluminium|brass|copper|plastic|rubber|stainless)\b/i,
|
|
170
|
+
);
|
|
171
|
+
if (materialMatch && !metadata.material)
|
|
172
|
+
metadata.material = materialMatch[1].toLowerCase();
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
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,
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Build specification from P4 (smart filters)
|
|
185
|
+
const p4Specification: Record<string, string> = {};
|
|
186
|
+
filters.forEach(f => {
|
|
187
|
+
p4Specification[f.label.toLowerCase()] = f.value;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Add image analysis specification
|
|
191
|
+
Object.entries(specification).forEach(([key, value]) => {
|
|
192
|
+
if (value && typeof value === 'string') {
|
|
193
|
+
p4Specification[key] = value;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 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
|
+
});
|
|
209
|
+
|
|
210
|
+
const userPrompt = `Evaluate these search results:
|
|
211
|
+
|
|
212
|
+
IMAGE DESCRIPTION: ${imageDescription}
|
|
213
|
+
|
|
214
|
+
SPECIFICATION (from image analysis):
|
|
215
|
+
${JSON.stringify(p4Specification, null, 2)}
|
|
216
|
+
|
|
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
|
+
).join('\n')}
|
|
223
|
+
|
|
224
|
+
COMMON ATTRIBUTES FOUND IN RESULTS (for filter suggestions):
|
|
225
|
+
${JSON.stringify(attributeFrequency, null, 2)}
|
|
226
|
+
|
|
227
|
+
TOTAL RESULTS: ${resultsSummary.length}
|
|
228
|
+
|
|
229
|
+
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.`;
|
|
230
|
+
|
|
231
|
+
const productsForImages = activeProducts.slice(0, 30);
|
|
232
|
+
const imageResults = await Promise.all(
|
|
233
|
+
productsForImages.map(async (p: any, i: number) => {
|
|
234
|
+
const url = getProductImageUrl(p);
|
|
235
|
+
if (!url) return { rank: i + 1, base64: null };
|
|
236
|
+
|
|
237
|
+
const base64 = await fetchProductImageBase64(url);
|
|
238
|
+
|
|
239
|
+
return { rank: i + 1, base64 };
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const hasVisualContext =
|
|
244
|
+
imageResults.filter(r => r.base64 !== null).length >= 1;
|
|
245
|
+
|
|
246
|
+
const productImageParts: Array<{
|
|
247
|
+
metadata: string;
|
|
248
|
+
image?: string;
|
|
249
|
+
mimeType?: string;
|
|
250
|
+
}> = [];
|
|
251
|
+
|
|
252
|
+
resultsSummary.forEach((result, i) => {
|
|
253
|
+
const originalProduct = productsForImages[i];
|
|
254
|
+
const tags = originalProduct?._tags;
|
|
255
|
+
const tagStr = tags
|
|
256
|
+
? ` | TAGS: type=${tags.productType}, color=${tags.color}, shape=${tags.shape}, material=${tags.material}`
|
|
257
|
+
: '';
|
|
258
|
+
|
|
259
|
+
const metaLine = `${result.rank}. ${result.title}${result.description ? ` - ${result.description}` : ''} (Score: ${result.score})${result.metadata ? ` | Metadata: ${JSON.stringify(result.metadata)}` : ''}${tagStr}`;
|
|
260
|
+
|
|
261
|
+
if (hasVisualContext && imageResults[i]?.base64) {
|
|
262
|
+
productImageParts.push({
|
|
263
|
+
metadata: metaLine,
|
|
264
|
+
image: imageResults[i].base64 || '',
|
|
265
|
+
mimeType: 'image/jpeg',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const requestImage = useRequestStore.getState().requestImages;
|
|
271
|
+
const regions = useRequestStore.getState().regions;
|
|
272
|
+
const croppedCanvas = cropImage(requestImage[0], regions[0]);
|
|
273
|
+
const base64Image = canvasToBase64(croppedCanvas, 'image/jpeg', 1);
|
|
274
|
+
|
|
275
|
+
const response = await chatAnalysis({
|
|
276
|
+
promptKey: 'result_evaluator_prompt',
|
|
277
|
+
multimodalInputs: [
|
|
278
|
+
{
|
|
279
|
+
metadata: userPrompt,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
metadata: 'request_image',
|
|
283
|
+
mimeType: 'image/jpeg',
|
|
284
|
+
image: base64Image,
|
|
285
|
+
},
|
|
286
|
+
...productImageParts,
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const evaluation: P2EvaluationResult = response;
|
|
291
|
+
|
|
292
|
+
let mappedOptions = (evaluation?.options || []).map(opt => ({
|
|
293
|
+
label: opt.label,
|
|
294
|
+
action_type: opt.action_type as
|
|
295
|
+
| 'filter'
|
|
296
|
+
| 'clarify'
|
|
297
|
+
| 'text_search_prompt'
|
|
298
|
+
| 'find_related_parts',
|
|
299
|
+
key: (opt as any).key,
|
|
300
|
+
value: (opt as any).value,
|
|
301
|
+
}));
|
|
302
|
+
if (relatedPartsUsed) {
|
|
303
|
+
mappedOptions = mappedOptions.filter(
|
|
304
|
+
opt => opt.action_type !== 'find_related_parts',
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
mappedOptions.push({
|
|
308
|
+
label: "Describe what you're looking for",
|
|
309
|
+
action_type: 'text_search_prompt',
|
|
310
|
+
key: 'text_search',
|
|
311
|
+
value: 'text_search',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Check lookup table for related parts on fresh image searches and store globally.
|
|
315
|
+
const table = await loadRelatedPartsTable();
|
|
316
|
+
|
|
317
|
+
const tableSuggestions = lookupRelatedParts(
|
|
318
|
+
table,
|
|
319
|
+
imageAnalysis.specification.product_category || '',
|
|
320
|
+
);
|
|
321
|
+
const hasTableSuggestions =
|
|
322
|
+
!!tableSuggestions && tableSuggestions.length > 0;
|
|
323
|
+
setRelatedPartsTableSuggestions(tableSuggestions || []);
|
|
324
|
+
|
|
325
|
+
// GC Gruppe: offer "Find related parts" in initial P2 response
|
|
326
|
+
if (
|
|
327
|
+
window.settings.vizo?.findRelatedParts &&
|
|
328
|
+
hasTableSuggestions &&
|
|
329
|
+
!relatedPartsUsed
|
|
330
|
+
) {
|
|
331
|
+
mappedOptions.push({
|
|
332
|
+
label: `Find parts related to ${imageAnalysis.specification.product_category || 'request image'}`,
|
|
333
|
+
action_type: 'find_related_parts',
|
|
334
|
+
key: 'find_related',
|
|
335
|
+
value: 'find_related_parts',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Enforce ≤5 rule: if initial results are ≤5, strip filter options, keep only text_search_prompt
|
|
340
|
+
if (activeProducts.length <= 5 && activeProducts.length > 0) {
|
|
341
|
+
mappedOptions = [
|
|
342
|
+
{
|
|
343
|
+
label: "Describe what you're looking for",
|
|
344
|
+
action_type: 'text_search_prompt' as const,
|
|
345
|
+
key: 'text_search',
|
|
346
|
+
value: 'text_search',
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
if (
|
|
351
|
+
window.settings.vizo?.findRelatedParts &&
|
|
352
|
+
hasTableSuggestions &&
|
|
353
|
+
!relatedPartsUsed
|
|
354
|
+
) {
|
|
355
|
+
mappedOptions.push({
|
|
356
|
+
label: `Find parts related to ${imageAnalysis.specification.product_category || 'request image'}`,
|
|
357
|
+
action_type: 'find_related_parts',
|
|
358
|
+
key: 'find_related',
|
|
359
|
+
value: 'find_related_parts',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
setEvaluationResult(
|
|
365
|
+
evaluation.quality,
|
|
366
|
+
evaluation.route,
|
|
367
|
+
evaluation.opening_message,
|
|
368
|
+
mappedOptions,
|
|
369
|
+
evaluation.keyword,
|
|
370
|
+
);
|
|
371
|
+
} catch (err: any) {
|
|
372
|
+
console.error('result_evaluator_prompt', err);
|
|
373
|
+
setRelatedPartsTableSuggestions([]);
|
|
374
|
+
setError(err.message || 'Failed to evaluate results');
|
|
375
|
+
|
|
376
|
+
// Fallback to good results on error
|
|
377
|
+
setEvaluationResult(
|
|
378
|
+
'good',
|
|
379
|
+
'P3',
|
|
380
|
+
"I found some results for your image. How can I help you find what you're looking for?",
|
|
381
|
+
[
|
|
382
|
+
{ label: 'Show me options', action_type: 'filter' as const },
|
|
383
|
+
{
|
|
384
|
+
label: "Describe what you're looking for",
|
|
385
|
+
action_type: 'text_search_prompt' as const,
|
|
386
|
+
key: 'text_search',
|
|
387
|
+
value: 'text_search',
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
);
|
|
391
|
+
} finally {
|
|
392
|
+
setIsLoading(false);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
396
|
+
[
|
|
397
|
+
setIsLoading,
|
|
398
|
+
setError,
|
|
399
|
+
setRelatedPartsTableSuggestions,
|
|
400
|
+
filters,
|
|
401
|
+
relatedPartsUsed,
|
|
402
|
+
setEvaluationResult,
|
|
403
|
+
],
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Reset the guard ref when a new session starts (isEvaluated goes back to false)
|
|
407
|
+
if (!isEvaluated) {
|
|
408
|
+
evaluatingRef.current = false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
evaluateResults,
|
|
413
|
+
isEvaluated,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export default useResultEvaluator;
|
package/src/index.css
CHANGED
package/src/index.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect } from 'react';
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo } from 'react';
|
|
2
2
|
import { Outlet, useLocation } from 'react-router';
|
|
3
3
|
|
|
4
4
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
@@ -18,6 +18,7 @@ import useRequestStore from 'stores/request/requestStore';
|
|
|
18
18
|
import useResultStore from 'stores/result/resultStore';
|
|
19
19
|
import useUiStore from 'stores/ui/uiStore';
|
|
20
20
|
import { Toaster } from 'components/Toaster';
|
|
21
|
+
import { getFilters } from 'services/filter';
|
|
21
22
|
|
|
22
23
|
i18n.use(initReactI18next).init({
|
|
23
24
|
resources: translations,
|
|
@@ -92,12 +93,62 @@ function AppLayout(): JSX.Element {
|
|
|
92
93
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
94
|
}, [status]);
|
|
94
95
|
|
|
96
|
+
const user = useAuth0().user;
|
|
97
|
+
|
|
98
|
+
const setPreFilterList = useRequestStore(state => state.setPreFilterList);
|
|
99
|
+
const setPreFilterLoading = useRequestStore(
|
|
100
|
+
state => state.setPreFilterLoading,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const settings = window.settings;
|
|
104
|
+
const setMetaFilter = useRequestStore(state => state.setMetaFilter);
|
|
105
|
+
|
|
106
|
+
const showPreFilter = useMemo(() => {
|
|
107
|
+
if (settings.shouldUseUserMetadata && user) {
|
|
108
|
+
if (user['/user_metadata'].value) {
|
|
109
|
+
setMetaFilter(user['/user_metadata'].value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (settings.shouldUseUserMetadata && user) {
|
|
114
|
+
if (settings.preFilterOption && !user['/user_metadata'].value) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return settings.preFilterOption;
|
|
121
|
+
}, [
|
|
122
|
+
setMetaFilter,
|
|
123
|
+
settings.preFilterOption,
|
|
124
|
+
settings.shouldUseUserMetadata,
|
|
125
|
+
user,
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const getPreFilters = useCallback(async () => {
|
|
129
|
+
setPreFilterLoading(true);
|
|
130
|
+
try {
|
|
131
|
+
const res = await getFilters(1000, settings);
|
|
132
|
+
setPreFilterList(res);
|
|
133
|
+
} catch (e: any) {
|
|
134
|
+
console.log('err getDataFilterDesktop', e);
|
|
135
|
+
} finally {
|
|
136
|
+
setPreFilterLoading(false);
|
|
137
|
+
}
|
|
138
|
+
}, [settings, setPreFilterList, setPreFilterLoading]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (showPreFilter) {
|
|
142
|
+
getPreFilters();
|
|
143
|
+
}
|
|
144
|
+
}, [getPreFilters, showPreFilter]);
|
|
145
|
+
|
|
95
146
|
if (!showLayout) {
|
|
96
147
|
return <Outlet />;
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
return (
|
|
100
|
-
<div className="full-height flex flex-col ">
|
|
151
|
+
<div className="full-height flex flex-col bg-[#F3F4F8]">
|
|
101
152
|
<div>
|
|
102
153
|
<Toaster />
|
|
103
154
|
</div>
|
package/src/pages/Home.tsx
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useAuth0 } from '@auth0/auth0-react';
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
3
2
|
import { twMerge } from 'tailwind-merge';
|
|
4
3
|
|
|
5
4
|
import DragDropFile from 'components/DragDropFile';
|
|
@@ -12,11 +11,10 @@ import LocationInfoPopup from '../components/LocationInfoPopup';
|
|
|
12
11
|
import Hint from '../components/Hint';
|
|
13
12
|
import Loading from '../components/Loading';
|
|
14
13
|
import useRequestStore from '../stores/request/requestStore';
|
|
15
|
-
import
|
|
14
|
+
import ImageUpload from 'components/ImageUpload';
|
|
16
15
|
|
|
17
16
|
function Home() {
|
|
18
17
|
const settings = window.settings;
|
|
19
|
-
const user = useAuth0().user;
|
|
20
18
|
const { experienceVisualSearch, experienceVisualSearchImages, geoLocation } =
|
|
21
19
|
settings;
|
|
22
20
|
const navigate = useNavigate();
|
|
@@ -25,33 +23,6 @@ function Home() {
|
|
|
25
23
|
useState<Blob[]>([]);
|
|
26
24
|
const [isOpenModalCamera, setOpenModalCamera] = useState<boolean>(false);
|
|
27
25
|
const showLoading = useRequestStore(store => store.showLoading);
|
|
28
|
-
const setPreFilterList = useRequestStore(state => state.setPreFilterList);
|
|
29
|
-
const setPreFilterLoading = useRequestStore(
|
|
30
|
-
state => state.setPreFilterLoading,
|
|
31
|
-
);
|
|
32
|
-
const setMetaFilter = useRequestStore(state => state.setMetaFilter);
|
|
33
|
-
|
|
34
|
-
const showPreFilter = useMemo(() => {
|
|
35
|
-
if (settings.shouldUseUserMetadata && user) {
|
|
36
|
-
if (user['/user_metadata'].value) {
|
|
37
|
-
setMetaFilter(user['/user_metadata'].value);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (settings.shouldUseUserMetadata && user) {
|
|
42
|
-
if (settings.preFilterOption && !user['/user_metadata'].value) {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return settings.preFilterOption;
|
|
49
|
-
}, [
|
|
50
|
-
setMetaFilter,
|
|
51
|
-
settings.preFilterOption,
|
|
52
|
-
settings.shouldUseUserMetadata,
|
|
53
|
-
user,
|
|
54
|
-
]);
|
|
55
26
|
|
|
56
27
|
const fetchImage = async (url: string) => {
|
|
57
28
|
const response = await fetch(url, { cache: 'force-cache' });
|
|
@@ -98,24 +69,6 @@ function Home() {
|
|
|
98
69
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
99
70
|
}, []);
|
|
100
71
|
|
|
101
|
-
const getPreFilters = useCallback(async () => {
|
|
102
|
-
setPreFilterLoading(true);
|
|
103
|
-
try {
|
|
104
|
-
const res = await getFilters(1000, settings);
|
|
105
|
-
setPreFilterList(res);
|
|
106
|
-
} catch (e: any) {
|
|
107
|
-
console.log('err getDataFilterDesktop', e);
|
|
108
|
-
} finally {
|
|
109
|
-
setPreFilterLoading(false);
|
|
110
|
-
}
|
|
111
|
-
}, [settings, setPreFilterList, setPreFilterLoading]);
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (showPreFilter) {
|
|
115
|
-
getPreFilters();
|
|
116
|
-
}
|
|
117
|
-
}, [getPreFilters, showPreFilter]);
|
|
118
|
-
|
|
119
72
|
return (
|
|
120
73
|
<>
|
|
121
74
|
{showLoading && (
|
|
@@ -149,8 +102,15 @@ function Home() {
|
|
|
149
102
|
</div>
|
|
150
103
|
)}
|
|
151
104
|
|
|
152
|
-
<TextSearch />
|
|
153
|
-
|
|
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 />
|
|
113
|
+
</div>
|
|
154
114
|
</div>
|
|
155
115
|
<div className="max-w-[532px] relative w-full">
|
|
156
116
|
<DragDropFile />
|
|
@@ -190,7 +150,6 @@ function Home() {
|
|
|
190
150
|
|
|
191
151
|
<div className="flex desktop:hidden w-full fixed bottom-12 px-2 gap-2">
|
|
192
152
|
<TextSearch className="flex md:hidden w-full " />
|
|
193
|
-
{/* <ImageUpload /> */}
|
|
194
153
|
</div>
|
|
195
154
|
</div>
|
|
196
155
|
</>
|
package/src/pages/Login.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
2
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
3
3
|
|
|
4
4
|
const Login = () => {
|
|
5
5
|
const { user, isAuthenticated, isLoading, loginWithRedirect } = useAuth0();
|
|
6
6
|
const { auth0 } = window.settings;
|
|
7
|
-
console.log({ auth0 });
|
|
8
7
|
|
|
9
8
|
useEffect(() => {
|
|
10
9
|
if (!isLoading && !user && !isAuthenticated && auth0.enabled) {
|
package/src/pages/Logout.tsx
CHANGED