@nyris/nyris-webapp 0.3.91 → 0.3.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.f2255597.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.f2255597.js +0 -3
- package/src/utils/addAssets.ts +0 -40
- /package/build/static/js/{main.f2255597.js.LICENSE.txt → main.36b77705.js.LICENSE.txt} +0 -0
package/src/services/vizo.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import { RectCoords } from '@nyris/nyris-api';
|
|
1
2
|
import axios from 'axios';
|
|
3
|
+
import { cropImage } from 'utils/cropImageToBase64';
|
|
4
|
+
|
|
5
|
+
export interface ChatMultimodalInput {
|
|
6
|
+
metadata: string;
|
|
7
|
+
mimeType?: string;
|
|
8
|
+
image?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ChatAnalysisParams {
|
|
12
|
+
promptKey: string;
|
|
13
|
+
multimodalInputs: ChatMultimodalInput[];
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
2
17
|
// Utility: Convert HTMLCanvasElement to Blob
|
|
3
18
|
export function canvasToBlob(
|
|
4
19
|
canvas: HTMLCanvasElement,
|
|
@@ -20,6 +35,149 @@ export function canvasToBlob(
|
|
|
20
35
|
});
|
|
21
36
|
}
|
|
22
37
|
|
|
38
|
+
export interface SmartFilterRerankItemMetadata {
|
|
39
|
+
title: string;
|
|
40
|
+
description: string;
|
|
41
|
+
language: string;
|
|
42
|
+
sku: string;
|
|
43
|
+
score: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SmartFilterRerankItem {
|
|
47
|
+
sku: string;
|
|
48
|
+
metadata: SmartFilterRerankItemMetadata;
|
|
49
|
+
imageUrl: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SmartFilterRerankPayload {
|
|
53
|
+
filters: Record<string, string>;
|
|
54
|
+
items: SmartFilterRerankItem[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SmartFilterRerankParams {
|
|
58
|
+
image: File | HTMLCanvasElement;
|
|
59
|
+
payload: SmartFilterRerankPayload;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
apiKey?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SmartFilterRerankResponse {
|
|
65
|
+
filtered_items: Array<{ sku: string }>;
|
|
66
|
+
// TODO: confirm final backend field name; some responses use `reranked`.
|
|
67
|
+
reranked?: boolean;
|
|
68
|
+
rerankingApplied?: boolean;
|
|
69
|
+
message?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface SmartFilterGenerateParams {
|
|
73
|
+
image: File | HTMLCanvasElement;
|
|
74
|
+
prompt: string;
|
|
75
|
+
model?: string;
|
|
76
|
+
timeoutMs?: number;
|
|
77
|
+
apiKey?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface SmartFilterGenerateResponse {
|
|
81
|
+
filters: Record<string, string | string[]>;
|
|
82
|
+
rerankingApplied?: boolean;
|
|
83
|
+
filtered_items?: Array<{ sku: string }>;
|
|
84
|
+
rerankedSkus?: string[];
|
|
85
|
+
message?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function rerankSmartFilters({
|
|
89
|
+
image,
|
|
90
|
+
payload,
|
|
91
|
+
apiKey,
|
|
92
|
+
}: SmartFilterRerankParams): Promise<SmartFilterRerankResponse> {
|
|
93
|
+
const settings = window.settings;
|
|
94
|
+
const isSmartFiltersEnabled = !!settings.useSmartFilters;
|
|
95
|
+
if (!isSmartFiltersEnabled) {
|
|
96
|
+
return {
|
|
97
|
+
filtered_items: [],
|
|
98
|
+
rerankingApplied: false,
|
|
99
|
+
message: 'SMART_FILTERS_DISABLED',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const formData = new FormData();
|
|
104
|
+
if (image instanceof HTMLCanvasElement) {
|
|
105
|
+
const imageBlob = await canvasToBlob(image);
|
|
106
|
+
formData.append('image', imageBlob, 'smart-filters-rerank.jpg');
|
|
107
|
+
} else {
|
|
108
|
+
formData.append('image', image);
|
|
109
|
+
}
|
|
110
|
+
formData.append('payload', JSON.stringify(payload));
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const response = await axios.post<SmartFilterRerankResponse>(
|
|
114
|
+
`${window.settings.baseUrl}/api/ai-analysis/smart-filters/rerank`,
|
|
115
|
+
formData,
|
|
116
|
+
{
|
|
117
|
+
headers: {
|
|
118
|
+
'x-api-key': apiKey || window.settings.aiApiKey || window.settings.apiKey,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return response.data;
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
if (error?.code === 'ECONNABORTED') {
|
|
126
|
+
throw new Error('SMART_FILTER_TIMEOUT');
|
|
127
|
+
}
|
|
128
|
+
if (error.response) {
|
|
129
|
+
throw new Error(`API error: ${error.response.statusText}`);
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error(`API error: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function generateSmartFilters({
|
|
137
|
+
image,
|
|
138
|
+
prompt,
|
|
139
|
+
}: SmartFilterGenerateParams): Promise<SmartFilterGenerateResponse> {
|
|
140
|
+
const settings = window.settings;
|
|
141
|
+
const isSmartFiltersEnabled = !!settings.useSmartFilters;
|
|
142
|
+
if (!isSmartFiltersEnabled) {
|
|
143
|
+
return {
|
|
144
|
+
filters: {},
|
|
145
|
+
rerankingApplied: false,
|
|
146
|
+
message: 'SMART_FILTERS_DISABLED',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const formData = new FormData();
|
|
151
|
+
if (image instanceof HTMLCanvasElement) {
|
|
152
|
+
const imageBlob = await canvasToBlob(image);
|
|
153
|
+
formData.append('image', imageBlob, 'smart-filters-generate.jpg');
|
|
154
|
+
} else {
|
|
155
|
+
formData.append('image', image);
|
|
156
|
+
}
|
|
157
|
+
formData.append('prompt', prompt);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const response = await axios.post<SmartFilterGenerateResponse>(
|
|
161
|
+
`${window.settings.baseUrl}/api/ai-analysis/smart-filters/generate`,
|
|
162
|
+
formData,
|
|
163
|
+
{
|
|
164
|
+
headers: {
|
|
165
|
+
'x-api-key': window.settings.aiApiKey || window.settings.apiKey,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
return response.data;
|
|
170
|
+
} catch (error: any) {
|
|
171
|
+
if (error?.code === 'ECONNABORTED') {
|
|
172
|
+
throw new Error('SMART_FILTER_TIMEOUT');
|
|
173
|
+
}
|
|
174
|
+
if (error.response) {
|
|
175
|
+
throw new Error(`API error: ${error.response.statusText}`);
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`API error: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
23
181
|
function buildProductEntry(p: any, index: number): string {
|
|
24
182
|
const title = p.Bezeichnung || p.title || p.name || '';
|
|
25
183
|
const desc =
|
|
@@ -50,7 +208,7 @@ function buildProductEntry(p: any, index: number): string {
|
|
|
50
208
|
|
|
51
209
|
const idStr = idParts.length > 0 ? ` | IDs: ${idParts.join(', ')}` : '';
|
|
52
210
|
|
|
53
|
-
return `
|
|
211
|
+
return `index: ${index} | SKU: ${sku} | Brand: ${brand} | ${title} | ${desc}${idStr}`;
|
|
54
212
|
}
|
|
55
213
|
|
|
56
214
|
/**
|
|
@@ -60,18 +218,18 @@ function buildProductEntry(p: any, index: number): string {
|
|
|
60
218
|
*/
|
|
61
219
|
export async function groundedSearch(
|
|
62
220
|
imageInput: File | HTMLCanvasElement,
|
|
63
|
-
|
|
221
|
+
region?: RectCoords,
|
|
64
222
|
): Promise<any> {
|
|
65
223
|
const formData = new FormData();
|
|
66
224
|
let imageBlobOrFile: File | Blob;
|
|
67
225
|
|
|
68
226
|
if (imageInput instanceof HTMLCanvasElement) {
|
|
69
|
-
|
|
227
|
+
const croppedCanvas = cropImage(imageInput, region);
|
|
228
|
+
imageBlobOrFile = await canvasToBlob(croppedCanvas);
|
|
70
229
|
formData.append('image', imageBlobOrFile, 'canvas.jpg');
|
|
71
230
|
} else {
|
|
72
231
|
formData.append('image', imageInput);
|
|
73
232
|
}
|
|
74
|
-
|
|
75
233
|
try {
|
|
76
234
|
const response = await axios.post(
|
|
77
235
|
'https://api.nyris.io/api/ai-analysis/grounded-search',
|
|
@@ -159,3 +317,33 @@ export async function groundingSearchQuery({
|
|
|
159
317
|
}
|
|
160
318
|
}
|
|
161
319
|
}
|
|
320
|
+
|
|
321
|
+
export async function chatAnalysis({
|
|
322
|
+
promptKey,
|
|
323
|
+
multimodalInputs,
|
|
324
|
+
apiKey,
|
|
325
|
+
}: ChatAnalysisParams): Promise<any> {
|
|
326
|
+
try {
|
|
327
|
+
const response = await axios.post(
|
|
328
|
+
'https://api.nyris.io/api/ai-analysis/chat',
|
|
329
|
+
{
|
|
330
|
+
promptKey,
|
|
331
|
+
multimodalInputs,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
headers: {
|
|
335
|
+
'x-api-key': apiKey || window.settings.aiApiKey || '',
|
|
336
|
+
'Content-Type': 'application/json',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
return response.data;
|
|
342
|
+
} catch (error: any) {
|
|
343
|
+
if (error.response) {
|
|
344
|
+
throw new Error(`API error: ${error.response.statusText}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new Error(`API error: ${error.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
export interface ChatMessage {
|
|
5
|
+
id: string;
|
|
6
|
+
role: 'user' | 'assistant' | 'system';
|
|
7
|
+
content: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
// Context that was included with this message
|
|
10
|
+
context?: {
|
|
11
|
+
productSkus?: string[];
|
|
12
|
+
imageDescription?: string;
|
|
13
|
+
filters?: string[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ProductContext {
|
|
18
|
+
sku: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
imageUrl?: string;
|
|
22
|
+
attributes?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ChatState {
|
|
26
|
+
// Messages
|
|
27
|
+
messages: ChatMessage[];
|
|
28
|
+
|
|
29
|
+
// UI State
|
|
30
|
+
isOpen: boolean;
|
|
31
|
+
isLoading: boolean;
|
|
32
|
+
isStreaming: boolean;
|
|
33
|
+
|
|
34
|
+
// Error handling
|
|
35
|
+
error: string | null;
|
|
36
|
+
|
|
37
|
+
// Session
|
|
38
|
+
conversationId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ChatActions {
|
|
42
|
+
// Message actions
|
|
43
|
+
addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'>) => void;
|
|
44
|
+
updateLastAssistantMessage: (content: string) => void;
|
|
45
|
+
clearMessages: () => void;
|
|
46
|
+
|
|
47
|
+
// UI actions
|
|
48
|
+
setIsOpen: (isOpen: boolean) => void;
|
|
49
|
+
toggleOpen: () => void;
|
|
50
|
+
setIsLoading: (isLoading: boolean) => void;
|
|
51
|
+
setIsStreaming: (isStreaming: boolean) => void;
|
|
52
|
+
|
|
53
|
+
// Error
|
|
54
|
+
setError: (error: string | null) => void;
|
|
55
|
+
|
|
56
|
+
// Session
|
|
57
|
+
newConversation: () => void;
|
|
58
|
+
|
|
59
|
+
// Reset
|
|
60
|
+
reset: () => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const generateId = () =>
|
|
64
|
+
`msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
65
|
+
const generateConversationId = () =>
|
|
66
|
+
`conv_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
67
|
+
|
|
68
|
+
const initialState: ChatState = {
|
|
69
|
+
messages: [],
|
|
70
|
+
isOpen: false,
|
|
71
|
+
isLoading: false,
|
|
72
|
+
isStreaming: false,
|
|
73
|
+
error: null,
|
|
74
|
+
conversationId: generateConversationId(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const useChatStore = create<ChatState & ChatActions>()(
|
|
78
|
+
persist(
|
|
79
|
+
(set, _get) => ({
|
|
80
|
+
...initialState,
|
|
81
|
+
|
|
82
|
+
addMessage: message =>
|
|
83
|
+
set(state => ({
|
|
84
|
+
messages: [
|
|
85
|
+
...state.messages,
|
|
86
|
+
{
|
|
87
|
+
...message,
|
|
88
|
+
id: generateId(),
|
|
89
|
+
timestamp: Date.now(),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
error: null,
|
|
93
|
+
})),
|
|
94
|
+
|
|
95
|
+
updateLastAssistantMessage: content =>
|
|
96
|
+
set(state => {
|
|
97
|
+
const messages = [...state.messages];
|
|
98
|
+
const lastIndex = messages.length - 1;
|
|
99
|
+
|
|
100
|
+
if (lastIndex >= 0 && messages[lastIndex].role === 'assistant') {
|
|
101
|
+
messages[lastIndex] = {
|
|
102
|
+
...messages[lastIndex],
|
|
103
|
+
content,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { messages };
|
|
108
|
+
}),
|
|
109
|
+
|
|
110
|
+
clearMessages: () =>
|
|
111
|
+
set({
|
|
112
|
+
messages: [],
|
|
113
|
+
error: null,
|
|
114
|
+
}),
|
|
115
|
+
|
|
116
|
+
setIsOpen: isOpen => set({ isOpen }),
|
|
117
|
+
|
|
118
|
+
toggleOpen: () => set(state => ({ isOpen: !state.isOpen })),
|
|
119
|
+
|
|
120
|
+
setIsLoading: isLoading => set({ isLoading }),
|
|
121
|
+
|
|
122
|
+
setIsStreaming: isStreaming => set({ isStreaming }),
|
|
123
|
+
|
|
124
|
+
setError: error => set({ error, isLoading: false, isStreaming: false }),
|
|
125
|
+
|
|
126
|
+
newConversation: () =>
|
|
127
|
+
set({
|
|
128
|
+
messages: [],
|
|
129
|
+
conversationId: generateConversationId(),
|
|
130
|
+
error: null,
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
reset: () =>
|
|
134
|
+
set({
|
|
135
|
+
...initialState,
|
|
136
|
+
conversationId: generateConversationId(),
|
|
137
|
+
}),
|
|
138
|
+
}),
|
|
139
|
+
{
|
|
140
|
+
name: 'chat-storage',
|
|
141
|
+
partialize: state => ({
|
|
142
|
+
// Only persist conversation history, not UI state
|
|
143
|
+
messages: state.messages.slice(-50), // Keep last 50 messages
|
|
144
|
+
conversationId: state.conversationId,
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
export default useChatStore;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
// Types for the conversation flow
|
|
4
|
+
export type ConversationRoute = 'pending' | 'P3' | 'P5';
|
|
5
|
+
export type ResultQuality = 'pending' | 'good' | 'bad';
|
|
6
|
+
export type FailureMode =
|
|
7
|
+
| null
|
|
8
|
+
| 'wrong_category'
|
|
9
|
+
| 'not_in_catalog'
|
|
10
|
+
| 'ocr_mismatch'
|
|
11
|
+
| 'ambiguous_image'
|
|
12
|
+
| 'full_machine'
|
|
13
|
+
| 'multiple_objects';
|
|
14
|
+
|
|
15
|
+
export type ActionType =
|
|
16
|
+
| 'filter_apply'
|
|
17
|
+
| 'filter_remove'
|
|
18
|
+
| 'filter_clear'
|
|
19
|
+
| 'text_search'
|
|
20
|
+
| 'catalog_search_image'
|
|
21
|
+
| 'catalog_search_text'
|
|
22
|
+
| 'none' // P3 actions
|
|
23
|
+
| 'ask'
|
|
24
|
+
| 'resurface'
|
|
25
|
+
| 'reset'
|
|
26
|
+
| 'exit'; // P5 actions
|
|
27
|
+
|
|
28
|
+
export interface QuickReplyOption {
|
|
29
|
+
label: string;
|
|
30
|
+
action_type:
|
|
31
|
+
| 'filter'
|
|
32
|
+
| 'compare'
|
|
33
|
+
| 'identify'
|
|
34
|
+
| 'clarify'
|
|
35
|
+
| 'confirm'
|
|
36
|
+
| 'text_search_prompt'
|
|
37
|
+
| 'catalog_search_image'
|
|
38
|
+
| 'catalog_search_text'
|
|
39
|
+
| 'restore_previous'
|
|
40
|
+
| 'related_part'
|
|
41
|
+
| 'find_related_parts'
|
|
42
|
+
| 'restore_original';
|
|
43
|
+
value?: string;
|
|
44
|
+
key?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChatMessage {
|
|
48
|
+
id: string;
|
|
49
|
+
role: 'user' | 'assistant' | 'system';
|
|
50
|
+
content: string;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
imageUrl?: string;
|
|
53
|
+
action?: {
|
|
54
|
+
type: ActionType;
|
|
55
|
+
key?: string;
|
|
56
|
+
value?: string;
|
|
57
|
+
filtered_results?: number[];
|
|
58
|
+
};
|
|
59
|
+
options?: QuickReplyOption[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ActiveFilter {
|
|
63
|
+
key: string;
|
|
64
|
+
value: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RelatedPartSuggestion {
|
|
68
|
+
label: string;
|
|
69
|
+
description: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ConversationState {
|
|
73
|
+
// Session state
|
|
74
|
+
sessionId: string;
|
|
75
|
+
isEvaluated: boolean; // P2 has fired
|
|
76
|
+
// Routing
|
|
77
|
+
route: ConversationRoute;
|
|
78
|
+
quality: ResultQuality;
|
|
79
|
+
failureMode: FailureMode;
|
|
80
|
+
|
|
81
|
+
// P5 recovery
|
|
82
|
+
attemptCounter: number; // 1-3 for P5 recovery
|
|
83
|
+
|
|
84
|
+
// Conversation
|
|
85
|
+
messages: ChatMessage[];
|
|
86
|
+
activeFilters: ActiveFilter[];
|
|
87
|
+
|
|
88
|
+
// UI state
|
|
89
|
+
isOpen: boolean;
|
|
90
|
+
hasNotification: boolean; // Pulse badge on chat icon when new message arrives
|
|
91
|
+
isLoading: boolean;
|
|
92
|
+
error: string | null;
|
|
93
|
+
|
|
94
|
+
// Opening message from P2
|
|
95
|
+
openingMessage: string | null;
|
|
96
|
+
openingOptions: QuickReplyOption[];
|
|
97
|
+
|
|
98
|
+
// Description mode — set when user clicks "Describe what you're looking for"
|
|
99
|
+
awaitingDescription: boolean;
|
|
100
|
+
|
|
101
|
+
// Pending catalog search — stored for deferred API search after AI refinement
|
|
102
|
+
pendingCatalogImage: File | string | null;
|
|
103
|
+
pendingCatalogText: string | null;
|
|
104
|
+
catalogSearchReady: boolean;
|
|
105
|
+
|
|
106
|
+
// Pre-catalog results — saved before catalog search so user can go back
|
|
107
|
+
preCatalogResults: any[] | null;
|
|
108
|
+
relatedPartsUsed: boolean;
|
|
109
|
+
pendingRelatedPart: string | null;
|
|
110
|
+
relatedPartsTableSuggestions: RelatedPartSuggestion[];
|
|
111
|
+
|
|
112
|
+
requestImageKeyword?: string; // Added to track the keyword extracted from the image in P2
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ConversationActions {
|
|
116
|
+
// Session management
|
|
117
|
+
startNewSession: () => void;
|
|
118
|
+
|
|
119
|
+
// P2 evaluation result
|
|
120
|
+
setEvaluationResult: (
|
|
121
|
+
quality: ResultQuality,
|
|
122
|
+
route: ConversationRoute,
|
|
123
|
+
openingMessage: string,
|
|
124
|
+
options: QuickReplyOption[],
|
|
125
|
+
requestImageKeyword?: string,
|
|
126
|
+
) => void;
|
|
127
|
+
|
|
128
|
+
// Messages
|
|
129
|
+
addMessage: (message: Omit<ChatMessage, 'id' | 'timestamp'>) => void;
|
|
130
|
+
clearMessages: () => void;
|
|
131
|
+
|
|
132
|
+
// Filters (for P3)
|
|
133
|
+
addFilter: (key: string, value: string) => void;
|
|
134
|
+
removeFilter: (key: string) => void;
|
|
135
|
+
clearFilters: () => void;
|
|
136
|
+
|
|
137
|
+
// P5 recovery
|
|
138
|
+
incrementAttempt: () => void;
|
|
139
|
+
|
|
140
|
+
// UI state
|
|
141
|
+
setIsOpen: (isOpen: boolean) => void;
|
|
142
|
+
clearNotification: () => void;
|
|
143
|
+
setIsLoading: (isLoading: boolean) => void;
|
|
144
|
+
setError: (error: string | null) => void;
|
|
145
|
+
setAwaitingDescription: (awaiting: boolean) => void;
|
|
146
|
+
setPendingCatalogImage: (file: File | string | null) => void;
|
|
147
|
+
setPendingCatalogText: (text: string | null) => void;
|
|
148
|
+
setCatalogSearchReady: (ready: boolean) => void;
|
|
149
|
+
setPreCatalogResults: (results: any[] | null) => void;
|
|
150
|
+
setPendingRelatedPart: (part: string | null) => void;
|
|
151
|
+
setRelatedPartsTableSuggestions: (
|
|
152
|
+
suggestions: RelatedPartSuggestion[],
|
|
153
|
+
) => void;
|
|
154
|
+
|
|
155
|
+
setRelatedPartsUsed: (used: boolean) => void;
|
|
156
|
+
|
|
157
|
+
// Reset
|
|
158
|
+
reset: () => void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const generateId = () =>
|
|
162
|
+
`msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
163
|
+
const generateSessionId = () =>
|
|
164
|
+
`session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
165
|
+
|
|
166
|
+
const initialState: ConversationState = {
|
|
167
|
+
sessionId: '',
|
|
168
|
+
isEvaluated: false,
|
|
169
|
+
route: 'pending',
|
|
170
|
+
quality: 'pending',
|
|
171
|
+
failureMode: null,
|
|
172
|
+
attemptCounter: 0,
|
|
173
|
+
messages: [],
|
|
174
|
+
activeFilters: [],
|
|
175
|
+
isOpen: false, // Start closed so left panel is open by default
|
|
176
|
+
hasNotification: false,
|
|
177
|
+
isLoading: false,
|
|
178
|
+
error: null,
|
|
179
|
+
openingMessage: null,
|
|
180
|
+
openingOptions: [],
|
|
181
|
+
awaitingDescription: false,
|
|
182
|
+
pendingCatalogImage: null,
|
|
183
|
+
pendingCatalogText: null,
|
|
184
|
+
catalogSearchReady: false,
|
|
185
|
+
preCatalogResults: null,
|
|
186
|
+
relatedPartsUsed: false,
|
|
187
|
+
pendingRelatedPart: null,
|
|
188
|
+
relatedPartsTableSuggestions: [],
|
|
189
|
+
requestImageKeyword: '',
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const useConversationStore = create<ConversationState & ConversationActions>()(
|
|
193
|
+
(set, get) => ({
|
|
194
|
+
...initialState,
|
|
195
|
+
|
|
196
|
+
startNewSession: () =>
|
|
197
|
+
set(state => ({
|
|
198
|
+
...initialState,
|
|
199
|
+
isEvaluated: false,
|
|
200
|
+
sessionId: generateSessionId(),
|
|
201
|
+
isOpen: state.isOpen, // Preserve current open state across searches
|
|
202
|
+
})),
|
|
203
|
+
|
|
204
|
+
setEvaluationResult: (
|
|
205
|
+
quality,
|
|
206
|
+
route,
|
|
207
|
+
openingMessage,
|
|
208
|
+
options,
|
|
209
|
+
requestImageKeyword,
|
|
210
|
+
) => {
|
|
211
|
+
const state = get();
|
|
212
|
+
|
|
213
|
+
// Only set if not already evaluated (P2 fires once)
|
|
214
|
+
if (state.isEvaluated) return;
|
|
215
|
+
|
|
216
|
+
set({
|
|
217
|
+
isEvaluated: true,
|
|
218
|
+
quality,
|
|
219
|
+
route,
|
|
220
|
+
openingMessage,
|
|
221
|
+
openingOptions: options,
|
|
222
|
+
attemptCounter: route === 'P5' ? 1 : 0,
|
|
223
|
+
isOpen: false, // Don't auto-open — show notification badge instead
|
|
224
|
+
hasNotification: true,
|
|
225
|
+
requestImageKeyword,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Add opening message to chat
|
|
229
|
+
set(state => ({
|
|
230
|
+
messages: [
|
|
231
|
+
...state.messages,
|
|
232
|
+
{
|
|
233
|
+
id: generateId(),
|
|
234
|
+
role: 'assistant' as const,
|
|
235
|
+
content: openingMessage,
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
options,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
}));
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
addMessage: message =>
|
|
244
|
+
set(state => ({
|
|
245
|
+
messages: [
|
|
246
|
+
...state.messages,
|
|
247
|
+
{
|
|
248
|
+
...message,
|
|
249
|
+
id: generateId(),
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
})),
|
|
254
|
+
|
|
255
|
+
clearMessages: () => set({ messages: [] }),
|
|
256
|
+
|
|
257
|
+
addFilter: (key, value) =>
|
|
258
|
+
set(state => ({
|
|
259
|
+
activeFilters: [
|
|
260
|
+
...state.activeFilters.filter(f => f.key !== key),
|
|
261
|
+
{ key, value },
|
|
262
|
+
],
|
|
263
|
+
})),
|
|
264
|
+
|
|
265
|
+
removeFilter: key =>
|
|
266
|
+
set(state => ({
|
|
267
|
+
activeFilters: state.activeFilters.filter(f => f.key !== key),
|
|
268
|
+
})),
|
|
269
|
+
|
|
270
|
+
clearFilters: () => set({ activeFilters: [] }),
|
|
271
|
+
|
|
272
|
+
incrementAttempt: () =>
|
|
273
|
+
set(state => ({
|
|
274
|
+
attemptCounter: Math.min(state.attemptCounter + 1, 3),
|
|
275
|
+
})),
|
|
276
|
+
|
|
277
|
+
setIsOpen: isOpen =>
|
|
278
|
+
set({ isOpen, ...(isOpen ? { hasNotification: false } : {}) }),
|
|
279
|
+
clearNotification: () => set({ hasNotification: false }),
|
|
280
|
+
setIsLoading: isLoading => set({ isLoading }),
|
|
281
|
+
setError: error => set({ error }),
|
|
282
|
+
setAwaitingDescription: awaiting => set({ awaitingDescription: awaiting }),
|
|
283
|
+
setPendingCatalogImage: file => set({ pendingCatalogImage: file }),
|
|
284
|
+
setPendingCatalogText: text => set({ pendingCatalogText: text }),
|
|
285
|
+
setCatalogSearchReady: ready => set({ catalogSearchReady: ready }),
|
|
286
|
+
setPreCatalogResults: results => set({ preCatalogResults: results }),
|
|
287
|
+
setRelatedPartsTableSuggestions: suggestions =>
|
|
288
|
+
set({ relatedPartsTableSuggestions: suggestions }),
|
|
289
|
+
setRelatedPartsUsed: used => set({ relatedPartsUsed: used }),
|
|
290
|
+
setPendingRelatedPart: part => set({ pendingRelatedPart: part }),
|
|
291
|
+
|
|
292
|
+
reset: () =>
|
|
293
|
+
set({
|
|
294
|
+
...initialState,
|
|
295
|
+
sessionId: generateSessionId(),
|
|
296
|
+
}),
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
export default useConversationStore;
|
|
@@ -4,8 +4,8 @@ import { initialState } from './misc.initialstate';
|
|
|
4
4
|
|
|
5
5
|
const miscSlice: StateCreator<MiscState & MiscAction> = set => ({
|
|
6
6
|
...initialState,
|
|
7
|
-
setMetaFilter: filter => set(
|
|
8
|
-
setGroundingQuery: query => set(
|
|
7
|
+
setMetaFilter: filter => set(() => ({ metaFilter: filter })),
|
|
8
|
+
setGroundingQuery: query => set(() => ({ groundingQuery: query })),
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
export default miscSlice;
|