@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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P5 - Bad Results Recovery
|
|
3
|
+
*
|
|
4
|
+
* Handles conversation when results don't match the uploaded image.
|
|
5
|
+
* Asks clarifying questions based on failure mode.
|
|
6
|
+
* Digs through all 100 results to resurface better matches.
|
|
7
|
+
* Maximum 3 attempts before suggesting a new image.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useRef } from 'react';
|
|
11
|
+
import useConversationStore, {
|
|
12
|
+
QuickReplyOption,
|
|
13
|
+
} from 'stores/chat/conversationStore';
|
|
14
|
+
import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
|
|
15
|
+
import useResultStore from 'stores/result/resultStore';
|
|
16
|
+
import { chatAnalysis } from 'services/vizo';
|
|
17
|
+
import {
|
|
18
|
+
getProductImageUrl,
|
|
19
|
+
fetchProductImageBase64,
|
|
20
|
+
} from 'utils/fetchProductImage';
|
|
21
|
+
|
|
22
|
+
interface P5Response {
|
|
23
|
+
message: string;
|
|
24
|
+
action: 'ask' | 'resurface' | 'reset' | 'exit';
|
|
25
|
+
filtered_results?: number[];
|
|
26
|
+
options: QuickReplyOption[];
|
|
27
|
+
attempt: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useBadResultsRecovery() {
|
|
31
|
+
const {
|
|
32
|
+
messages,
|
|
33
|
+
failureMode,
|
|
34
|
+
attemptCounter,
|
|
35
|
+
addMessage,
|
|
36
|
+
incrementAttempt,
|
|
37
|
+
setIsLoading,
|
|
38
|
+
setError,
|
|
39
|
+
} = useConversationStore();
|
|
40
|
+
|
|
41
|
+
const { filters } = useSmartFiltersStore();
|
|
42
|
+
|
|
43
|
+
const productsFromFindApi = useResultStore(
|
|
44
|
+
state => state.productsFromFindApi,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const setFindApiProducts = useResultStore(state => state.setFindApiProducts);
|
|
48
|
+
const imageAnalysis = useResultStore(state => state.imageAnalysis);
|
|
49
|
+
|
|
50
|
+
const imageCacheRef = useRef<Map<string, string | null>>(new Map());
|
|
51
|
+
|
|
52
|
+
const activeProducts = productsFromFindApi;
|
|
53
|
+
const setActiveProducts = setFindApiProducts;
|
|
54
|
+
|
|
55
|
+
const sendMessage = useCallback(
|
|
56
|
+
async (userMessage: string) => {
|
|
57
|
+
// Add user message to chat
|
|
58
|
+
addMessage({
|
|
59
|
+
role: 'user',
|
|
60
|
+
content: userMessage,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Check if we've exceeded max attempts
|
|
64
|
+
if (attemptCounter >= 3) {
|
|
65
|
+
addMessage({
|
|
66
|
+
role: 'assistant',
|
|
67
|
+
content:
|
|
68
|
+
"I wasn't able to find a match in the current results. Try uploading a clearer image or from a different angle.",
|
|
69
|
+
action: { type: 'exit' },
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setIsLoading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Build context for P5
|
|
79
|
+
const specification = imageAnalysis?.specification || {};
|
|
80
|
+
const imageDescription =
|
|
81
|
+
imageAnalysis?.imageDescription || 'No description available';
|
|
82
|
+
|
|
83
|
+
// Get P4 specification from smart filters
|
|
84
|
+
const p4Specification: Record<string, string> = {};
|
|
85
|
+
filters.forEach(f => {
|
|
86
|
+
p4Specification[f.label.toLowerCase()] = f.value;
|
|
87
|
+
});
|
|
88
|
+
Object.entries(specification).forEach(([key, value]) => {
|
|
89
|
+
if (value && typeof value === 'string') {
|
|
90
|
+
p4Specification[key] = value;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Get all 100 results with detailed metadata
|
|
95
|
+
// ALWAYS use firstAlgoliaProducts (original) so indices are consistent
|
|
96
|
+
// This allows "show original results" and filtering from original to work correctly
|
|
97
|
+
const originalProducts = activeProducts;
|
|
98
|
+
const allResults = originalProducts
|
|
99
|
+
.slice(0, 100)
|
|
100
|
+
.map((p: any, i: number) => {
|
|
101
|
+
// Extract all potentially useful metadata
|
|
102
|
+
const metadata: Record<string, any> = {};
|
|
103
|
+
const metadataFields = [
|
|
104
|
+
'voltage',
|
|
105
|
+
'size',
|
|
106
|
+
'brand',
|
|
107
|
+
'manufacturer',
|
|
108
|
+
'material',
|
|
109
|
+
'type',
|
|
110
|
+
'category',
|
|
111
|
+
'color',
|
|
112
|
+
'dimensions',
|
|
113
|
+
'power',
|
|
114
|
+
'current',
|
|
115
|
+
'connector',
|
|
116
|
+
'model',
|
|
117
|
+
'series',
|
|
118
|
+
'product_line',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
metadataFields.forEach(field => {
|
|
122
|
+
if (p[field]) metadata[field] = p[field];
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Extract from title/description using common patterns
|
|
126
|
+
const title = p.title || p.name || '';
|
|
127
|
+
const titleDesc = `${title} ${p.description || ''}`;
|
|
128
|
+
|
|
129
|
+
// Extract brand from title - often first word(s) in CAPS or before asterisk
|
|
130
|
+
if (!metadata.brand && !metadata.manufacturer) {
|
|
131
|
+
const capsMatch = title.match(/^([A-Z][A-Z0-9]+)(?:\*|\s)/);
|
|
132
|
+
if (capsMatch) metadata.brand = capsMatch[1];
|
|
133
|
+
const asteriskMatch = title.match(/^([^*]+)\*/);
|
|
134
|
+
if (asteriskMatch && asteriskMatch[1].length <= 20)
|
|
135
|
+
metadata.brand = asteriskMatch[1].trim();
|
|
136
|
+
const tokenMatch = titleDesc.match(/\b([A-Z][A-Z0-9]{2,})\b/);
|
|
137
|
+
if (tokenMatch) metadata.brand = tokenMatch[1].toUpperCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract dimensions like "382 mm x 260 mm x 3 mm" or "344 x 221 x 3"
|
|
141
|
+
const dimensionMatch = titleDesc.match(
|
|
142
|
+
/(\d+)\s*(?:mm)?\s*[xX×]\s*(\d+)\s*(?:mm)?\s*(?:[xX×]\s*(\d+))?/i,
|
|
143
|
+
);
|
|
144
|
+
if (dimensionMatch && !metadata.dimensions) {
|
|
145
|
+
const dims = dimensionMatch[3]
|
|
146
|
+
? `${dimensionMatch[1]}x${dimensionMatch[2]}x${dimensionMatch[3]}mm`
|
|
147
|
+
: `${dimensionMatch[1]}x${dimensionMatch[2]}mm`;
|
|
148
|
+
metadata.dimensions = dims;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Extract voltage
|
|
152
|
+
const voltageMatch = titleDesc.match(/(\d+)\s*V\s*(DC|AC)?/i);
|
|
153
|
+
if (voltageMatch && !metadata.voltage)
|
|
154
|
+
metadata.voltage = voltageMatch[0];
|
|
155
|
+
|
|
156
|
+
// Extract generic product type keywords from title/description
|
|
157
|
+
if (!metadata.product_type) {
|
|
158
|
+
const typeMatch = titleDesc.match(
|
|
159
|
+
/\b(bearing|ring|relay|sensor|motor|fan|contactor|valve|pump|faucet|thermostat|filter|housing)\b/i,
|
|
160
|
+
);
|
|
161
|
+
if (typeMatch) {
|
|
162
|
+
metadata.product_type = typeMatch[1].toUpperCase();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract material
|
|
167
|
+
const materialMatch = titleDesc.match(
|
|
168
|
+
/\b(steel|aluminum|aluminium|brass|copper|plastic|rubber|stainless)\b/i,
|
|
169
|
+
);
|
|
170
|
+
if (materialMatch && !metadata.material)
|
|
171
|
+
metadata.material = materialMatch[1].toLowerCase();
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
index: i,
|
|
175
|
+
sku: p.sku || p.objectID || `product-${i}`,
|
|
176
|
+
title: title || 'Untitled',
|
|
177
|
+
description: (p.description || '').substring(0, 100),
|
|
178
|
+
category: p.category || p.type || '',
|
|
179
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Analyze common attributes across results for smart question options
|
|
184
|
+
const attributeFrequency: Record<string, Record<string, number>> = {};
|
|
185
|
+
allResults.forEach((r: any) => {
|
|
186
|
+
if (r.metadata) {
|
|
187
|
+
Object.entries(r.metadata).forEach(([key, value]) => {
|
|
188
|
+
if (!attributeFrequency[key]) attributeFrequency[key] = {};
|
|
189
|
+
const strValue = String(value);
|
|
190
|
+
attributeFrequency[key][strValue] =
|
|
191
|
+
(attributeFrequency[key][strValue] || 0) + 1;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Build conversation history
|
|
197
|
+
const conversationHistory = messages.map(m => ({
|
|
198
|
+
role: m.role,
|
|
199
|
+
content: m.content,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Fetch product thumbnails for first 30 results (same limit as P3)
|
|
203
|
+
const productsForImages = originalProducts.slice(0, 30);
|
|
204
|
+
const imageResults = await Promise.all(
|
|
205
|
+
productsForImages.map(async (p: any, i: number) => {
|
|
206
|
+
const url = getProductImageUrl(p);
|
|
207
|
+
if (!url) return { index: i, base64: null };
|
|
208
|
+
|
|
209
|
+
if (imageCacheRef.current.has(url)) {
|
|
210
|
+
return {
|
|
211
|
+
index: i,
|
|
212
|
+
base64: imageCacheRef.current.get(url) ?? null,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const base64 = await fetchProductImageBase64(url);
|
|
217
|
+
imageCacheRef.current.set(url, base64);
|
|
218
|
+
return { index: i, base64 };
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const imagesAvailable = imageResults.filter(
|
|
223
|
+
r => r.base64 !== null,
|
|
224
|
+
).length;
|
|
225
|
+
const hasVisualContext = imagesAvailable >= 3;
|
|
226
|
+
|
|
227
|
+
const visualNotice = hasVisualContext
|
|
228
|
+
? `PRODUCT IMAGES: ${imagesAvailable} result thumbnails are included below (first 30 results). Use them to visually compare against the uploaded image when resurfacing better matches.`
|
|
229
|
+
: `PRODUCT IMAGES: NOT AVAILABLE — thumbnails could not be loaded. Use text metadata only for filtering.`;
|
|
230
|
+
|
|
231
|
+
// Build multimodal parts
|
|
232
|
+
const multimodalParts: any[] = [];
|
|
233
|
+
|
|
234
|
+
// Part 1: Text context
|
|
235
|
+
multimodalParts.push({
|
|
236
|
+
metadata: `CONTEXT:
|
|
237
|
+
|
|
238
|
+
IMAGE DESCRIPTION: ${imageDescription}
|
|
239
|
+
|
|
240
|
+
SPECIFICATION (from image analysis):
|
|
241
|
+
${JSON.stringify(p4Specification, null, 2)}
|
|
242
|
+
|
|
243
|
+
FAILURE MODE: ${failureMode || 'unknown'}
|
|
244
|
+
|
|
245
|
+
COMMON ATTRIBUTES FOUND IN RESULTS (use these for question options - ask about varied attributes, NOT the obvious category):
|
|
246
|
+
${JSON.stringify(attributeFrequency, null, 2)}
|
|
247
|
+
|
|
248
|
+
CURRENT ATTEMPT: ${attemptCounter} of 3
|
|
249
|
+
|
|
250
|
+
${visualNotice}
|
|
251
|
+
|
|
252
|
+
PRODUCT LIST (first 30 with thumbnails where available, indices 0-based for filtered_results):`,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Part 2: First 30 results interleaved with thumbnails
|
|
256
|
+
allResults
|
|
257
|
+
.slice(0, 30)
|
|
258
|
+
.forEach((r: (typeof allResults)[number], i: number) => {
|
|
259
|
+
const metaLine = `index: ${r.index} ${r.sku} | ${r.title}${r.metadata ? ` | ${JSON.stringify(r.metadata)}` : ''}`;
|
|
260
|
+
|
|
261
|
+
if (hasVisualContext && imageResults[i]?.base64) {
|
|
262
|
+
multimodalParts.push({
|
|
263
|
+
metadata: metaLine,
|
|
264
|
+
image: imageResults[i].base64,
|
|
265
|
+
mimeType: 'image/jpeg',
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
multimodalParts.push({ metadata: metaLine });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Part 3: Remaining results (31–100) as text only
|
|
273
|
+
if (allResults.length > 30) {
|
|
274
|
+
multimodalParts.push({
|
|
275
|
+
metadata: `REMAINING RESULTS (indices 30–${allResults.length - 1}, text only):
|
|
276
|
+
${allResults
|
|
277
|
+
.slice(30)
|
|
278
|
+
.map(
|
|
279
|
+
(r: (typeof allResults)[number]) =>
|
|
280
|
+
`index: ${r.index} | sku: ${r.sku} | ${r.title}${r.metadata ? ` | ${JSON.stringify(r.metadata)}` : ''}`,
|
|
281
|
+
)
|
|
282
|
+
.join('\n')}`,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Part 4: Conversation + user message
|
|
287
|
+
multimodalParts.push({
|
|
288
|
+
metadata: `CONVERSATION HISTORY:
|
|
289
|
+
${conversationHistory.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n')}
|
|
290
|
+
|
|
291
|
+
USER MESSAGE: ${userMessage}
|
|
292
|
+
|
|
293
|
+
Based on the user's answer and the failure mode strategy, decide whether to ask another question, resurface better matches, or exit.`,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const response = await chatAnalysis({
|
|
297
|
+
promptKey: 'recovery_prompt',
|
|
298
|
+
multimodalInputs: multimodalParts,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const responseText = response;
|
|
302
|
+
|
|
303
|
+
// Parse the JSON response
|
|
304
|
+
const p5Response: P5Response = responseText;
|
|
305
|
+
|
|
306
|
+
// Execute the action
|
|
307
|
+
// ALWAYS use firstAlgoliaProducts for index lookup (matches what we sent to LLM)
|
|
308
|
+
const sourceProducts = activeProducts;
|
|
309
|
+
let options = p5Response.options || [];
|
|
310
|
+
switch (p5Response.action) {
|
|
311
|
+
case 'resurface':
|
|
312
|
+
// FILTER products to only show matching ones (not just reorder)
|
|
313
|
+
if (
|
|
314
|
+
p5Response.filtered_results &&
|
|
315
|
+
p5Response.filtered_results.length > 0
|
|
316
|
+
) {
|
|
317
|
+
const indices = p5Response.filtered_results;
|
|
318
|
+
const filteredProducts: any[] = [];
|
|
319
|
+
const usedIndices = new Set<number>();
|
|
320
|
+
|
|
321
|
+
// Only add products that match the filter criteria
|
|
322
|
+
for (const idx of indices) {
|
|
323
|
+
if (
|
|
324
|
+
idx >= 0 &&
|
|
325
|
+
idx < sourceProducts.length &&
|
|
326
|
+
!usedIndices.has(idx)
|
|
327
|
+
) {
|
|
328
|
+
filteredProducts.push(sourceProducts[idx]);
|
|
329
|
+
usedIndices.add(idx);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Only show filtered products, not all products
|
|
334
|
+
if (filteredProducts.length > 0) {
|
|
335
|
+
setActiveProducts(filteredProducts);
|
|
336
|
+
|
|
337
|
+
options = [
|
|
338
|
+
...options,
|
|
339
|
+
{
|
|
340
|
+
label: 'Go back to original results',
|
|
341
|
+
action_type: 'restore_original' as const,
|
|
342
|
+
key: 'restore',
|
|
343
|
+
value: 'restore',
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
case 'ask':
|
|
350
|
+
incrementAttempt();
|
|
351
|
+
break;
|
|
352
|
+
case 'reset':
|
|
353
|
+
if (activeProducts.length > 0) {
|
|
354
|
+
setActiveProducts(activeProducts);
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
case 'exit':
|
|
358
|
+
// No action needed, just show exit message
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Add assistant message
|
|
363
|
+
addMessage({
|
|
364
|
+
role: 'assistant',
|
|
365
|
+
content: p5Response.message,
|
|
366
|
+
action: { type: p5Response.action },
|
|
367
|
+
options: options,
|
|
368
|
+
});
|
|
369
|
+
} catch (err: any) {
|
|
370
|
+
console.error('[P5] Error:', err);
|
|
371
|
+
setError(err.message || 'Failed to process message');
|
|
372
|
+
addMessage({
|
|
373
|
+
role: 'assistant',
|
|
374
|
+
content:
|
|
375
|
+
'Oops, something went wrong on my end. Would you like to try with a different image?',
|
|
376
|
+
options: [{ label: 'Upload new image', action_type: 'clarify' }],
|
|
377
|
+
});
|
|
378
|
+
} finally {
|
|
379
|
+
setIsLoading(false);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
383
|
+
[
|
|
384
|
+
messages,
|
|
385
|
+
failureMode,
|
|
386
|
+
attemptCounter,
|
|
387
|
+
filters,
|
|
388
|
+
activeProducts,
|
|
389
|
+
|
|
390
|
+
imageAnalysis,
|
|
391
|
+
addMessage,
|
|
392
|
+
incrementAttempt,
|
|
393
|
+
setActiveProducts,
|
|
394
|
+
setIsLoading,
|
|
395
|
+
setError,
|
|
396
|
+
],
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
sendMessage,
|
|
401
|
+
messages,
|
|
402
|
+
failureMode,
|
|
403
|
+
attemptCounter,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export default useBadResultsRecovery;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import useRequestStore from 'stores/request/requestStore';
|
|
3
|
+
import useResultStore from 'stores/result/resultStore';
|
|
4
|
+
import { filterProductsByText } from 'utils/textSearchFilter';
|
|
5
|
+
|
|
6
|
+
export function useEffectiveGroundingResults() {
|
|
7
|
+
const productsFromFindApi = useResultStore(
|
|
8
|
+
state => state.productsFromFindApi,
|
|
9
|
+
);
|
|
10
|
+
const groundingFilterResult = useResultStore(
|
|
11
|
+
state => state.groundingFilterResult,
|
|
12
|
+
);
|
|
13
|
+
const query = useRequestStore(state => state.query);
|
|
14
|
+
const requestImages = useRequestStore(state => state.requestImages);
|
|
15
|
+
const postFilterSelections = useRequestStore(
|
|
16
|
+
state => state.postFilterSelections,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const effectiveGroundingResults = useMemo(() => {
|
|
20
|
+
let filteredProducts = productsFromFindApi;
|
|
21
|
+
|
|
22
|
+
if (requestImages?.length && query?.trim()) {
|
|
23
|
+
filteredProducts = filterProductsByText(query, productsFromFindApi);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const postEntries = Object.entries(postFilterSelections || {}).filter(
|
|
27
|
+
([, values]) => values?.length,
|
|
28
|
+
);
|
|
29
|
+
if (postEntries.length > 0) {
|
|
30
|
+
filteredProducts = filteredProducts.filter((product: any) => {
|
|
31
|
+
const productFilters = product?.filters || {};
|
|
32
|
+
return postEntries.every(([attribute, selectedValues]) => {
|
|
33
|
+
const productValues = productFilters?.[attribute];
|
|
34
|
+
if (!Array.isArray(productValues)) return false;
|
|
35
|
+
return selectedValues.some((value: string) =>
|
|
36
|
+
productValues.includes(value),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return groundingFilterResult.filter((result: any) =>
|
|
43
|
+
filteredProducts.some((product: any) => product.sku === result.sku),
|
|
44
|
+
);
|
|
45
|
+
}, [
|
|
46
|
+
productsFromFindApi,
|
|
47
|
+
groundingFilterResult,
|
|
48
|
+
query,
|
|
49
|
+
requestImages,
|
|
50
|
+
postFilterSelections,
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
return effectiveGroundingResults;
|
|
54
|
+
}
|