@nyris/nyris-webapp 0.3.99 → 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.
- package/build/asset-manifest.json +6 -6
- package/build/index.html +1 -1
- package/build/js/settings.example.js +3 -1
- package/build/static/css/main.a7ff7a7f.css +4 -0
- package/build/static/css/main.a7ff7a7f.css.map +1 -0
- package/build/static/js/main.916c157c.js +3 -0
- package/build/static/js/{main.2339271b.js.map → main.916c157c.js.map} +1 -1
- package/package.json +3 -3
- package/public/js/settings.example.js +3 -1
- package/src/components/ChatAssistant/ChatAssistant.test.tsx +213 -0
- package/src/components/ChatAssistant/ChatAssistant.tsx +18 -1
- package/src/components/ChatAssistant/MobileChatAssistant.tsx +18 -1
- package/src/components/ChatAssistant/OptionChip.tsx +23 -1
- package/src/components/ChatAssistant/useChatAssistantLogic.ts +35 -14
- package/src/components/Inquiry/InquiryModal.tsx +58 -26
- package/src/hooks/chatProductMetadata.test.ts +307 -0
- package/src/hooks/useBadResultsRecovery.ts +26 -49
- package/src/hooks/useGoodResultsChat.ts +20 -17
- package/src/hooks/useResultEvaluator.ts +21 -55
- package/src/services/Feedback.ts +5 -2
- package/src/stores/chat/conversationStore.test.ts +133 -0
- package/src/stores/chat/conversationStore.ts +55 -14
- package/src/types.ts +1 -0
- package/src/utils/chatProductMetadata.test.ts +130 -0
- package/src/utils/chatProductMetadata.ts +93 -0
- package/src/utils/compressImage.ts +3 -3
- package/build/static/css/main.5cb72c09.css +0 -4
- package/build/static/css/main.5cb72c09.css.map +0 -1
- package/build/static/js/main.2339271b.js +0 -3
- /package/build/static/js/{main.2339271b.js.LICENSE.txt → main.916c157c.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { chatAnalysis } from 'services/vizo';
|
|
3
|
+
import useConversationStore from 'stores/chat/conversationStore';
|
|
4
|
+
import { compressImage } from 'utils/compressImage';
|
|
5
|
+
import {
|
|
6
|
+
fetchProductImageBase64,
|
|
7
|
+
getProductImageUrl,
|
|
8
|
+
} from 'utils/fetchProductImage';
|
|
9
|
+
import { loadRelatedPartsTable } from 'utils/relatedParts';
|
|
10
|
+
import { useChatAssistantLogic } from 'components/ChatAssistant/useChatAssistantLogic';
|
|
11
|
+
import { useBadResultsRecovery } from './useBadResultsRecovery';
|
|
12
|
+
import { useGoodResultsChat } from './useGoodResultsChat';
|
|
13
|
+
import { useResultEvaluator } from './useResultEvaluator';
|
|
14
|
+
|
|
15
|
+
const mockProduct = {
|
|
16
|
+
sku: 'pump-1',
|
|
17
|
+
title: 'Pump',
|
|
18
|
+
description: 'Complete technical specification. '.repeat(20),
|
|
19
|
+
customIds: { mpn: 'MPN-1', gtin: '123456' },
|
|
20
|
+
attributes: { pressure: 0, available: false, material: 'steel' },
|
|
21
|
+
metadata: { customerAttribute: 'custom-value' },
|
|
22
|
+
categories: ['Industrial', 'Pumps'],
|
|
23
|
+
_tags: { productType: 'pump', color: 'blue', shape: 'round' },
|
|
24
|
+
url: 'https://example.com/product',
|
|
25
|
+
imageUrl: 'https://example.com/image.jpg',
|
|
26
|
+
images: [{ url: 'https://example.com/image.jpg', width: 300 }],
|
|
27
|
+
};
|
|
28
|
+
const mockResultState = {
|
|
29
|
+
productsFromFindApi: [mockProduct],
|
|
30
|
+
productsFromAlgolia: [] as typeof mockProduct[],
|
|
31
|
+
firstSearchResults: [mockProduct],
|
|
32
|
+
fullResultPool: [mockProduct],
|
|
33
|
+
imageAnalysis: {},
|
|
34
|
+
setFindApiProducts: jest.fn(),
|
|
35
|
+
setFullResultPool: jest.fn(),
|
|
36
|
+
setImageAnalysis: jest.fn(),
|
|
37
|
+
};
|
|
38
|
+
const mockRequestState = { requestImages: [], regions: [] };
|
|
39
|
+
|
|
40
|
+
jest.mock('stores/result/resultStore', () => ({
|
|
41
|
+
__esModule: true,
|
|
42
|
+
default: Object.assign(
|
|
43
|
+
(selector: (state: typeof mockResultState) => unknown) =>
|
|
44
|
+
selector(mockResultState),
|
|
45
|
+
{ getState: () => mockResultState },
|
|
46
|
+
),
|
|
47
|
+
}));
|
|
48
|
+
jest.mock('stores/request/requestStore', () => ({
|
|
49
|
+
__esModule: true,
|
|
50
|
+
default: Object.assign(
|
|
51
|
+
(selector: (state: typeof mockRequestState) => unknown) =>
|
|
52
|
+
selector(mockRequestState),
|
|
53
|
+
{ getState: () => mockRequestState },
|
|
54
|
+
),
|
|
55
|
+
}));
|
|
56
|
+
jest.mock('stores/smartFilters/smartFiltersStore', () => ({
|
|
57
|
+
__esModule: true,
|
|
58
|
+
default: () => ({ filters: [] }),
|
|
59
|
+
}));
|
|
60
|
+
jest.mock('services/vizo', () => ({ chatAnalysis: jest.fn() }));
|
|
61
|
+
jest.mock('utils/compressImage', () => ({ compressImage: jest.fn() }));
|
|
62
|
+
jest.mock('utils/fetchProductImage', () => ({
|
|
63
|
+
fetchProductImageBase64: jest.fn(),
|
|
64
|
+
getProductImageUrl: jest.fn(),
|
|
65
|
+
}));
|
|
66
|
+
jest.mock('utils/relatedParts', () => ({
|
|
67
|
+
loadRelatedPartsTable: jest.fn(),
|
|
68
|
+
lookupRelatedParts: jest.fn(),
|
|
69
|
+
}));
|
|
70
|
+
jest.mock('utils/nyrisApiKey', () => ({ getEffectiveApiKey: jest.fn() }));
|
|
71
|
+
|
|
72
|
+
const mockChatAnalysis = chatAnalysis as jest.MockedFunction<typeof chatAnalysis>;
|
|
73
|
+
const mockFetchImage = fetchProductImageBase64 as jest.MockedFunction<
|
|
74
|
+
typeof fetchProductImageBase64
|
|
75
|
+
>;
|
|
76
|
+
const mockGetImageUrl = getProductImageUrl as jest.MockedFunction<
|
|
77
|
+
typeof getProductImageUrl
|
|
78
|
+
>;
|
|
79
|
+
const mockLoadRelatedParts = loadRelatedPartsTable as jest.MockedFunction<
|
|
80
|
+
typeof loadRelatedPartsTable
|
|
81
|
+
>;
|
|
82
|
+
const mockCompressImage = compressImage as jest.MockedFunction<
|
|
83
|
+
typeof compressImage
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
jest.clearAllMocks();
|
|
88
|
+
window.settings = {
|
|
89
|
+
algolia: { enabled: false },
|
|
90
|
+
vizo: { findRelatedParts: false },
|
|
91
|
+
} as typeof window.settings;
|
|
92
|
+
mockResultState.productsFromFindApi = [mockProduct];
|
|
93
|
+
mockResultState.productsFromAlgolia = [];
|
|
94
|
+
mockResultState.firstSearchResults = [mockProduct];
|
|
95
|
+
mockResultState.fullResultPool = [mockProduct];
|
|
96
|
+
useConversationStore.getState().reset();
|
|
97
|
+
mockGetImageUrl.mockReturnValue(mockProduct.imageUrl);
|
|
98
|
+
mockFetchImage.mockResolvedValue(null);
|
|
99
|
+
mockLoadRelatedParts.mockResolvedValue({});
|
|
100
|
+
mockCompressImage.mockResolvedValue('data:image/jpeg;base64,new-image');
|
|
101
|
+
mockChatAnalysis.mockResolvedValue({
|
|
102
|
+
quality: 'good',
|
|
103
|
+
route: 'P3',
|
|
104
|
+
opening_message: 'How can I help?',
|
|
105
|
+
message: 'How can I help?',
|
|
106
|
+
action: { type: 'none' },
|
|
107
|
+
options: [],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function expectCompleteMetadata(promptKey: string) {
|
|
112
|
+
expect(mockChatAnalysis).toHaveBeenCalledTimes(1);
|
|
113
|
+
const request = mockChatAnalysis.mock.calls[0][0];
|
|
114
|
+
expect(request.promptKey).toBe(promptKey);
|
|
115
|
+
const text = request.multimodalInputs.map(input => input.metadata).join('\n');
|
|
116
|
+
expect(text).toContain(JSON.stringify(mockProduct.description));
|
|
117
|
+
expect(text).toContain(JSON.stringify(mockProduct.customIds));
|
|
118
|
+
expect(text).toContain(JSON.stringify(mockProduct.attributes));
|
|
119
|
+
expect(text).toContain(JSON.stringify(mockProduct.metadata));
|
|
120
|
+
expect(text).toContain(JSON.stringify(mockProduct.categories));
|
|
121
|
+
expect(text).toContain(JSON.stringify(mockProduct._tags));
|
|
122
|
+
expect(text).toContain('"images":[{"width":300}]');
|
|
123
|
+
expect(text).not.toContain('https://');
|
|
124
|
+
expect(text).not.toContain('"imageUrl"');
|
|
125
|
+
expect(text).not.toContain('"url"');
|
|
126
|
+
expect(mockProduct.url).toBe('https://example.com/product');
|
|
127
|
+
return request;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expectProductEnvelope(text: string, key: string, value: string | number) {
|
|
131
|
+
const product = JSON.parse(text.replace(/\. Result_images\.$/, ''));
|
|
132
|
+
expect(Object.keys(product).sort()).toEqual([key, 'metadata'].sort());
|
|
133
|
+
expect(product[key]).toBe(value);
|
|
134
|
+
expect(product.metadata.title).toBe(mockProduct.title);
|
|
135
|
+
expect(product.metadata.description).toBe(mockProduct.description);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
test.each([
|
|
139
|
+
{ phase: 'evaluation', limit: 30 },
|
|
140
|
+
{ phase: 'recovery', limit: 100 },
|
|
141
|
+
])('$phase counts curated traits within its result limit', async ({ phase, limit }) => {
|
|
142
|
+
const products = Array.from({ length: limit + 1 }, (_, index) => ({
|
|
143
|
+
...mockProduct,
|
|
144
|
+
sku: `pump-${index}`,
|
|
145
|
+
title: `ACME pump 24V DC ${index}`,
|
|
146
|
+
description: `steel housing ${index}`,
|
|
147
|
+
material: { name: 'steel' },
|
|
148
|
+
brand: { name: 'ACME' },
|
|
149
|
+
voltage: ['24V DC'],
|
|
150
|
+
}));
|
|
151
|
+
mockResultState.productsFromFindApi = products;
|
|
152
|
+
const snapshot = JSON.stringify(products);
|
|
153
|
+
const { result } = renderHook(() => ({
|
|
154
|
+
evaluator: useResultEvaluator(),
|
|
155
|
+
recovery: useBadResultsRecovery(),
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
await act(async () => {
|
|
159
|
+
if (phase === 'evaluation') {
|
|
160
|
+
await result.current.evaluator.evaluateResults({
|
|
161
|
+
imageAnalysis: {},
|
|
162
|
+
activeProducts: products,
|
|
163
|
+
query: 'pump',
|
|
164
|
+
});
|
|
165
|
+
} else {
|
|
166
|
+
await result.current.recovery.sendMessage('Find a pump');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(mockChatAnalysis).toHaveBeenCalledTimes(1);
|
|
171
|
+
const request = mockChatAnalysis.mock.calls[0][0];
|
|
172
|
+
const context = request.multimodalInputs[0].metadata;
|
|
173
|
+
const frequency = JSON.parse(
|
|
174
|
+
context.split(/COMMON ATTRIBUTES FOUND IN RESULTS[^\n]*\n/)[1].split('\n\n')[0],
|
|
175
|
+
);
|
|
176
|
+
expect(frequency).toEqual({
|
|
177
|
+
brand: { ACME: limit },
|
|
178
|
+
voltage: { '24V DC': limit },
|
|
179
|
+
product_type: { PUMP: limit },
|
|
180
|
+
material: { steel: limit },
|
|
181
|
+
});
|
|
182
|
+
const product = JSON.parse(
|
|
183
|
+
request.multimodalInputs[1].metadata.replace(/\. Result_images\.$/, ''),
|
|
184
|
+
);
|
|
185
|
+
expect(product.metadata.sku).toBe('pump-0');
|
|
186
|
+
expect(product.metadata.description).toBe('steel housing 0');
|
|
187
|
+
expect(product.metadata.attributes).toEqual(mockProduct.attributes);
|
|
188
|
+
expect(product.metadata.customIds).toEqual(mockProduct.customIds);
|
|
189
|
+
expect(product.metadata.brand).toEqual({ name: 'ACME' });
|
|
190
|
+
expect(product.metadata.voltage).toEqual(['24V DC']);
|
|
191
|
+
expect(product.metadata.material).toEqual({ name: 'steel' });
|
|
192
|
+
expect(Object.keys(product).sort()).toEqual(
|
|
193
|
+
[phase === 'evaluation' ? 'rank' : 'index', 'metadata'].sort(),
|
|
194
|
+
);
|
|
195
|
+
expect(JSON.stringify(products)).toBe(snapshot);
|
|
196
|
+
expect(useConversationStore.getState().error).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test.each([false, true])(
|
|
200
|
+
'evaluation includes complete metadata even when thumbnails are available: %s',
|
|
201
|
+
async hasImages => {
|
|
202
|
+
mockFetchImage.mockResolvedValue(hasImages ? 'product-image' : null);
|
|
203
|
+
const { result } = renderHook(() => useResultEvaluator());
|
|
204
|
+
await act(async () => {
|
|
205
|
+
await result.current.evaluateResults({
|
|
206
|
+
imageAnalysis: {},
|
|
207
|
+
activeProducts: [mockProduct],
|
|
208
|
+
query: 'pump',
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
const request = expectCompleteMetadata('result_evaluator_prompt');
|
|
212
|
+
expectProductEnvelope(request.multimodalInputs[1].metadata, 'rank', 1);
|
|
213
|
+
expect(request.multimodalInputs[1].image).toBe(
|
|
214
|
+
hasImages ? 'product-image' : undefined,
|
|
215
|
+
);
|
|
216
|
+
expect(useConversationStore.getState().error).toBeNull();
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
test.each([
|
|
221
|
+
{ silent: false, algolia: false, hasImages: false },
|
|
222
|
+
{ silent: true, algolia: false, hasImages: true },
|
|
223
|
+
{ silent: false, algolia: true, hasImages: true },
|
|
224
|
+
])('chat sends full metadata for %j', async ({ silent, algolia, hasImages }) => {
|
|
225
|
+
window.settings.algolia.enabled = algolia;
|
|
226
|
+
if (algolia) {
|
|
227
|
+
mockResultState.productsFromAlgolia = [
|
|
228
|
+
mockProduct,
|
|
229
|
+
mockProduct,
|
|
230
|
+
mockProduct,
|
|
231
|
+
];
|
|
232
|
+
mockResultState.fullResultPool = [];
|
|
233
|
+
} else {
|
|
234
|
+
mockResultState.fullResultPool = [mockProduct, mockProduct, mockProduct];
|
|
235
|
+
}
|
|
236
|
+
mockFetchImage.mockResolvedValue(hasImages ? 'product-image' : null);
|
|
237
|
+
const { result } = renderHook(() => useGoodResultsChat());
|
|
238
|
+
await act(async () => {
|
|
239
|
+
await result.current.sendMessage('Find a pump', { silent });
|
|
240
|
+
});
|
|
241
|
+
const request = expectCompleteMetadata('chat_assistant_prompt');
|
|
242
|
+
expectProductEnvelope(request.multimodalInputs[1].metadata, 'id', 'pump-1');
|
|
243
|
+
expect(request.multimodalInputs[1].image).toBe(
|
|
244
|
+
hasImages ? 'product-image' : undefined,
|
|
245
|
+
);
|
|
246
|
+
expect(useConversationStore.getState().error).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test.each([false, true])(
|
|
250
|
+
'recovery includes all metadata for thumbnail and text-only results: %s',
|
|
251
|
+
async hasImages => {
|
|
252
|
+
mockResultState.productsFromFindApi = Array.from(
|
|
253
|
+
{ length: 31 },
|
|
254
|
+
(_, index) => ({ ...mockProduct, sku: `pump-${index}` }),
|
|
255
|
+
);
|
|
256
|
+
mockFetchImage.mockResolvedValue(hasImages ? 'product-image' : null);
|
|
257
|
+
mockChatAnalysis.mockResolvedValue({
|
|
258
|
+
message: 'What pressure do you need?',
|
|
259
|
+
action: 'ask',
|
|
260
|
+
options: [],
|
|
261
|
+
});
|
|
262
|
+
const { result } = renderHook(() => useBadResultsRecovery());
|
|
263
|
+
await act(async () => {
|
|
264
|
+
await result.current.sendMessage('Find a pump');
|
|
265
|
+
});
|
|
266
|
+
const request = expectCompleteMetadata('recovery_prompt');
|
|
267
|
+
expectProductEnvelope(request.multimodalInputs[1].metadata, 'index', 0);
|
|
268
|
+
expect(request.multimodalInputs[1].image).toBe(
|
|
269
|
+
hasImages ? 'product-image' : undefined,
|
|
270
|
+
);
|
|
271
|
+
const remaining = request.multimodalInputs.find(input =>
|
|
272
|
+
input.metadata.startsWith('REMAINING RESULTS'),
|
|
273
|
+
);
|
|
274
|
+
expect(remaining?.metadata).toContain('"index":30');
|
|
275
|
+
expectProductEnvelope(remaining!.metadata.split('\n')[1], 'index', 30);
|
|
276
|
+
expect(remaining?.metadata).toContain(
|
|
277
|
+
JSON.stringify(mockProduct.description),
|
|
278
|
+
);
|
|
279
|
+
expect(remaining?.metadata).toContain(JSON.stringify(mockProduct.attributes));
|
|
280
|
+
expect(useConversationStore.getState().error).toBeNull();
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
test.each([false, true])(
|
|
285
|
+
'image refinement sends complete metadata with thumbnails available: %s',
|
|
286
|
+
async hasImages => {
|
|
287
|
+
mockResultState.productsFromFindApi = [mockProduct, mockProduct, mockProduct];
|
|
288
|
+
mockFetchImage.mockResolvedValue(hasImages ? 'product-image' : null);
|
|
289
|
+
mockChatAnalysis.mockResolvedValue({
|
|
290
|
+
message: 'Found a pump',
|
|
291
|
+
matched_indices: [1],
|
|
292
|
+
});
|
|
293
|
+
const { result } = renderHook(() => useChatAssistantLogic());
|
|
294
|
+
await act(async () => {
|
|
295
|
+
await result.current.handleImageUpload('data:image/jpeg;base64,new-image');
|
|
296
|
+
});
|
|
297
|
+
const request = expectCompleteMetadata('image_refinement');
|
|
298
|
+
expectProductEnvelope(request.multimodalInputs[2].metadata, 'rank', 1);
|
|
299
|
+
expect(request.multimodalInputs[2].image).toBe(
|
|
300
|
+
hasImages ? 'product-image' : undefined,
|
|
301
|
+
);
|
|
302
|
+
expect(mockResultState.setFindApiProducts).toHaveBeenCalledWith([
|
|
303
|
+
mockProduct,
|
|
304
|
+
]);
|
|
305
|
+
expect(useConversationStore.getState().error).toBeNull();
|
|
306
|
+
},
|
|
307
|
+
);
|
|
@@ -14,6 +14,11 @@ import useConversationStore, {
|
|
|
14
14
|
import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
|
|
15
15
|
import useResultStore from 'stores/result/resultStore';
|
|
16
16
|
import { chatAnalysis } from 'services/vizo';
|
|
17
|
+
import {
|
|
18
|
+
getChatAttributeFrequency,
|
|
19
|
+
getChatProductAttributes,
|
|
20
|
+
getChatProductMetadata,
|
|
21
|
+
} from 'utils/chatProductMetadata';
|
|
17
22
|
import {
|
|
18
23
|
getProductImageUrl,
|
|
19
24
|
fetchProductImageBase64,
|
|
@@ -98,29 +103,8 @@ export function useBadResultsRecovery() {
|
|
|
98
103
|
const allResults = originalProducts
|
|
99
104
|
.slice(0, 100)
|
|
100
105
|
.map((p: any, i: number) => {
|
|
101
|
-
|
|
102
|
-
const metadata
|
|
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
|
-
});
|
|
106
|
+
const fullMetadata = getChatProductMetadata(p);
|
|
107
|
+
const metadata = getChatProductAttributes(fullMetadata);
|
|
124
108
|
|
|
125
109
|
// Extract from title/description using common patterns
|
|
126
110
|
const title = p.title || p.name || '';
|
|
@@ -172,26 +156,15 @@ export function useBadResultsRecovery() {
|
|
|
172
156
|
|
|
173
157
|
return {
|
|
174
158
|
index: i,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
description: (p.description || '').substring(0, 100),
|
|
178
|
-
category: p.category || p.type || '',
|
|
179
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
|
159
|
+
metadata: fullMetadata,
|
|
160
|
+
attributes: metadata,
|
|
180
161
|
};
|
|
181
162
|
});
|
|
182
163
|
|
|
183
164
|
// Analyze common attributes across results for smart question options
|
|
184
|
-
const attributeFrequency
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
});
|
|
165
|
+
const attributeFrequency = getChatAttributeFrequency(
|
|
166
|
+
allResults.map(result => ({ metadata: result.attributes })),
|
|
167
|
+
);
|
|
195
168
|
|
|
196
169
|
// Build conversation history
|
|
197
170
|
const conversationHistory = messages.map(m => ({
|
|
@@ -255,8 +228,9 @@ PRODUCT LIST (first 30 with thumbnails where available, indices 0-based for filt
|
|
|
255
228
|
// Part 2: First 30 results interleaved with thumbnails
|
|
256
229
|
allResults
|
|
257
230
|
.slice(0, 30)
|
|
258
|
-
.forEach((
|
|
259
|
-
const
|
|
231
|
+
.forEach(({ index, metadata }, i: number) => {
|
|
232
|
+
const r = { index, metadata };
|
|
233
|
+
const metaLine = JSON.stringify(getChatProductMetadata(r));
|
|
260
234
|
|
|
261
235
|
if (hasVisualContext && imageResults[i]?.base64) {
|
|
262
236
|
multimodalParts.push({
|
|
@@ -276,8 +250,8 @@ PRODUCT LIST (first 30 with thumbnails where available, indices 0-based for filt
|
|
|
276
250
|
${allResults
|
|
277
251
|
.slice(30)
|
|
278
252
|
.map(
|
|
279
|
-
(
|
|
280
|
-
|
|
253
|
+
({ index, metadata }) =>
|
|
254
|
+
JSON.stringify(getChatProductMetadata({ index, metadata })),
|
|
281
255
|
)
|
|
282
256
|
.join('\n')}`,
|
|
283
257
|
});
|
|
@@ -360,12 +334,15 @@ Based on the user's answer and the failure mode strategy, decide whether to ask
|
|
|
360
334
|
}
|
|
361
335
|
|
|
362
336
|
// Add assistant message
|
|
363
|
-
addMessage(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
337
|
+
addMessage(
|
|
338
|
+
{
|
|
339
|
+
role: 'assistant',
|
|
340
|
+
content: p5Response.message,
|
|
341
|
+
action: { type: p5Response.action },
|
|
342
|
+
options: options,
|
|
343
|
+
},
|
|
344
|
+
{ isApiResponse: true },
|
|
345
|
+
);
|
|
369
346
|
} catch (err: any) {
|
|
370
347
|
console.error('[P5] Error:', err);
|
|
371
348
|
setError(err.message || 'Failed to process message');
|
|
@@ -12,6 +12,7 @@ import useSmartFiltersStore from 'stores/smartFilters/smartFiltersStore';
|
|
|
12
12
|
import useResultStore from 'stores/result/resultStore';
|
|
13
13
|
import useRequestStore from 'stores/request/requestStore';
|
|
14
14
|
import { chatAnalysis } from 'services/vizo';
|
|
15
|
+
import { getChatProductMetadata } from 'utils/chatProductMetadata';
|
|
15
16
|
import {
|
|
16
17
|
fetchProductImageBase64,
|
|
17
18
|
getProductImageUrl,
|
|
@@ -234,7 +235,7 @@ export function useGoodResultsChat() {
|
|
|
234
235
|
id: p.sku || p.objectID || `product-${i}`,
|
|
235
236
|
title: title || 'Untitled',
|
|
236
237
|
brand: p.brand || '',
|
|
237
|
-
description:
|
|
238
|
+
description: p.description || '',
|
|
238
239
|
mpn: p.customIds?.mpn || p.ids?.mpn || '',
|
|
239
240
|
};
|
|
240
241
|
});
|
|
@@ -267,7 +268,7 @@ export function useGoodResultsChat() {
|
|
|
267
268
|
content: m.content,
|
|
268
269
|
}));
|
|
269
270
|
|
|
270
|
-
// Fetch product thumbnail images with caching — send images for ALL results (up to
|
|
271
|
+
// Fetch product thumbnail images with caching — send images for ALL results (up to 30)
|
|
271
272
|
// Cache persists across filter operations; only clears on new search session
|
|
272
273
|
const productsForImages = poolForAI.slice(0, 30);
|
|
273
274
|
const currentSessionId = useConversationStore.getState().sessionId;
|
|
@@ -317,19 +318,18 @@ ${JSON.stringify(attributeFrequency, null, 2)}
|
|
|
317
318
|
|
|
318
319
|
${visualNotice}
|
|
319
320
|
|
|
320
|
-
PRODUCT LIST (up to
|
|
321
|
+
PRODUCT LIST (up to 30 results — each JSON entry has a unique id and complete product metadata. When present, metadata._tags contains AI-generated type/color/shape/material tags pre-analyzed from product images. Use these tags for filtering. Return matching id values in matched_ids array):
|
|
321
322
|
`,
|
|
322
323
|
});
|
|
323
324
|
|
|
324
|
-
// Part 2: Product entries with interleaved images (for all results up to
|
|
325
|
+
// Part 2: Product entries with interleaved images (for all results up to 30)
|
|
325
326
|
visibleResults.forEach((r, i) => {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const metaLine = `id: ${r.id} | ${r.title} | brand: ${r.brand} | mpn: ${r.mpn} | desc: ${r.description}${tagStr}`;
|
|
327
|
+
const metaLine = JSON.stringify(
|
|
328
|
+
getChatProductMetadata({
|
|
329
|
+
id: r.id,
|
|
330
|
+
metadata: getChatProductMetadata(poolForAI[i]),
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
333
|
|
|
334
334
|
// Include product image if available
|
|
335
335
|
if (
|
|
@@ -587,12 +587,15 @@ Respond to the user's message with an appropriate action and follow-up options.`
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
// Add assistant message
|
|
590
|
-
addMessage(
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
590
|
+
addMessage(
|
|
591
|
+
{
|
|
592
|
+
role: 'assistant',
|
|
593
|
+
content: finalMessage,
|
|
594
|
+
action: p3Response.action,
|
|
595
|
+
options: finalOptions,
|
|
596
|
+
},
|
|
597
|
+
{ isApiResponse: true },
|
|
598
|
+
);
|
|
596
599
|
} catch (err: any) {
|
|
597
600
|
console.error('[P3] Error:', err);
|
|
598
601
|
setError(err.message || 'Failed to process message');
|
|
@@ -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 {
|
|
@@ -74,29 +79,8 @@ export function useResultEvaluator() {
|
|
|
74
79
|
const resultsSummary = activeProducts
|
|
75
80
|
.slice(0, 30)
|
|
76
81
|
.map((p: any, i: number) => {
|
|
77
|
-
|
|
78
|
-
const metadata
|
|
79
|
-
const metadataFields = [
|
|
80
|
-
'voltage',
|
|
81
|
-
'size',
|
|
82
|
-
'brand',
|
|
83
|
-
'manufacturer',
|
|
84
|
-
'material',
|
|
85
|
-
'type',
|
|
86
|
-
'category',
|
|
87
|
-
'color',
|
|
88
|
-
'dimensions',
|
|
89
|
-
'power',
|
|
90
|
-
'current',
|
|
91
|
-
'connector',
|
|
92
|
-
'model',
|
|
93
|
-
'series',
|
|
94
|
-
'product_line',
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
metadataFields.forEach(field => {
|
|
98
|
-
if (p[field]) metadata[field] = p[field];
|
|
99
|
-
});
|
|
82
|
+
const fullMetadata = getChatProductMetadata(p);
|
|
83
|
+
const metadata = getChatProductAttributes(fullMetadata);
|
|
100
84
|
|
|
101
85
|
// Extract from title/description using common patterns
|
|
102
86
|
const title = p.title || p.name || '';
|
|
@@ -181,11 +165,8 @@ export function useResultEvaluator() {
|
|
|
181
165
|
|
|
182
166
|
return {
|
|
183
167
|
rank: i + 1,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
category: p.category || p.type || 'Unknown',
|
|
187
|
-
score: p.score || p.similarity || 0,
|
|
188
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
|
168
|
+
metadata: fullMetadata,
|
|
169
|
+
attributes: metadata,
|
|
189
170
|
};
|
|
190
171
|
});
|
|
191
172
|
|
|
@@ -203,17 +184,9 @@ export function useResultEvaluator() {
|
|
|
203
184
|
});
|
|
204
185
|
|
|
205
186
|
// Analyze common attributes across results for filter suggestions
|
|
206
|
-
const attributeFrequency
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
Object.entries(r.metadata).forEach(([key, value]) => {
|
|
210
|
-
if (!attributeFrequency[key]) attributeFrequency[key] = {};
|
|
211
|
-
const strValue = String(value);
|
|
212
|
-
attributeFrequency[key][strValue] =
|
|
213
|
-
(attributeFrequency[key][strValue] || 0) + 1;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
});
|
|
187
|
+
const attributeFrequency = getChatAttributeFrequency(
|
|
188
|
+
resultsSummary.map(result => ({ metadata: result.attributes })),
|
|
189
|
+
);
|
|
217
190
|
|
|
218
191
|
const requestImage = useRequestStore.getState().requestImages;
|
|
219
192
|
const regions = useRequestStore.getState().regions;
|
|
@@ -232,20 +205,16 @@ IMAGE DESCRIPTION: ${hasRequestImage ? imageDescription : 'N/A'}
|
|
|
232
205
|
SPECIFICATION (from image analysis):
|
|
233
206
|
${hasRequestImage ? JSON.stringify(p4Specification, null, 2) : 'N/A'}
|
|
234
207
|
|
|
235
|
-
TOP 20 RESULTS WITH METADATA:
|
|
236
|
-
${resultsSummary
|
|
237
|
-
.map(
|
|
238
|
-
r =>
|
|
239
|
-
`${r.rank}. ${r.title}${r.description ? ` - ${r.description}` : ''} (Score: ${r.score})${r.metadata ? `\n Metadata: ${JSON.stringify(r.metadata)}` : ''}`,
|
|
240
|
-
)
|
|
241
|
-
.join('\n')}
|
|
242
208
|
|
|
243
209
|
COMMON ATTRIBUTES FOUND IN RESULTS (for filter suggestions):
|
|
244
210
|
${JSON.stringify(attributeFrequency, null, 2)}
|
|
245
211
|
|
|
246
212
|
TOTAL RESULTS: ${resultsSummary.length}
|
|
247
213
|
|
|
248
|
-
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
|
|
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
|
+
`;
|
|
249
218
|
|
|
250
219
|
const productsForImages = activeProducts.slice(0, 30);
|
|
251
220
|
const imageResults = await Promise.all(
|
|
@@ -268,14 +237,9 @@ Evaluate if these results match ${evaluationTarget}. If they do (same type of pr
|
|
|
268
237
|
mimeType?: string;
|
|
269
238
|
}> = [];
|
|
270
239
|
|
|
271
|
-
resultsSummary.forEach((
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
const tagStr = tags
|
|
275
|
-
? ` | TAGS: type=${tags.productType}, color=${tags.color}, shape=${tags.shape}, material=${tags.material}`
|
|
276
|
-
: '';
|
|
277
|
-
|
|
278
|
-
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.`;
|
|
279
243
|
|
|
280
244
|
if (hasVisualContext && imageResults[i]?.base64) {
|
|
281
245
|
productImageParts.push({
|
|
@@ -283,6 +247,8 @@ Evaluate if these results match ${evaluationTarget}. If they do (same type of pr
|
|
|
283
247
|
image: imageResults[i].base64 || '',
|
|
284
248
|
mimeType: 'image/jpeg',
|
|
285
249
|
});
|
|
250
|
+
} else {
|
|
251
|
+
productImageParts.push({ metadata: metaLine });
|
|
286
252
|
}
|
|
287
253
|
});
|
|
288
254
|
|
package/src/services/Feedback.ts
CHANGED
|
@@ -6,6 +6,7 @@ import NyrisAPI, {
|
|
|
6
6
|
} from '@nyris/nyris-api';
|
|
7
7
|
import { getNyrisSettings } from 'utils/nyrisApiKey';
|
|
8
8
|
import useResultStore from 'stores/result/resultStore';
|
|
9
|
+
import useConversationStore from 'stores/chat/conversationStore';
|
|
9
10
|
import { CONVERSION_SUBTYPE, normalizeSubtype } from './feedbackSubtypes';
|
|
10
11
|
|
|
11
12
|
export interface FeedbackIds {
|
|
@@ -164,8 +165,9 @@ export const trackResultFeedback = (
|
|
|
164
165
|
success: boolean,
|
|
165
166
|
comment?: string,
|
|
166
167
|
ids?: FeedbackIds,
|
|
167
|
-
) =>
|
|
168
|
-
|
|
168
|
+
) => {
|
|
169
|
+
useConversationStore.getState().setSearchResolved(success);
|
|
170
|
+
return trackFeedbackEvent(
|
|
169
171
|
{
|
|
170
172
|
event: 'feedback',
|
|
171
173
|
data: {
|
|
@@ -175,6 +177,7 @@ export const trackResultFeedback = (
|
|
|
175
177
|
},
|
|
176
178
|
ids,
|
|
177
179
|
);
|
|
180
|
+
};
|
|
178
181
|
|
|
179
182
|
/** `region` event; converts x1/y1/x2/y2 to the API's x/y/w/h rectangle. */
|
|
180
183
|
export const trackRegion = (
|