@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.
Files changed (106) hide show
  1. package/build/asset-manifest.json +6 -6
  2. package/build/data/related-parts.json +83 -0
  3. package/build/index.html +1 -1
  4. package/build/js/settings.example.js +3 -0
  5. package/build/static/css/main.5ea01690.css +4 -0
  6. package/build/static/css/main.5ea01690.css.map +1 -0
  7. package/build/static/js/main.36b77705.js +3 -0
  8. package/build/static/js/{main.f2255597.js.map → main.36b77705.js.map} +1 -1
  9. package/package.json +4 -3
  10. package/public/data/related-parts.json +83 -0
  11. package/public/js/settings.example.js +3 -0
  12. package/src/App.test.tsx +0 -1
  13. package/src/App.tsx +0 -1
  14. package/src/assets/arrow_down_expanded.svg +3 -0
  15. package/src/assets/arrow_enter.svg +3 -0
  16. package/src/assets/camera.svg +3 -0
  17. package/src/assets/close.svg +3 -0
  18. package/src/assets/enter.svg +3 -0
  19. package/src/assets/refresh.svg +3 -0
  20. package/src/assets/vizo_avatar.svg +16 -0
  21. package/src/components/Cadenas/CadenasWebViewer.tsx +1 -1
  22. package/src/components/Cart.tsx +48 -36
  23. package/src/components/ChatAssistant/ChatAssistant.tsx +289 -0
  24. package/src/components/ChatAssistant/MobileChatAssistant.tsx +291 -0
  25. package/src/components/ChatAssistant/OptionChip.tsx +78 -0
  26. package/src/components/ChatAssistant/index.ts +3 -0
  27. package/src/components/ChatAssistant/useChatAssistantLogic.ts +745 -0
  28. package/src/components/CurrentRefinements.tsx +2 -2
  29. package/src/components/CustomCameraDrawer.tsx +56 -13
  30. package/src/components/DragDropFile.tsx +5 -5
  31. package/src/components/ExperienceVisualSearch/ExperienceVisualSearch.tsx +1 -1
  32. package/src/components/Header.tsx +116 -96
  33. package/src/components/Hint.tsx +1 -2
  34. package/src/components/HitsPerPage.tsx +9 -3
  35. package/src/components/ImagePreview.tsx +32 -17
  36. package/src/components/ImageUpload.tsx +16 -8
  37. package/src/components/Inquiry/InquiryBanner.tsx +1 -1
  38. package/src/components/Inquiry/InquiryModal.tsx +35 -29
  39. package/src/components/ItemSpecification.tsx +58 -126
  40. package/src/components/LocationInfoPopup.tsx +33 -33
  41. package/src/components/MatchNotificationBanner.tsx +90 -36
  42. package/src/components/PostFilter/PostFilter.tsx +1 -1
  43. package/src/components/PostFilter/PostFilterComponent.tsx +0 -1
  44. package/src/components/PostFilter/PostFilterFindApi.tsx +0 -1
  45. package/src/components/PoweredBy.tsx +1 -1
  46. package/src/components/PreFilter/PreFilter.tsx +14 -3
  47. package/src/components/PreFilter/PreFilterModal.tsx +0 -1
  48. package/src/components/Product/Product.tsx +15 -11
  49. package/src/components/Product/ProductAttribute.tsx +4 -5
  50. package/src/components/Product/ProductDetailViewModal.tsx +2 -4
  51. package/src/components/Product/ProductList.tsx +26 -13
  52. package/src/components/Rfq/RfqModal.tsx +1 -1
  53. package/src/components/SidePanel.tsx +124 -91
  54. package/src/components/SmartFilter.tsx +320 -0
  55. package/src/components/TextSearch.tsx +134 -70
  56. package/src/components/UploadDisclaimer.tsx +1 -1
  57. package/src/hooks/useBadResultsRecovery.ts +407 -0
  58. package/src/hooks/useEffectiveGroundingResults.ts +54 -0
  59. package/src/hooks/useGoodResultsChat.ts +651 -0
  60. package/src/hooks/useGroundedSearch.ts +88 -0
  61. package/src/hooks/useImageSearch.ts +139 -187
  62. package/src/hooks/useResultEvaluator.ts +417 -0
  63. package/src/index.css +1 -1
  64. package/src/index.tsx +0 -1
  65. package/src/layouts/AppLayout.tsx +53 -2
  66. package/src/pages/Home.tsx +11 -52
  67. package/src/pages/Login.tsx +1 -2
  68. package/src/pages/Logout.tsx +1 -1
  69. package/src/pages/Result.tsx +198 -200
  70. package/src/providers/AuthProvider.tsx +0 -1
  71. package/src/services/Feedback.ts +1 -1
  72. package/src/services/visualSearch.ts +0 -21
  73. package/src/services/vizo.ts +192 -4
  74. package/src/stores/chat/chatStore.ts +150 -0
  75. package/src/stores/chat/conversationStore.ts +300 -0
  76. package/src/stores/request/Misc/misc.slice.ts +2 -2
  77. package/src/stores/request/filter/filter.slice.ts +8 -8
  78. package/src/stores/request/query/query.slice.ts +2 -2
  79. package/src/stores/request/requestImage/requestImage.slice.ts +6 -6
  80. package/src/stores/request/specifications/specifications.slice.ts +10 -7
  81. package/src/stores/result/detectedRegions/detectedRegions.slice.ts +1 -1
  82. package/src/stores/result/prodcuts/products.initialState.ts +12 -0
  83. package/src/stores/result/prodcuts/products.slice.ts +28 -8
  84. package/src/stores/result/session/session.slice.ts +2 -2
  85. package/src/stores/smartFilters/smartFiltersStore.ts +270 -0
  86. package/src/stores/types.ts +41 -0
  87. package/src/stores/ui/ai/ai.initialState.ts +5 -0
  88. package/src/stores/ui/ai/ai.slice.ts +15 -0
  89. package/src/stores/ui/banner/banner.initialState.ts +6 -0
  90. package/src/stores/ui/banner/banner.slice.ts +14 -0
  91. package/src/stores/ui/feedback/feedback.slice.ts +1 -1
  92. package/src/stores/ui/loading/loading.slice.ts +4 -4
  93. package/src/stores/ui/uiStore.ts +7 -1
  94. package/src/styles/product.scss +0 -2
  95. package/src/types.ts +3 -7
  96. package/src/utils/cropImageToBase64.ts +32 -0
  97. package/src/utils/fetchProductImage.ts +109 -0
  98. package/src/utils/imageConverters.ts +124 -0
  99. package/src/utils/relatedParts.ts +35 -0
  100. package/src/utils/specificationFilter.ts +1 -5
  101. package/tailwind.config.js +3 -2
  102. package/build/static/css/main.734b52e1.css +0 -4
  103. package/build/static/css/main.734b52e1.css.map +0 -1
  104. package/build/static/js/main.f2255597.js +0 -3
  105. package/src/utils/addAssets.ts +0 -40
  106. /package/build/static/js/{main.f2255597.js.LICENSE.txt → main.36b77705.js.LICENSE.txt} +0 -0
@@ -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 `[${index}] SKU: ${sku} | Brand: ${brand} | ${title} | ${desc}${idStr}`;
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
- apiKey: string,
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
- imageBlobOrFile = await canvasToBlob(imageInput);
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(state => ({ metaFilter: filter })),
8
- setGroundingQuery: query => set(state => ({ groundingQuery: query })),
7
+ setMetaFilter: filter => set(() => ({ metaFilter: filter })),
8
+ setGroundingQuery: query => set(() => ({ groundingQuery: query })),
9
9
  });
10
10
 
11
11
  export default miscSlice;