orchid-ai 1.0.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/README.md +225 -0
- package/dist/components/ChatPanel.d.ts +123 -0
- package/dist/components/Conversation.d.ts +75 -0
- package/dist/components/ErrorBoundary.d.ts +16 -0
- package/dist/components/Icon.d.ts +84 -0
- package/dist/components/ModelSwitcher.d.ts +24 -0
- package/dist/components/SuggestionsPanel.d.ts +27 -0
- package/dist/constants.d.ts +353 -0
- package/dist/hooks/useAiMerge.d.ts +20 -0
- package/dist/hooks/useModelSwitcher.d.ts +65 -0
- package/dist/hooks/useStreamingAI.d.ts +29 -0
- package/dist/hooks/useSuggestions.d.ts +48 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.esm.js +3385 -0
- package/dist/index.js +3400 -0
- package/dist/server/components/ChatPanel.d.ts +123 -0
- package/dist/server/components/Conversation.d.ts +75 -0
- package/dist/server/components/ErrorBoundary.d.ts +16 -0
- package/dist/server/components/Icon.d.ts +84 -0
- package/dist/server/components/ModelSwitcher.d.ts +24 -0
- package/dist/server/components/SuggestionsPanel.d.ts +27 -0
- package/dist/server/constants.d.ts +353 -0
- package/dist/server/contextual-service.d.ts +59 -0
- package/dist/server/document-processor.d.ts +60 -0
- package/dist/server/hooks/useAiMerge.d.ts +20 -0
- package/dist/server/hooks/useModelSwitcher.d.ts +65 -0
- package/dist/server/hooks/useStreamingAI.d.ts +29 -0
- package/dist/server/hooks/useSuggestions.d.ts +48 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.esm.js +14008 -0
- package/dist/server/index.js +14019 -0
- package/dist/server/server/contextual-service.d.ts +59 -0
- package/dist/server/server/document-processor.d.ts +60 -0
- package/dist/server/server/index.d.ts +7 -0
- package/dist/server/server/server.d.ts +32 -0
- package/dist/server/server/service.d.ts +267 -0
- package/dist/server/server/training-schema.d.ts +17 -0
- package/dist/server/server/training.d.ts +231 -0
- package/dist/server/server/utils.d.ts +209 -0
- package/dist/server/server.d.ts +32 -0
- package/dist/server/service.d.ts +267 -0
- package/dist/server/training-schema.d.ts +17 -0
- package/dist/server/training.d.ts +231 -0
- package/dist/server/types/types.d.ts +481 -0
- package/dist/server/utils/fileHandler.d.ts +20 -0
- package/dist/server/utils/mergeWithAi.d.ts +19 -0
- package/dist/server/utils.d.ts +209 -0
- package/dist/types/types.d.ts +481 -0
- package/dist/utils/fileHandler.d.ts +20 -0
- package/dist/utils/mergeWithAi.d.ts +19 -0
- package/dist/utils.d.ts +19 -0
- package/package.json +137 -0
|
@@ -0,0 +1,3385 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect, Component, useRef } from 'react';
|
|
2
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
service: 'claude',
|
|
6
|
+
model: 'claude-sonnet-4-20250514',
|
|
7
|
+
temperature: 0.7,
|
|
8
|
+
maxTokens: 4096,
|
|
9
|
+
chatLevel: 'none',
|
|
10
|
+
supportsImages: true,
|
|
11
|
+
};
|
|
12
|
+
// Default model configurations with compute weights
|
|
13
|
+
const DEFAULT_MODELS = {
|
|
14
|
+
openai: [
|
|
15
|
+
{
|
|
16
|
+
id: 'gpt-4-turbo',
|
|
17
|
+
name: 'GPT-4 Turbo',
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
available: true,
|
|
20
|
+
supportsImages: true,
|
|
21
|
+
computeWeight: 1.0,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'gpt-4o',
|
|
25
|
+
name: 'GPT-4o',
|
|
26
|
+
provider: 'openai',
|
|
27
|
+
available: true,
|
|
28
|
+
supportsImages: true,
|
|
29
|
+
computeWeight: 0.8,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'gpt-4o-mini',
|
|
33
|
+
name: 'GPT-4o Mini',
|
|
34
|
+
provider: 'openai',
|
|
35
|
+
available: true,
|
|
36
|
+
supportsImages: true,
|
|
37
|
+
computeWeight: 0.3,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
claude: [
|
|
41
|
+
{
|
|
42
|
+
id: 'claude-opus-4-20250514',
|
|
43
|
+
name: 'Claude Opus 4',
|
|
44
|
+
provider: 'claude',
|
|
45
|
+
available: true,
|
|
46
|
+
supportsImages: true,
|
|
47
|
+
computeWeight: 1.2,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'claude-sonnet-4-20250514',
|
|
51
|
+
name: 'Claude Sonnet 4',
|
|
52
|
+
provider: 'claude',
|
|
53
|
+
available: true,
|
|
54
|
+
supportsImages: true,
|
|
55
|
+
computeWeight: 0.6,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'claude-3-5-haiku-20241022',
|
|
59
|
+
name: 'Claude Haiku 3.5',
|
|
60
|
+
provider: 'claude',
|
|
61
|
+
available: true,
|
|
62
|
+
supportsImages: true,
|
|
63
|
+
computeWeight: 0.2,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
gemini: [
|
|
67
|
+
{
|
|
68
|
+
id: 'gemini-2.5-flash-lite',
|
|
69
|
+
name: 'Gemini 2.5 Flash Lite',
|
|
70
|
+
provider: 'gemini',
|
|
71
|
+
available: true,
|
|
72
|
+
supportsImages: true,
|
|
73
|
+
computeWeight: 0.2,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'gemini-2.5-flash',
|
|
77
|
+
name: 'Gemini 2.5 Flash',
|
|
78
|
+
provider: 'gemini',
|
|
79
|
+
available: true,
|
|
80
|
+
supportsImages: true,
|
|
81
|
+
computeWeight: 0.5,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'gemini-2.5-pro',
|
|
85
|
+
name: 'Gemini 2.5 Pro',
|
|
86
|
+
provider: 'gemini',
|
|
87
|
+
available: true,
|
|
88
|
+
supportsImages: true,
|
|
89
|
+
computeWeight: 1.0,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
const defaultTheme = {
|
|
94
|
+
// Default blue theme
|
|
95
|
+
primary: '#3b82f6',
|
|
96
|
+
primaryHover: '#2563eb',
|
|
97
|
+
primaryLight: '#eff6ff',
|
|
98
|
+
secondary: '#6b7280',
|
|
99
|
+
secondaryHover: '#4b5563',
|
|
100
|
+
background: '#ffffff',
|
|
101
|
+
backgroundSecondary: '#f9fafb',
|
|
102
|
+
backgroundTertiary: '#f3f4f6',
|
|
103
|
+
textPrimary: '#111827',
|
|
104
|
+
textSecondary: '#6b7280',
|
|
105
|
+
textTertiary: '#9ca3af',
|
|
106
|
+
textInverse: '#ffffff',
|
|
107
|
+
borderPrimary: '#e5e7eb',
|
|
108
|
+
borderSecondary: '#d1d5db',
|
|
109
|
+
success: '#22c55e',
|
|
110
|
+
warning: '#f59e0b',
|
|
111
|
+
error: '#ef4444',
|
|
112
|
+
info: '#3b82f6',
|
|
113
|
+
};
|
|
114
|
+
// Default dark theme
|
|
115
|
+
const defaultDarkTheme = {
|
|
116
|
+
colors: {
|
|
117
|
+
primary: {
|
|
118
|
+
50: 'rgb(239 246 255)', // blue-50
|
|
119
|
+
100: 'rgb(219 234 254)', // blue-100
|
|
120
|
+
400: 'rgb(96 165 250)', // blue-400
|
|
121
|
+
500: 'rgb(59 130 246)', // blue-500
|
|
122
|
+
600: 'rgb(37 99 235)', // blue-600
|
|
123
|
+
700: 'rgb(29 78 216)', // blue-700
|
|
124
|
+
800: 'rgb(30 64 175)', // blue-800
|
|
125
|
+
900: 'rgb(30 58 138)', // blue-900
|
|
126
|
+
},
|
|
127
|
+
background: {
|
|
128
|
+
primary: 'rgb(17 24 39)', // gray-900
|
|
129
|
+
secondary: 'rgb(31 41 55)', // gray-800
|
|
130
|
+
tertiary: 'rgb(55 65 81)', // gray-700
|
|
131
|
+
},
|
|
132
|
+
text: {
|
|
133
|
+
primary: 'rgb(249 250 251)', // gray-50
|
|
134
|
+
secondary: 'rgb(209 213 219)', // gray-300
|
|
135
|
+
tertiary: 'rgb(156 163 175)', // gray-400
|
|
136
|
+
inverse: 'rgb(17 24 39)', // gray-900
|
|
137
|
+
},
|
|
138
|
+
border: {
|
|
139
|
+
primary: 'rgb(55 65 81)', // gray-700
|
|
140
|
+
secondary: 'rgb(75 85 99)', // gray-600
|
|
141
|
+
},
|
|
142
|
+
surface: {
|
|
143
|
+
primary: 'rgb(31 41 55)', // gray-800
|
|
144
|
+
secondary: 'rgb(55 65 81)', // gray-700
|
|
145
|
+
tertiary: 'rgb(75 85 99)', // gray-600
|
|
146
|
+
elevated: 'rgb(17 24 39)', // gray-900
|
|
147
|
+
},
|
|
148
|
+
state: {
|
|
149
|
+
error: {
|
|
150
|
+
background: 'rgb(127 29 29 / 0.2)', // red-900/20
|
|
151
|
+
border: 'rgb(153 27 27)', // red-800
|
|
152
|
+
text: 'rgb(248 113 113)', // red-400
|
|
153
|
+
},
|
|
154
|
+
warning: {
|
|
155
|
+
background: 'rgb(146 64 14 / 0.2)', // amber-900/20
|
|
156
|
+
border: 'rgb(180 83 9)', // amber-800
|
|
157
|
+
text: 'rgb(251 191 36)', // amber-400
|
|
158
|
+
},
|
|
159
|
+
success: {
|
|
160
|
+
background: 'rgb(20 83 45 / 0.2)', // green-900/20
|
|
161
|
+
border: 'rgb(22 101 52)', // green-800
|
|
162
|
+
text: 'rgb(74 222 128)', // green-400
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
spacing: {
|
|
167
|
+
xs: '0.25rem', // 1
|
|
168
|
+
sm: '0.5rem', // 2
|
|
169
|
+
md: '1rem', // 4
|
|
170
|
+
lg: '1.5rem', // 6
|
|
171
|
+
xl: '2rem', // 8
|
|
172
|
+
},
|
|
173
|
+
borderRadius: {
|
|
174
|
+
sm: '0.25rem',
|
|
175
|
+
md: '0.375rem',
|
|
176
|
+
lg: '0.5rem',
|
|
177
|
+
full: '9999px',
|
|
178
|
+
},
|
|
179
|
+
shadows: {
|
|
180
|
+
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
181
|
+
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
182
|
+
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
183
|
+
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
// Default light theme
|
|
187
|
+
const defaultLightTheme = {
|
|
188
|
+
colors: {
|
|
189
|
+
primary: {
|
|
190
|
+
50: 'rgb(239 246 255)', // blue-50
|
|
191
|
+
100: 'rgb(219 234 254)', // blue-100
|
|
192
|
+
400: 'rgb(96 165 250)', // blue-400
|
|
193
|
+
500: 'rgb(59 130 246)', // blue-500
|
|
194
|
+
600: 'rgb(37 99 235)', // blue-600
|
|
195
|
+
700: 'rgb(29 78 216)', // blue-700
|
|
196
|
+
800: 'rgb(30 64 175)', // blue-800
|
|
197
|
+
900: 'rgb(30 58 138)', // blue-900
|
|
198
|
+
},
|
|
199
|
+
background: {
|
|
200
|
+
primary: 'rgb(255 255 255)', // white
|
|
201
|
+
secondary: 'rgb(249 250 251)', // gray-50
|
|
202
|
+
tertiary: 'rgb(243 244 246)', // gray-100
|
|
203
|
+
},
|
|
204
|
+
text: {
|
|
205
|
+
primary: 'rgb(17 24 39)', // gray-900
|
|
206
|
+
secondary: 'rgb(75 85 99)', // gray-600
|
|
207
|
+
tertiary: 'rgb(107 114 128)', // gray-500
|
|
208
|
+
inverse: 'rgb(255 255 255)', // white
|
|
209
|
+
},
|
|
210
|
+
border: {
|
|
211
|
+
primary: 'rgb(229 231 235)', // gray-200
|
|
212
|
+
secondary: 'rgb(209 213 219)', // gray-300
|
|
213
|
+
},
|
|
214
|
+
surface: {
|
|
215
|
+
primary: 'rgb(249 250 251)', // gray-50
|
|
216
|
+
secondary: 'rgb(243 244 246)', // gray-100
|
|
217
|
+
tertiary: 'rgb(229 231 235)', // gray-200
|
|
218
|
+
elevated: 'rgb(255 255 255)', // white
|
|
219
|
+
},
|
|
220
|
+
state: {
|
|
221
|
+
error: {
|
|
222
|
+
background: 'rgb(254 242 242)', // red-50
|
|
223
|
+
border: 'rgb(252 165 165)', // red-200
|
|
224
|
+
text: 'rgb(153 27 27)', // red-800
|
|
225
|
+
},
|
|
226
|
+
warning: {
|
|
227
|
+
background: 'rgb(255 251 235)', // amber-50
|
|
228
|
+
border: 'rgb(252 211 77)', // amber-200
|
|
229
|
+
text: 'rgb(146 64 14)', // amber-800
|
|
230
|
+
},
|
|
231
|
+
success: {
|
|
232
|
+
background: 'rgb(240 253 244)', // green-50
|
|
233
|
+
border: 'rgb(167 243 208)', // green-200
|
|
234
|
+
text: 'rgb(22 101 52)', // green-800
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
spacing: {
|
|
239
|
+
xs: '0.25rem',
|
|
240
|
+
sm: '0.5rem',
|
|
241
|
+
md: '1rem',
|
|
242
|
+
lg: '1.5rem',
|
|
243
|
+
xl: '2rem',
|
|
244
|
+
},
|
|
245
|
+
borderRadius: {
|
|
246
|
+
sm: '0.25rem',
|
|
247
|
+
md: '0.375rem',
|
|
248
|
+
lg: '0.5rem',
|
|
249
|
+
full: '9999px',
|
|
250
|
+
},
|
|
251
|
+
shadows: {
|
|
252
|
+
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
|
253
|
+
md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
|
|
254
|
+
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
|
255
|
+
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
function useStreamingAI({ userId, serverConfig, formData, chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId, modelSelection, onUsageTracked, chatLevel, additionalContext, verbose = false, }) {
|
|
260
|
+
// Map of chatId -> chat array
|
|
261
|
+
const [chats, setChats] = useState({});
|
|
262
|
+
const [currentChatId, setCurrentChatId] = useState(() => Date.now().toString());
|
|
263
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
264
|
+
const chatsToUse = useMemo(() => (externalChats !== undefined ? externalChats : chats), [externalChats, chats]);
|
|
265
|
+
const setChatsToUse = useMemo(() => (externalSetChats !== undefined ? externalSetChats : setChats), [externalSetChats, setChats]);
|
|
266
|
+
const currentChatIdToUse = useMemo(() => externalCurrentChatId !== undefined
|
|
267
|
+
? externalCurrentChatId
|
|
268
|
+
: currentChatId, [externalCurrentChatId, currentChatId]);
|
|
269
|
+
const setCurrentChatIdToUse = useMemo(() => externalSetCurrentChatId !== undefined
|
|
270
|
+
? externalSetCurrentChatId
|
|
271
|
+
: setCurrentChatId, [externalSetCurrentChatId, setCurrentChatId]);
|
|
272
|
+
const log = (...args) => {
|
|
273
|
+
if (verbose) {
|
|
274
|
+
console.log(...args);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
// Helper function to build URL from config
|
|
278
|
+
const buildUrlFromConfig = (config) => {
|
|
279
|
+
const protocol = config.secure !== undefined
|
|
280
|
+
? config.secure
|
|
281
|
+
? 'https:'
|
|
282
|
+
: 'http:'
|
|
283
|
+
: window.location.protocol;
|
|
284
|
+
const hostname = config.domain || window.location.hostname;
|
|
285
|
+
const port = config.port
|
|
286
|
+
? `:${config.port}`
|
|
287
|
+
: window.location.port
|
|
288
|
+
? `:${window.location.port}`
|
|
289
|
+
: '';
|
|
290
|
+
const suffix = config.suffix || '';
|
|
291
|
+
// If no domain specified, use relative URL (common with webpack proxy)
|
|
292
|
+
if (!config.domain && !config.port && !config.secure) {
|
|
293
|
+
return suffix;
|
|
294
|
+
}
|
|
295
|
+
return `${protocol}//${hostname}${port}${suffix}`;
|
|
296
|
+
};
|
|
297
|
+
// Helper to get current chat
|
|
298
|
+
const chat = chatsToUse[currentChatIdToUse] || [];
|
|
299
|
+
// Final cleanup function to fix any remaining formatting issues
|
|
300
|
+
const finalCleanup = (text) => {
|
|
301
|
+
return (text
|
|
302
|
+
// Fix spaces in email addresses (e.g., "max.ad ams" -> "max.adams")
|
|
303
|
+
.replace(/(\w+)\.(\w+)\s+(\w+)/g, '$1.$2$3')
|
|
304
|
+
// Fix missing spaces between words (e.g., "Company Corp Phone" -> "Company Corp Phone")
|
|
305
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
306
|
+
// Remove the word "json" when it appears at the end of sentences
|
|
307
|
+
.replace(/\s+json\s*$/g, '')
|
|
308
|
+
.replace(/\s+json\s*\./g, '.')
|
|
309
|
+
// Fix any remaining spacing issues
|
|
310
|
+
.replace(/\s+/g, ' ')
|
|
311
|
+
.trim());
|
|
312
|
+
};
|
|
313
|
+
const sendQuery = (query, files, context) => {
|
|
314
|
+
// Allow sending if there's either text OR files
|
|
315
|
+
if (!query.trim() && (!files || files.length === 0)) {
|
|
316
|
+
log('❌ [CLIENT] Nothing to send - no text and no files');
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
// Ensure we have a query when sending files
|
|
320
|
+
const finalQuery = query.trim()
|
|
321
|
+
? query.trim()
|
|
322
|
+
: files && files.length > 0
|
|
323
|
+
? 'Analyze the uploaded content and suggest relevant actions.'
|
|
324
|
+
: query;
|
|
325
|
+
const current = chatsToUse[currentChatIdToUse] || [];
|
|
326
|
+
const newMessages = [
|
|
327
|
+
...current,
|
|
328
|
+
{
|
|
329
|
+
sender: 'user',
|
|
330
|
+
content: [{ type: 'text', content: finalQuery }],
|
|
331
|
+
timestamp: Date.now(),
|
|
332
|
+
files: files,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
sender: 'ai',
|
|
336
|
+
content: [],
|
|
337
|
+
isLoading: true,
|
|
338
|
+
aiStatus: 'thinking',
|
|
339
|
+
timestamp: Date.now(),
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
setChatsToUse((prev) => ({
|
|
343
|
+
...prev,
|
|
344
|
+
[currentChatIdToUse]: newMessages,
|
|
345
|
+
}));
|
|
346
|
+
setIsLoading(true);
|
|
347
|
+
// Get current chat history (excluding the new messages we just added)
|
|
348
|
+
const chatHistory = current.map((msg) => ({
|
|
349
|
+
role: msg.sender === 'user' ? 'user' : 'assistant',
|
|
350
|
+
content: msg.content
|
|
351
|
+
.filter((block) => block.type === 'text')
|
|
352
|
+
.map((block) => block.content)
|
|
353
|
+
.join('\n'),
|
|
354
|
+
}));
|
|
355
|
+
// Build API URL from config with sensible default
|
|
356
|
+
const apiUrl = serverConfig ? buildUrlFromConfig(serverConfig) : '/api';
|
|
357
|
+
// Always use POST approach now - server will handle all file processing
|
|
358
|
+
if (files && files.length > 0) {
|
|
359
|
+
sendWithFiles(finalQuery, files, chatHistory, apiUrl, context);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
// For text-only, we'll still use POST for consistency
|
|
363
|
+
sendWithFiles(finalQuery, [], chatHistory, apiUrl, context);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
// Unified POST approach - let server handle all file processing
|
|
367
|
+
const sendWithFiles = async (query, files, chatHistory, apiUrl, context // <-- Per-message context
|
|
368
|
+
) => {
|
|
369
|
+
try {
|
|
370
|
+
// Convert files to a format the server can process
|
|
371
|
+
const filePromises = files.map(async (file) => {
|
|
372
|
+
return new Promise((resolve, reject) => {
|
|
373
|
+
const reader = new FileReader();
|
|
374
|
+
reader.onload = () => {
|
|
375
|
+
const result = reader.result;
|
|
376
|
+
const base64Data = result.split(',')[1]; // Remove data URL prefix
|
|
377
|
+
resolve({
|
|
378
|
+
name: file.name,
|
|
379
|
+
type: file.type,
|
|
380
|
+
size: file.size,
|
|
381
|
+
data: base64Data,
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
reader.onerror = reject;
|
|
385
|
+
reader.readAsDataURL(file);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
const processedFiles = await Promise.all(filePromises);
|
|
389
|
+
// Extract schema, additionalContext, and stayOnPage from serverConfig
|
|
390
|
+
const schema = serverConfig?.schema;
|
|
391
|
+
const serverAdditionalContext = serverConfig?.additionalContext;
|
|
392
|
+
const stayOnPage = serverConfig?.stayOnPage;
|
|
393
|
+
// Build stay-on-page context instruction
|
|
394
|
+
const stayOnPageContext = stayOnPage
|
|
395
|
+
? 'IMPORTANT: You are embedded within a specific page. Do NOT generate navigation suggestions or routes to other pages. Focus ONLY on helping with the current form/page content. Stay on this page and do not suggest navigating elsewhere.'
|
|
396
|
+
: '';
|
|
397
|
+
// Merge all context sources (stayOnPage context takes priority)
|
|
398
|
+
const mergedContext = [
|
|
399
|
+
stayOnPageContext,
|
|
400
|
+
serverAdditionalContext || '',
|
|
401
|
+
additionalContext ? (typeof additionalContext === 'string' ? additionalContext : JSON.stringify(additionalContext)) : '',
|
|
402
|
+
context || ''
|
|
403
|
+
].filter(Boolean).join('\n\n');
|
|
404
|
+
const requestBody = {
|
|
405
|
+
query,
|
|
406
|
+
userId,
|
|
407
|
+
chatHistory,
|
|
408
|
+
chatLevel,
|
|
409
|
+
formData: formData || {},
|
|
410
|
+
files: processedFiles,
|
|
411
|
+
// New: Include model selection in request
|
|
412
|
+
modelSelection: modelSelection || {
|
|
413
|
+
provider: DEFAULT_CONFIG.service,
|
|
414
|
+
model: DEFAULT_CONFIG.model,
|
|
415
|
+
capabilities: {
|
|
416
|
+
supportsImages: DEFAULT_CONFIG.supportsImages,
|
|
417
|
+
computeWeight: 1.0,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
...(schema ? { schema } : {}), // <-- NEW: Include schema from serverConfig!
|
|
421
|
+
...(mergedContext ? { additionalContext: mergedContext } : {}), // <-- Include merged context
|
|
422
|
+
};
|
|
423
|
+
// Use EventSource-style approach with POST for streaming (bypasses webpack proxy issues)
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
const xhr = new XMLHttpRequest();
|
|
426
|
+
xhr.open('POST', `${apiUrl}/suggest`, true);
|
|
427
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
428
|
+
xhr.setRequestHeader('Accept', 'text/event-stream');
|
|
429
|
+
xhr.setRequestHeader('Cache-Control', 'no-cache');
|
|
430
|
+
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
431
|
+
let buffer = '';
|
|
432
|
+
let isProcessing = false;
|
|
433
|
+
let processedLines = new Set(); // Track processed lines to prevent duplication
|
|
434
|
+
xhr.onreadystatechange = async () => {
|
|
435
|
+
if (xhr.readyState === 3) {
|
|
436
|
+
// LOADING - receiving data
|
|
437
|
+
if (isProcessing)
|
|
438
|
+
return; // Prevent overlapping processing
|
|
439
|
+
isProcessing = true;
|
|
440
|
+
const newData = xhr.responseText.substring(buffer.length);
|
|
441
|
+
buffer = xhr.responseText;
|
|
442
|
+
if (newData) {
|
|
443
|
+
// Process each line immediately as it arrives
|
|
444
|
+
const lines = newData.split('\n');
|
|
445
|
+
for (const line of lines) {
|
|
446
|
+
// Skip if we've already processed this line
|
|
447
|
+
if (processedLines.has(line))
|
|
448
|
+
continue;
|
|
449
|
+
processedLines.add(line);
|
|
450
|
+
if (line.startsWith('data: ')) {
|
|
451
|
+
try {
|
|
452
|
+
const data = JSON.parse(line.slice(6));
|
|
453
|
+
await handleStreamData(data);
|
|
454
|
+
// Force immediate UI update
|
|
455
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
456
|
+
}
|
|
457
|
+
catch (e) {
|
|
458
|
+
console.warn('Failed to parse SSE data:', line);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (line.startsWith('event: end')) {
|
|
462
|
+
handleStreamEnd();
|
|
463
|
+
resolve();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
else if (line.startsWith('event: error')) {
|
|
467
|
+
const errorLine = lines.find((l) => l.startsWith('data: '));
|
|
468
|
+
if (errorLine) {
|
|
469
|
+
const errorData = JSON.parse(errorLine.slice(6));
|
|
470
|
+
handleStreamError(errorData);
|
|
471
|
+
}
|
|
472
|
+
reject(new Error('Stream error'));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
isProcessing = false;
|
|
478
|
+
}
|
|
479
|
+
else if (xhr.readyState === 4) {
|
|
480
|
+
// DONE - This is crucial for completion detection
|
|
481
|
+
// Process any remaining data in buffer (only unprocessed lines)
|
|
482
|
+
if (buffer) {
|
|
483
|
+
const lines = buffer.split('\n');
|
|
484
|
+
for (const line of lines) {
|
|
485
|
+
// Skip if we've already processed this line
|
|
486
|
+
if (processedLines.has(line))
|
|
487
|
+
continue;
|
|
488
|
+
processedLines.add(line);
|
|
489
|
+
if (line.startsWith('data: ')) {
|
|
490
|
+
try {
|
|
491
|
+
const data = JSON.parse(line.slice(6));
|
|
492
|
+
await handleStreamData(data);
|
|
493
|
+
}
|
|
494
|
+
catch (e) {
|
|
495
|
+
console.warn('Failed to parse final SSE data:', line);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Always ensure loading state is cleared when XHR completes
|
|
501
|
+
setIsLoading(false);
|
|
502
|
+
// Also clear the message-level loading state
|
|
503
|
+
setChatsToUse((prev) => {
|
|
504
|
+
const currentChat = prev[currentChatIdToUse] || [];
|
|
505
|
+
const lastIdx = currentChat.length - 1;
|
|
506
|
+
const lastMsg = currentChat[lastIdx];
|
|
507
|
+
if (!lastMsg || lastMsg.sender !== 'ai')
|
|
508
|
+
return prev;
|
|
509
|
+
return {
|
|
510
|
+
...prev,
|
|
511
|
+
[currentChatIdToUse]: [
|
|
512
|
+
...currentChat.slice(0, lastIdx),
|
|
513
|
+
{
|
|
514
|
+
...lastMsg,
|
|
515
|
+
isLoading: false,
|
|
516
|
+
aiStatus: 'none',
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
522
|
+
resolve();
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
reject(new Error(`HTTP error! status: ${xhr.status}`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
xhr.onerror = () => {
|
|
530
|
+
console.error('XHR error:', xhr.statusText);
|
|
531
|
+
handleStreamError({
|
|
532
|
+
message: xhr.statusText || 'Network error',
|
|
533
|
+
type: 'connection_error',
|
|
534
|
+
});
|
|
535
|
+
reject(new Error('Network error'));
|
|
536
|
+
};
|
|
537
|
+
// Add a timeout fallback to ensure loading state is cleared
|
|
538
|
+
const timeoutId = setTimeout(() => {
|
|
539
|
+
setIsLoading(false);
|
|
540
|
+
}, 10000); // 10 second timeout
|
|
541
|
+
xhr.send(JSON.stringify(requestBody));
|
|
542
|
+
// Clear timeout when request completes
|
|
543
|
+
const originalResolve = resolve;
|
|
544
|
+
resolve = () => {
|
|
545
|
+
clearTimeout(timeoutId);
|
|
546
|
+
originalResolve();
|
|
547
|
+
};
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
console.error('Error sending request:', error);
|
|
552
|
+
handleStreamError({
|
|
553
|
+
message: error.message || 'Unknown error',
|
|
554
|
+
type: 'connection_error',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
// Common stream data handler
|
|
559
|
+
const handleStreamData = async (data) => {
|
|
560
|
+
setChatsToUse((prev) => {
|
|
561
|
+
const currentChat = prev[currentChatIdToUse] || [];
|
|
562
|
+
const lastIdx = currentChat.length - 1;
|
|
563
|
+
const lastMsg = currentChat[lastIdx];
|
|
564
|
+
if (!lastMsg || lastMsg.sender !== 'ai')
|
|
565
|
+
return prev;
|
|
566
|
+
let updatedContent = [...(lastMsg.content || [])];
|
|
567
|
+
// Handle text content (simplified - backend handles JSON detection)
|
|
568
|
+
if (data.text) {
|
|
569
|
+
log('📥 [useStreamingAI] Raw text received:', JSON.stringify(data.text));
|
|
570
|
+
// Debug: Log character analysis
|
|
571
|
+
log(`🔍 [useStreamingAI] Character analysis of received text: "${data.text}"`);
|
|
572
|
+
log(`🔍 [useStreamingAI] Length: ${data.text.length}, Contains spaces: ${data.text.includes(' ')}, Contains newlines: ${data.text.includes('\n')}`);
|
|
573
|
+
// Debug: Check for 'd' characters specifically
|
|
574
|
+
if (data.text.includes('d')) {
|
|
575
|
+
// Log each 'd' character with its context
|
|
576
|
+
for (let i = 0; i < data.text.length; i++) {
|
|
577
|
+
if (data.text[i] === 'd') {
|
|
578
|
+
const before = data.text.substring(Math.max(0, i - 2), i);
|
|
579
|
+
const after = data.text.substring(i + 1, Math.min(data.text.length, i + 3));
|
|
580
|
+
log(`🔍 [useStreamingAI] 'd' at position ${i}: "${before}[d]${after}" (char codes: ${data.text.charCodeAt(i - 1)}, ${data.text.charCodeAt(i)}, ${data.text.charCodeAt(i + 1)})`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// NO CLEANING - just use the raw text as-is
|
|
585
|
+
const rawText = data.text;
|
|
586
|
+
log('🧹 [useStreamingAI] After cleaning:', JSON.stringify(rawText));
|
|
587
|
+
// Only process if there's still content after cleaning
|
|
588
|
+
if (rawText.trim()) {
|
|
589
|
+
// If last content block is text, append to it; otherwise create new text block
|
|
590
|
+
const lastContentIdx = updatedContent.length - 1;
|
|
591
|
+
if (lastContentIdx >= 0 &&
|
|
592
|
+
updatedContent[lastContentIdx].type === 'text') {
|
|
593
|
+
const currentText = updatedContent[lastContentIdx].content;
|
|
594
|
+
// Add space between words if needed
|
|
595
|
+
let newText = currentText;
|
|
596
|
+
if (data.text.trim()) {
|
|
597
|
+
// For ChatGPT, we need to be more careful about spacing
|
|
598
|
+
// ChatGPT sends individual words with leading spaces, so we need to handle this properly
|
|
599
|
+
// SIMPLIFIED: Just use the text as-is from the backend
|
|
600
|
+
// The backend is already handling spacing correctly for each provider
|
|
601
|
+
newText = currentText + data.text;
|
|
602
|
+
log(`🔧 [useStreamingAI] Using text as-is from backend: "${currentText}" + "${data.text}"`);
|
|
603
|
+
}
|
|
604
|
+
log('📝 [useStreamingAI] Appending to existing text block:');
|
|
605
|
+
log(' Current text:', JSON.stringify(currentText));
|
|
606
|
+
log(' New text:', JSON.stringify(data.text));
|
|
607
|
+
log(' Combined text:', JSON.stringify(newText));
|
|
608
|
+
// Update the text block
|
|
609
|
+
updatedContent[lastContentIdx] = {
|
|
610
|
+
type: 'text',
|
|
611
|
+
content: newText,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
log('📝 [useStreamingAI] Creating new text block:', JSON.stringify(rawText));
|
|
616
|
+
// Create new text block
|
|
617
|
+
updatedContent.push({
|
|
618
|
+
type: 'text',
|
|
619
|
+
content: rawText,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// Handle loading suggestions (sent by backend when JSON starts)
|
|
625
|
+
if (data.type === 'loading-suggestions') {
|
|
626
|
+
log('🎯 [useStreamingAI] Received loading suggestions signal');
|
|
627
|
+
// Remove any existing loading-suggestions components
|
|
628
|
+
updatedContent = updatedContent.filter((block) => block.type !== 'loading-suggestions');
|
|
629
|
+
// Add loading suggestions component
|
|
630
|
+
updatedContent.push({
|
|
631
|
+
type: 'loading-suggestions',
|
|
632
|
+
content: data.message || 'Generating suggestions...',
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
// Handle suggestions (sent by backend when JSON is complete)
|
|
636
|
+
if (data.suggestions !== undefined) {
|
|
637
|
+
log('🎯 [useStreamingAI] Received suggestions:', data.suggestions);
|
|
638
|
+
// Remove any loading-suggestions components when we get actual suggestions
|
|
639
|
+
updatedContent = updatedContent.filter((block) => block.type !== 'loading-suggestions');
|
|
640
|
+
updatedContent.push({
|
|
641
|
+
type: 'suggestions',
|
|
642
|
+
content: data.suggestions,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// Handle JSON type - add suggestions if not already handled by text stream
|
|
646
|
+
if (data.type === 'json' &&
|
|
647
|
+
data.suggestions &&
|
|
648
|
+
data.suggestions.length > 0) {
|
|
649
|
+
// Check if we already have these suggestions (from text stream processing)
|
|
650
|
+
const hasSuggestions = updatedContent.some((block) => block.type === 'suggestions');
|
|
651
|
+
if (!hasSuggestions) {
|
|
652
|
+
// Add the suggestions to the content
|
|
653
|
+
updatedContent.push({
|
|
654
|
+
type: 'suggestions',
|
|
655
|
+
content: data.suggestions,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Determine if we should still be loading based on the data
|
|
660
|
+
const shouldBeLoading = data.aiStatus === 'typing' ||
|
|
661
|
+
data.aiStatus === 'suggesting' ||
|
|
662
|
+
data.isIntermediate ||
|
|
663
|
+
(data.text && data.aiStatus !== 'none');
|
|
664
|
+
const updatedMsg = {
|
|
665
|
+
...lastMsg,
|
|
666
|
+
content: updatedContent,
|
|
667
|
+
isLoading: shouldBeLoading,
|
|
668
|
+
aiStatus: data.aiStatus || (data.isIntermediate ? 'suggesting' : 'none'),
|
|
669
|
+
};
|
|
670
|
+
// If this is a completion signal, force loading to false
|
|
671
|
+
if (data.aiStatus === 'none' ||
|
|
672
|
+
data.usage ||
|
|
673
|
+
(data.type === 'text' && data.aiStatus === 'none')) {
|
|
674
|
+
updatedMsg.isLoading = false;
|
|
675
|
+
updatedMsg.aiStatus = 'none';
|
|
676
|
+
// Apply final cleanup to all text blocks when stream completes
|
|
677
|
+
updatedContent = updatedContent.map((block) => {
|
|
678
|
+
if (block.type === 'text') {
|
|
679
|
+
const cleanedContent = finalCleanup(block.content);
|
|
680
|
+
log('🧹 [useStreamingAI] Final cleanup applied:');
|
|
681
|
+
log(' Before:', JSON.stringify(block.content));
|
|
682
|
+
log(' After:', JSON.stringify(cleanedContent));
|
|
683
|
+
return {
|
|
684
|
+
...block,
|
|
685
|
+
content: cleanedContent,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
return block;
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
// Track usage when request completes (only for final responses)
|
|
692
|
+
if (!data.isIntermediate && data.usage && onUsageTracked) {
|
|
693
|
+
const tokens = data.usage.tokens || 0;
|
|
694
|
+
const computeWeight = modelSelection?.capabilities?.computeWeight || 1.0;
|
|
695
|
+
const computeUnits = tokens * computeWeight;
|
|
696
|
+
onUsageTracked(tokens, computeUnits);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
...prev,
|
|
700
|
+
[currentChatIdToUse]: [...currentChat.slice(0, lastIdx), updatedMsg],
|
|
701
|
+
};
|
|
702
|
+
});
|
|
703
|
+
// Clear loading state when response is complete
|
|
704
|
+
if (data.aiStatus === 'none') {
|
|
705
|
+
// Explicit end of response
|
|
706
|
+
log('🎯 [useStreamingAI] Setting loading to false (aiStatus: none)');
|
|
707
|
+
setIsLoading(false);
|
|
708
|
+
}
|
|
709
|
+
else if (data.type === 'json' && !data.isIntermediate) {
|
|
710
|
+
// Final JSON response (not intermediate)
|
|
711
|
+
log('🎯 [useStreamingAI] Setting loading to false (final JSON)');
|
|
712
|
+
setIsLoading(false);
|
|
713
|
+
}
|
|
714
|
+
else if (data.type === 'text' && data.aiStatus === 'none') {
|
|
715
|
+
// Text with explicit completion signal
|
|
716
|
+
log('🎯 [useStreamingAI] Setting loading to false (text with aiStatus: none)');
|
|
717
|
+
setIsLoading(false);
|
|
718
|
+
}
|
|
719
|
+
else if (data.usage) {
|
|
720
|
+
// If we have usage data, this is likely the final response
|
|
721
|
+
log('🎯 [useStreamingAI] Setting loading to false (usage data received)');
|
|
722
|
+
setIsLoading(false);
|
|
723
|
+
}
|
|
724
|
+
else if (data.isIntermediate) {
|
|
725
|
+
// Keep loading for intermediate responses
|
|
726
|
+
log('🎯 [useStreamingAI] Setting loading to true (intermediate)');
|
|
727
|
+
setIsLoading(true);
|
|
728
|
+
}
|
|
729
|
+
else if (data.text && data.aiStatus === 'typing') {
|
|
730
|
+
// Keep loading for text streaming
|
|
731
|
+
log('🎯 [useStreamingAI] Keeping loading true (text streaming)');
|
|
732
|
+
setIsLoading(true);
|
|
733
|
+
}
|
|
734
|
+
else if (data.text && !data.aiStatus) {
|
|
735
|
+
// If we have text but no specific status, assume we're done
|
|
736
|
+
log('🎯 [useStreamingAI] Setting loading to false (text with no status)');
|
|
737
|
+
setIsLoading(false);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
// Common stream end handler
|
|
741
|
+
const handleStreamEnd = () => {
|
|
742
|
+
setChatsToUse((prev) => {
|
|
743
|
+
const currentChat = prev[currentChatIdToUse] || [];
|
|
744
|
+
const lastIdx = currentChat.length - 1;
|
|
745
|
+
const lastMsg = currentChat[lastIdx];
|
|
746
|
+
if (!lastMsg || lastMsg.sender !== 'ai')
|
|
747
|
+
return prev;
|
|
748
|
+
return {
|
|
749
|
+
...prev,
|
|
750
|
+
[currentChatIdToUse]: [
|
|
751
|
+
...currentChat.slice(0, lastIdx),
|
|
752
|
+
{
|
|
753
|
+
...lastMsg,
|
|
754
|
+
isLoading: false,
|
|
755
|
+
aiStatus: 'none',
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
setIsLoading(false);
|
|
761
|
+
};
|
|
762
|
+
// Common stream error handler
|
|
763
|
+
const handleStreamError = (errorData) => {
|
|
764
|
+
try {
|
|
765
|
+
// Create a system error message in the chat
|
|
766
|
+
setChatsToUse((prev) => {
|
|
767
|
+
const currentChat = prev[currentChatIdToUse] || [];
|
|
768
|
+
const lastIdx = currentChat.length - 1;
|
|
769
|
+
// Remove the loading AI message and add an error message
|
|
770
|
+
const messagesWithoutLoading = currentChat.slice(0, lastIdx);
|
|
771
|
+
const errorMessage = {
|
|
772
|
+
sender: 'system',
|
|
773
|
+
content: [
|
|
774
|
+
{
|
|
775
|
+
type: 'text',
|
|
776
|
+
content: errorData.message ||
|
|
777
|
+
errorData.error ||
|
|
778
|
+
'An error occurred while processing your request.',
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
isError: true,
|
|
782
|
+
errorType: errorData.type || 'unknown_error',
|
|
783
|
+
retryAfter: errorData.retryAfter,
|
|
784
|
+
timestamp: Date.now(),
|
|
785
|
+
};
|
|
786
|
+
return {
|
|
787
|
+
...prev,
|
|
788
|
+
[currentChatIdToUse]: [...messagesWithoutLoading, errorMessage],
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
catch (parseError) {
|
|
793
|
+
// Fallback for non-structured errors
|
|
794
|
+
setChatsToUse((prev) => {
|
|
795
|
+
const currentChat = prev[currentChatIdToUse] || [];
|
|
796
|
+
const lastIdx = currentChat.length - 1;
|
|
797
|
+
const messagesWithoutLoading = currentChat.slice(0, lastIdx);
|
|
798
|
+
const errorMessage = {
|
|
799
|
+
sender: 'system',
|
|
800
|
+
content: [
|
|
801
|
+
{
|
|
802
|
+
type: 'text',
|
|
803
|
+
content: '❌ Unable to connect to AI service. Please check your connection and try again.',
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
isError: true,
|
|
807
|
+
errorType: 'connection_error',
|
|
808
|
+
timestamp: Date.now(),
|
|
809
|
+
};
|
|
810
|
+
return {
|
|
811
|
+
...prev,
|
|
812
|
+
[currentChatIdToUse]: [...messagesWithoutLoading, errorMessage],
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
setIsLoading(false);
|
|
817
|
+
};
|
|
818
|
+
// Create a new chat session
|
|
819
|
+
const newChat = () => {
|
|
820
|
+
const newId = Date.now().toString();
|
|
821
|
+
setCurrentChatIdToUse(newId);
|
|
822
|
+
setChatsToUse((prev) => ({ ...prev, [newId]: [] }));
|
|
823
|
+
};
|
|
824
|
+
// Optionally: expose a way to switch chats
|
|
825
|
+
const switchChat = (chatId) => {
|
|
826
|
+
setCurrentChatIdToUse(chatId);
|
|
827
|
+
};
|
|
828
|
+
return {
|
|
829
|
+
chat,
|
|
830
|
+
chats: chatsToUse,
|
|
831
|
+
currentChatId: currentChatIdToUse,
|
|
832
|
+
isLoading,
|
|
833
|
+
sendQuery,
|
|
834
|
+
newChat,
|
|
835
|
+
switchChat,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Hook for getting AI suggestions based on a query
|
|
841
|
+
*
|
|
842
|
+
* @param userId - User ID for the suggestion request
|
|
843
|
+
* @param formData - Current form data context
|
|
844
|
+
* @param serverConfig - Server configuration
|
|
845
|
+
* @param debounceMs - Debounce delay in milliseconds (default: 300)
|
|
846
|
+
* @param maxSuggestions - Maximum number of suggestions to return (default: 10)
|
|
847
|
+
* @param chatLevel - Chat level to send to the server (default: 'none')
|
|
848
|
+
* @returns Object containing suggestions, loading state, error state, and control functions
|
|
849
|
+
*/
|
|
850
|
+
function useSuggestions({ userId, formData, serverConfig, debounceMs = 300, maxSuggestions = 10, chatLevel = 'none', // <-- default to 'none'
|
|
851
|
+
modelSelection, onUsageTracked, context, // <-- Add context here
|
|
852
|
+
}) {
|
|
853
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
854
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
855
|
+
const [error, setError] = useState(null);
|
|
856
|
+
// Build API URL from config
|
|
857
|
+
const buildUrlFromConfig = useCallback((config) => {
|
|
858
|
+
const protocol = config.secure !== undefined
|
|
859
|
+
? config.secure
|
|
860
|
+
? 'https:'
|
|
861
|
+
: 'http:'
|
|
862
|
+
: window.location.protocol;
|
|
863
|
+
const hostname = config.domain || window.location.hostname;
|
|
864
|
+
const port = config.port
|
|
865
|
+
? `:${config.port}`
|
|
866
|
+
: window.location.port
|
|
867
|
+
? `:${window.location.port}`
|
|
868
|
+
: '';
|
|
869
|
+
const suffix = config.suffix || '';
|
|
870
|
+
if (!config.domain && !config.port && !config.secure) {
|
|
871
|
+
return suffix;
|
|
872
|
+
}
|
|
873
|
+
return `${protocol}//${hostname}${port}${suffix}`;
|
|
874
|
+
}, []);
|
|
875
|
+
const getSuggestions = useCallback(async (query, additionalContext) => {
|
|
876
|
+
if (!query.trim()) {
|
|
877
|
+
setSuggestions([]);
|
|
878
|
+
return [];
|
|
879
|
+
}
|
|
880
|
+
setIsLoading(true);
|
|
881
|
+
setError(null);
|
|
882
|
+
try {
|
|
883
|
+
const apiUrl = serverConfig ? buildUrlFromConfig(serverConfig) : '/api';
|
|
884
|
+
// Combine context with proper spacing and handle edge cases
|
|
885
|
+
const contextToSend = [context, additionalContext]
|
|
886
|
+
.filter(Boolean)
|
|
887
|
+
.join(' ')
|
|
888
|
+
.trim();
|
|
889
|
+
const response = await fetch(`${apiUrl}/suggest`, {
|
|
890
|
+
method: 'POST',
|
|
891
|
+
headers: {
|
|
892
|
+
'Content-Type': 'application/json',
|
|
893
|
+
},
|
|
894
|
+
body: JSON.stringify({
|
|
895
|
+
query: query.trim(),
|
|
896
|
+
userId,
|
|
897
|
+
formData: formData || {},
|
|
898
|
+
chatHistory: [], // Empty for standalone suggestions
|
|
899
|
+
isCommandSearch: true, // Flag to indicate this is for command search
|
|
900
|
+
chatLevel, // <-- always send this
|
|
901
|
+
modelSelection: modelSelection || {
|
|
902
|
+
provider: DEFAULT_CONFIG.service,
|
|
903
|
+
model: DEFAULT_CONFIG.model,
|
|
904
|
+
capabilities: {
|
|
905
|
+
supportsImages: DEFAULT_CONFIG.supportsImages,
|
|
906
|
+
computeWeight: 1.0,
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
context: contextToSend,
|
|
910
|
+
}),
|
|
911
|
+
});
|
|
912
|
+
if (!response.ok) {
|
|
913
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
914
|
+
}
|
|
915
|
+
// Handle streaming response
|
|
916
|
+
const reader = response.body?.getReader();
|
|
917
|
+
if (!reader) {
|
|
918
|
+
throw new Error('No response body');
|
|
919
|
+
}
|
|
920
|
+
const decoder = new TextDecoder();
|
|
921
|
+
let buffer = '';
|
|
922
|
+
let collectedSuggestions = [];
|
|
923
|
+
while (true) {
|
|
924
|
+
const { done, value } = await reader.read();
|
|
925
|
+
if (done)
|
|
926
|
+
break;
|
|
927
|
+
buffer += decoder.decode(value, { stream: true });
|
|
928
|
+
const lines = buffer.split('\n');
|
|
929
|
+
buffer = lines.pop() || '';
|
|
930
|
+
for (const line of lines) {
|
|
931
|
+
if (line.startsWith('data: ')) {
|
|
932
|
+
try {
|
|
933
|
+
const data = JSON.parse(line.slice(6));
|
|
934
|
+
if (data.suggestions) {
|
|
935
|
+
collectedSuggestions = data.suggestions;
|
|
936
|
+
}
|
|
937
|
+
else if (data.suggestion) {
|
|
938
|
+
collectedSuggestions.push(data.suggestion);
|
|
939
|
+
}
|
|
940
|
+
else if (data.streamedText) {
|
|
941
|
+
// Extract JSON from streamedText for suggestions
|
|
942
|
+
const jsonMatch = data.streamedText.match(/```json\s*(\[[\s\S]*?\])\s*```/);
|
|
943
|
+
if (jsonMatch) {
|
|
944
|
+
try {
|
|
945
|
+
const jsonSuggestions = JSON.parse(jsonMatch[1]);
|
|
946
|
+
if (Array.isArray(jsonSuggestions) &&
|
|
947
|
+
jsonSuggestions.length > 0) {
|
|
948
|
+
collectedSuggestions = jsonSuggestions;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch (e) {
|
|
952
|
+
console.warn('Failed to parse JSON from streamedText:', e);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
// Handle usage tracking
|
|
957
|
+
if (data.usage && onUsageTracked) {
|
|
958
|
+
const tokens = data.usage.tokens || 0;
|
|
959
|
+
const computeUnits = data.usage.computeUnits || 0;
|
|
960
|
+
onUsageTracked(tokens, computeUnits);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
catch (e) {
|
|
964
|
+
console.warn('Failed to parse SSE data:', line);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
else if (line.startsWith('event: end')) {
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
else if (line.startsWith('event: error')) {
|
|
971
|
+
const errorLine = lines.find((l) => l.startsWith('data: '));
|
|
972
|
+
if (errorLine) {
|
|
973
|
+
const errorData = JSON.parse(errorLine.slice(6));
|
|
974
|
+
throw new Error(errorData.message || 'Unknown error');
|
|
975
|
+
}
|
|
976
|
+
throw new Error('Unknown error occurred');
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
// Apply max suggestions limit
|
|
981
|
+
const limitedSuggestions = collectedSuggestions.slice(0, maxSuggestions);
|
|
982
|
+
setSuggestions(limitedSuggestions);
|
|
983
|
+
return limitedSuggestions;
|
|
984
|
+
}
|
|
985
|
+
catch (err) {
|
|
986
|
+
console.error('Error fetching suggestions:', err);
|
|
987
|
+
// Try to extract a structured error from the response
|
|
988
|
+
if (err instanceof Response) {
|
|
989
|
+
try {
|
|
990
|
+
const errorObj = await err.json();
|
|
991
|
+
setError(errorObj);
|
|
992
|
+
setSuggestions([]);
|
|
993
|
+
return errorObj;
|
|
994
|
+
}
|
|
995
|
+
catch (parseErr) {
|
|
996
|
+
setError(err.statusText || 'Unknown error');
|
|
997
|
+
setSuggestions([]);
|
|
998
|
+
return new Error(err.statusText || 'Unknown error');
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// If the error is already a structured object (from fetch or thrown by backend)
|
|
1002
|
+
if (err &&
|
|
1003
|
+
typeof err === 'object' &&
|
|
1004
|
+
'error' in err &&
|
|
1005
|
+
err.error &&
|
|
1006
|
+
typeof err.error === 'object' &&
|
|
1007
|
+
'message' in err.error) {
|
|
1008
|
+
setError(err.error);
|
|
1009
|
+
setSuggestions([]);
|
|
1010
|
+
return err.error;
|
|
1011
|
+
}
|
|
1012
|
+
// Fallback: just a string or Error
|
|
1013
|
+
setError(err instanceof Error ? err.message : 'Unknown error');
|
|
1014
|
+
setSuggestions([]);
|
|
1015
|
+
return err instanceof Error ? err : new Error('Unknown error');
|
|
1016
|
+
}
|
|
1017
|
+
finally {
|
|
1018
|
+
setIsLoading(false);
|
|
1019
|
+
}
|
|
1020
|
+
}, [
|
|
1021
|
+
userId,
|
|
1022
|
+
formData,
|
|
1023
|
+
serverConfig,
|
|
1024
|
+
maxSuggestions,
|
|
1025
|
+
chatLevel,
|
|
1026
|
+
modelSelection,
|
|
1027
|
+
onUsageTracked,
|
|
1028
|
+
buildUrlFromConfig,
|
|
1029
|
+
context,
|
|
1030
|
+
]);
|
|
1031
|
+
const clearSuggestions = useCallback(() => {
|
|
1032
|
+
setSuggestions([]);
|
|
1033
|
+
setError(null);
|
|
1034
|
+
}, []);
|
|
1035
|
+
return {
|
|
1036
|
+
suggestions,
|
|
1037
|
+
isLoading,
|
|
1038
|
+
error,
|
|
1039
|
+
getSuggestions,
|
|
1040
|
+
clearSuggestions,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Hook for getting suggestions with automatic debouncing
|
|
1045
|
+
*
|
|
1046
|
+
* @param query - The search query
|
|
1047
|
+
* @param options - Configuration options
|
|
1048
|
+
* @returns Object containing suggestions, loading state, and error state
|
|
1049
|
+
*/
|
|
1050
|
+
function useDebouncedSuggestions(query, options) {
|
|
1051
|
+
const { suggestions, isLoading, error, getSuggestions, clearSuggestions } = useSuggestions(options);
|
|
1052
|
+
const { debounceMs = 300 } = options;
|
|
1053
|
+
useEffect(() => {
|
|
1054
|
+
if (!query.trim()) {
|
|
1055
|
+
clearSuggestions();
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const timeoutId = setTimeout(() => {
|
|
1059
|
+
getSuggestions(query);
|
|
1060
|
+
}, debounceMs);
|
|
1061
|
+
return () => clearTimeout(timeoutId);
|
|
1062
|
+
}, [query, getSuggestions, clearSuggestions, debounceMs]);
|
|
1063
|
+
return {
|
|
1064
|
+
suggestions,
|
|
1065
|
+
isLoading,
|
|
1066
|
+
error,
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Size mapping
|
|
1071
|
+
const sizeMap = {
|
|
1072
|
+
xs: 12,
|
|
1073
|
+
sm: 16,
|
|
1074
|
+
md: 20,
|
|
1075
|
+
lg: 24,
|
|
1076
|
+
xl: 32,
|
|
1077
|
+
};
|
|
1078
|
+
// Get numeric size from IconSize
|
|
1079
|
+
const getSize = (size) => {
|
|
1080
|
+
return typeof size === 'number' ? size : sizeMap[size];
|
|
1081
|
+
};
|
|
1082
|
+
// Map action types to icon names
|
|
1083
|
+
const getIconNameForAction = (action) => {
|
|
1084
|
+
const lower = action.toLowerCase();
|
|
1085
|
+
if (lower.includes('create') || lower.includes('new'))
|
|
1086
|
+
return 'plus';
|
|
1087
|
+
if (lower.includes('edit') || lower.includes('update'))
|
|
1088
|
+
return 'pencil';
|
|
1089
|
+
if (lower.includes('delete') || lower.includes('remove'))
|
|
1090
|
+
return 'trash';
|
|
1091
|
+
if (lower.includes('view'))
|
|
1092
|
+
return 'eye';
|
|
1093
|
+
return null;
|
|
1094
|
+
};
|
|
1095
|
+
// Individual icon components
|
|
1096
|
+
const iconComponents = {
|
|
1097
|
+
// UI Icons
|
|
1098
|
+
ai: ({ size, className = '', color = 'currentColor' }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", xmlns: "http://www.w3.org/2000/svg", className: `relative left-[0.5px] ${className}`, children: jsx("path", { d: "M18.7801 12.4715L13.4471 10.4877L11.4801 5.19677C11.3572 4.91331 11.149 4.62911 10.8748 4.44014C10.6005 4.25116 10.2696 4.06182 9.92912 4.06182C9.59815 4.06182 9.26696 4.25116 8.99273 4.44014C8.71849 4.62911 8.50133 4.91331 8.38786 5.19677L6.42087 10.4877L1.07815 12.4715C0.76609 12.566 0.491783 12.8491 0.293201 13.0381C0.104075 13.3215 0 13.6995 0 13.9829C0 14.3609 0.104075 14.6443 0.293201 14.9278C0.491783 15.2113 0.76609 15.4958 1.07815 15.5903L6.41117 17.5742L8.37816 22.8651C8.49164 23.1485 8.70926 23.4327 8.98349 23.6217C9.25773 23.8107 9.58892 24 9.91989 24C10.2603 24 10.5913 23.8107 10.8655 23.6217C11.1398 23.4327 11.3475 23.1485 11.4704 22.8651L13.4374 17.5742L18.7709 15.5903C19.0924 15.4958 19.3667 15.2127 19.5558 15.0238C19.7544 14.7403 19.849 14.3624 19.849 14.0789C19.849 13.7009 19.7544 13.4175 19.5558 13.134C19.3667 12.8506 19.0924 12.566 18.7709 12.4715H18.7801ZM12.8699 15.9686C12.643 16.0631 12.435 16.2506 12.2743 16.3451C12.1041 16.5341 11.9715 16.7242 11.8864 17.0076L9.92912 22.2985L7.96214 17.0076C7.88649 16.7242 7.75419 16.5341 7.58398 16.3451C7.41376 16.2506 7.21529 16.0631 6.98834 15.9686L1.67379 14.0789L6.98834 12.0932C7.21529 11.9987 7.41376 11.8112 7.58398 11.7167C7.75419 11.5278 7.88649 11.3377 7.96214 11.0542L9.92912 5.76332L11.8961 11.0542C11.9718 11.3377 12.1041 11.5278 12.2743 11.7167C12.4445 11.9057 12.643 11.9987 12.8699 12.0932L18.1942 14.0789L12.8699 15.9686ZM13.2388 3.30704C13.2388 3.02358 13.3237 2.83497 13.4845 2.74048C13.6358 2.55151 13.8439 2.45629 14.0709 2.45629H15.7257V0.85075C15.7257 0.567289 15.8108 0.377209 15.9621 0.188235C16.1229 0.0937486 16.331 0 16.5485 0C16.766 0 16.9742 0.0937486 17.1349 0.188235C17.2862 0.377209 17.3806 0.567289 17.3806 0.85075V2.45629H19.0354C19.2529 2.45629 19.4613 2.55151 19.6126 2.74048C19.7734 2.83497 19.8582 3.02358 19.8582 3.30704C19.8582 3.49601 19.7734 3.68461 19.6126 3.87359C19.4613 4.06256 19.2529 4.06182 19.0354 4.06182H17.3806V5.76332C17.3806 5.9523 17.2862 6.23539 17.1349 6.32987C16.9742 6.51885 16.766 6.61407 16.5485 6.61407C16.331 6.61407 16.1229 6.51885 15.9621 6.32987C15.8108 6.23539 15.7257 5.9523 15.7257 5.76332V4.06182H14.0709C13.8439 4.06182 13.6358 4.06256 13.4845 3.87359C13.3237 3.68461 13.2388 3.49601 13.2388 3.30704ZM24 8.21961C24 8.50307 23.9057 8.69167 23.7544 8.78616C23.5936 8.97513 23.3855 9.07036 23.168 9.07036H22.3451V9.92111C22.3451 10.1101 22.2508 10.2987 22.0995 10.4877C21.9387 10.6766 21.7306 10.6759 21.5131 10.6759C21.2956 10.6759 21.0875 10.6766 20.9267 10.4877C20.7754 10.2987 20.6903 10.1101 20.6903 9.92111V9.07036H19.8582C19.6408 9.07036 19.4326 8.97513 19.2718 8.78616C19.1205 8.69167 19.0354 8.50307 19.0354 8.21961C19.0354 8.03063 19.1205 7.84203 19.2718 7.65306C19.4326 7.46408 19.6408 7.46482 19.8582 7.46482H20.6903V6.61407C20.6903 6.33061 20.7754 6.14201 20.9267 6.04752C21.0875 5.85855 21.2956 5.76332 21.5131 5.76332C21.7306 5.76332 21.9387 5.85855 22.0995 6.04752C22.2508 6.14201 22.3451 6.33061 22.3451 6.61407V7.46482H23.168C23.3855 7.46482 23.5936 7.46408 23.7544 7.65306C23.9057 7.84203 24 8.03063 24 8.21961Z", fill: color }) })),
|
|
1099
|
+
user: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: `lucide lucide-user ${className}`, children: [jsx("path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }), jsx("circle", { cx: "12", cy: "7", r: "4" })] })),
|
|
1100
|
+
system: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: `lucide lucide-settings ${className}`, children: [jsx("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.39a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }), jsx("circle", { cx: "12", cy: "12", r: "3" })] })),
|
|
1101
|
+
paperclip: ({ size, className = '', color = 'currentColor' }) => (jsx("svg", { width: size, height: size, fill: "none", stroke: color, strokeWidth: "2", viewBox: "0 0 24 24", className: className, children: jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) })),
|
|
1102
|
+
image: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, fill: "none", stroke: color, strokeWidth: "2", viewBox: "0 0 24 24", className: className, children: [jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }), jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }), jsx("polyline", { points: "21,15 16,10 5,21" })] })),
|
|
1103
|
+
noImage: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, fill: "none", stroke: color, strokeWidth: "2", viewBox: "0 0 24 24", className: className, children: [jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }), jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }), jsx("polyline", { points: "21,15 16,10 5,21" }), jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21" })] })),
|
|
1104
|
+
microphone: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, fill: "none", stroke: color, strokeWidth: "2", viewBox: "0 0 24 24", className: className, children: [jsx("path", { d: "M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" }), jsx("path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }), jsx("line", { x1: "12", y1: "19", x2: "12", y2: "23" }), jsx("line", { x1: "8", y1: "23", x2: "16", y2: "23" })] })),
|
|
1105
|
+
send: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, fill: "none", stroke: color, strokeWidth: "2", viewBox: "0 0 24 24", className: className, children: [jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), jsx("polygon", { points: "22,2 15,22 11,13 2,9 22,2" })] })),
|
|
1106
|
+
// Action Icons
|
|
1107
|
+
plus: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }), jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })] })),
|
|
1108
|
+
// AI Provider Icons
|
|
1109
|
+
claude: ({ size, className = '' }) => (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", shapeRendering: "geometricPrecision", textRendering: "geometricPrecision", imageRendering: "optimizeQuality", fillRule: "evenodd", clipRule: "evenodd", viewBox: "0 0 512 509.64", width: size, height: size, className: className, children: [jsx("path", { fill: "#D77655", d: "M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.612-115.613 115.612H115.612C52.026 509.639 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z" }), jsx("path", { fill: "#FCF2EE", fillRule: "nonzero", d: "M142.27 316.619l73.655-41.326 1.238-3.589-1.238-1.996-3.589-.001-12.31-.759-42.084-1.138-36.498-1.516-35.361-1.896-8.897-1.895-8.34-10.995.859-5.484 7.482-5.03 10.717.935 23.683 1.617 35.537 2.452 25.782 1.517 38.193 3.968h6.064l.86-2.451-2.073-1.517-1.618-1.517-36.776-24.922-39.81-26.338-20.852-15.166-11.273-7.683-5.687-7.204-2.451-15.721 10.237-11.273 13.75.935 3.513.936 13.928 10.716 29.749 23.027 38.848 28.612 5.687 4.727 2.275-1.617.278-1.138-2.553-4.271-21.13-38.193-22.546-38.848-10.035-16.101-2.654-9.655c-.935-3.968-1.617-7.304-1.617-11.374l11.652-15.823 6.445-2.073 15.545 2.073 6.547 5.687 9.655 22.092 15.646 34.78 24.265 47.291 7.103 14.028 3.791 12.992 1.416 3.968 2.449-.001v-2.275l1.997-26.641 3.69-32.707 3.589-42.084 1.239-11.854 5.863-14.206 11.652-7.683 9.099 4.348 7.482 10.716-1.036 6.926-4.449 28.915-8.72 45.294-5.687 30.331h3.313l3.792-3.791 15.342-20.372 25.782-32.227 11.374-12.789 13.27-14.129 8.517-6.724 16.1-.001 11.854 17.617-5.307 18.199-16.581 21.029-13.75 17.819-19.716 26.54-12.309 21.231 1.138 1.694 2.932-.278 44.536-9.479 24.062-4.347 28.714-4.928 12.992 6.066 1.416 6.167-5.106 12.613-30.71 7.583-36.018 7.204-53.636 12.689-.657.48.758.935 24.164 2.275 10.337.556h25.301l47.114 3.514 12.309 8.139 7.381 9.959-1.238 7.583-18.957 9.655-25.579-6.066-59.702-14.205-20.474-5.106-2.83-.001v1.694l17.061 16.682 31.266 28.233 39.152 36.397 1.997 8.999-5.03 7.102-5.307-.758-34.401-25.883-13.27-11.651-30.053-25.302-1.996-.001v2.654l6.926 10.136 36.574 54.975 1.895 16.859-2.653 5.485-9.479 3.311-10.414-1.895-21.408-30.054-22.092-33.844-17.819-30.331-2.173 1.238-10.515 113.261-4.929 5.788-11.374 4.348-9.478-7.204-5.03-11.652 5.03-23.027 6.066-30.052 4.928-23.886 4.449-29.674 2.654-9.858-.177-.657-2.173.278-22.37 30.71-34.021 45.977-26.919 28.815-6.445 2.553-11.173-5.789 1.037-10.337 6.243-9.2 37.257-47.392 22.47-29.371 14.508-16.961-.101-2.451h-.859l-98.954 64.251-17.618 2.275-7.583-7.103.936-11.652 3.589-3.791 29.749-20.474-.101.102.024.101z" })] })),
|
|
1110
|
+
openai: ({ size, className = '' }) => (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", shapeRendering: "geometricPrecision", textRendering: "geometricPrecision", imageRendering: "optimizeQuality", fillRule: "evenodd", clipRule: "evenodd", viewBox: "0 0 512 509.639", width: size, height: size, className: className, children: [jsx("path", { fill: "#fff", d: "M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.613-115.613 115.613H115.612C52.026 509.64 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z" }), jsx("path", { fillRule: "nonzero", d: "M412.037 221.764a90.834 90.834 0 004.648-28.67 90.79 90.79 0 00-12.443-45.87c-16.37-28.496-46.738-46.089-79.605-46.089-6.466 0-12.943.683-19.264 2.04a90.765 90.765 0 00-67.881-30.515h-.576c-.059.002-.149.002-.216.002-39.807 0-75.108 25.686-87.346 63.554-25.626 5.239-47.748 21.31-60.682 44.03a91.873 91.873 0 00-12.407 46.077 91.833 91.833 0 0023.694 61.553 90.802 90.802 0 00-4.649 28.67 90.804 90.804 0 0012.442 45.87c16.369 28.504 46.74 46.087 79.61 46.087a91.81 91.81 0 0019.253-2.04 90.783 90.783 0 0067.887 30.516h.576l.234-.001c39.829 0 75.119-25.686 87.357-63.588 25.626-5.242 47.748-21.312 60.682-44.033a91.718 91.718 0 0012.383-46.035 91.83 91.83 0 00-23.693-61.553l-.004-.005zM275.102 413.161h-.094a68.146 68.146 0 01-43.611-15.8 56.936 56.936 0 002.155-1.221l72.54-41.901a11.799 11.799 0 005.962-10.251V241.651l30.661 17.704c.326.163.55.479.596.84v84.693c-.042 37.653-30.554 68.198-68.21 68.273h.001zm-146.689-62.649a68.128 68.128 0 01-9.152-34.085c0-3.904.341-7.817 1.005-11.663.539.323 1.48.897 2.155 1.285l72.54 41.901a11.832 11.832 0 0011.918-.002l88.563-51.137v35.408a1.1 1.1 0 01-.438.94l-73.33 42.339a68.43 68.43 0 01-34.11 9.12 68.359 68.359 0 01-59.15-34.11l-.001.004zm-19.083-158.36a68.044 68.044 0 0135.538-29.934c0 .625-.036 1.731-.036 2.5v83.801l-.001.07a11.79 11.79 0 005.954 10.242l88.564 51.13-30.661 17.704a1.096 1.096 0 01-1.034.093l-73.337-42.375a68.36 68.36 0 01-34.095-59.143 68.412 68.412 0 019.112-34.085l-.004-.003zm251.907 58.621l-88.563-51.137 30.661-17.697a1.097 1.097 0 011.034-.094l73.337 42.339c21.109 12.195 34.132 34.746 34.132 59.132 0 28.604-17.849 54.199-44.686 64.078v-86.308c.004-.032.004-.065.004-.096 0-4.219-2.261-8.119-5.919-10.217zm30.518-45.93c-.539-.331-1.48-.898-2.155-1.286l-72.54-41.901a11.842 11.842 0 00-5.958-1.611c-2.092 0-4.15.558-5.957 1.611l-88.564 51.137v-35.408l-.001-.061a1.1 1.1 0 01.44-.88l73.33-42.303a68.301 68.301 0 0134.108-9.129c37.704 0 68.281 30.577 68.281 68.281a68.69 68.69 0 01-.984 11.545v.005zm-191.843 63.109l-30.668-17.704a1.09 1.09 0 01-.596-.84v-84.692c.016-37.685 30.593-68.236 68.281-68.236a68.332 68.332 0 0143.689 15.804 63.09 63.09 0 00-2.155 1.222l-72.54 41.9a11.794 11.794 0 00-5.961 10.248v.068l-.05 102.23zm16.655-35.91l39.445-22.782 39.444 22.767v45.55l-39.444 22.767-39.445-22.767v-45.535z" })] })),
|
|
1111
|
+
gemini: ({ size, className = '' }) => (jsxs("svg", { fill: "none", xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 16 16", width: size, height: size, className: className, children: [jsx("path", { d: "M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z", fill: "url(#prefix__paint0_radial_980_20147)" }), jsx("defs", { children: jsxs("radialGradient", { id: "prefix__paint0_radial_980_20147", cx: "0", cy: "0", r: "1", gradientUnits: "userSpaceOnUse", gradientTransform: "matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)", children: [jsx("stop", { offset: ".067", stopColor: "#9168C0" }), jsx("stop", { offset: ".343", stopColor: "#5684D1" }), jsx("stop", { offset: ".672", stopColor: "#1BA1E3" })] }) })] })),
|
|
1112
|
+
grok: ({ size, className = '' }) => (jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", shapeRendering: "geometricPrecision", textRendering: "geometricPrecision", imageRendering: "optimizeQuality", fillRule: "evenodd", clipRule: "evenodd", viewBox: "0 0 512 509.641", width: size, height: size, className: className, children: [jsx("path", { d: "M115.612 0h280.776C459.975 0 512 52.026 512 115.612v278.416c0 63.587-52.025 115.613-115.612 115.613H115.612C52.026 509.641 0 457.615 0 394.028V115.612C0 52.026 52.026 0 115.612 0z" }), jsx("path", { fill: "#fff", d: "M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437l-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z" })] })),
|
|
1113
|
+
pencil: ({ size, className = '', color = 'currentColor' }) => (jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: jsx("path", { d: "M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" }) })),
|
|
1114
|
+
trash: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [jsx("polyline", { points: "3,6 5,6 21,6" }), jsx("path", { d: "M19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2" })] })),
|
|
1115
|
+
eye: ({ size, className = '', color = 'currentColor' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: className, children: [jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }), jsx("circle", { cx: "12", cy: "12", r: "3" })] })),
|
|
1116
|
+
// Error Icons
|
|
1117
|
+
error: ({ size, className = '', color = '#EF4444' }) => (jsx("svg", { className: `w-${size} h-${size} ${className}`, fill: color, viewBox: "0 0 20 20", width: size, height: size, children: jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", clipRule: "evenodd" }) })),
|
|
1118
|
+
warning: ({ size, className = '', color = '#F59E0B' }) => (jsx("svg", { className: `w-${size} h-${size} ${className}`, fill: color, viewBox: "0 0 20 20", width: size, height: size, children: jsx("path", { fillRule: "evenodd", d: "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", clipRule: "evenodd" }) })),
|
|
1119
|
+
timeout: ({ size, className = '', color = '#EAB308' }) => (jsx("svg", { className: `w-${size} h-${size} ${className}`, fill: color, viewBox: "0 0 20 20", width: size, height: size, children: jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.414L11 9.586V6z", clipRule: "evenodd" }) })),
|
|
1120
|
+
'auth-error': ({ size, className = '', color = '#EF4444' }) => (jsx("svg", { className: `w-${size} h-${size} ${className}`, fill: color, viewBox: "0 0 20 20", width: size, height: size, children: jsx("path", { fillRule: "evenodd", d: "M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-2a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z", clipRule: "evenodd" }) })),
|
|
1121
|
+
// File Type Icons
|
|
1122
|
+
pdf: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#DC2626" }), jsx("path", { d: "M19 2v5h5", stroke: "#B91C1C", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "2", rx: "1", fill: "white", opacity: "0.9" }), jsx("rect", { x: "9", y: "16", width: "10", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("rect", { x: "9", y: "19", width: "12", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("text", { x: "16", y: "25", fontSize: "5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "PDF" })] })),
|
|
1123
|
+
word: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#2563EB" }), jsx("path", { d: "M19 2v5h5", stroke: "#1D4ED8", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "2", rx: "1", fill: "white", opacity: "0.9" }), jsx("rect", { x: "9", y: "16", width: "10", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("rect", { x: "9", y: "19", width: "12", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("text", { x: "16", y: "25", fontSize: "4.5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "DOC" })] })),
|
|
1124
|
+
excel: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#059669" }), jsx("path", { d: "M19 2v5h5", stroke: "#047857", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "8", fill: "white", opacity: "0.2", rx: "1" }), jsx("line", { x1: "12", y1: "12", x2: "12", y2: "20", stroke: "white", strokeWidth: "0.5", opacity: "0.6" }), jsx("line", { x1: "16", y1: "12", x2: "16", y2: "20", stroke: "white", strokeWidth: "0.5", opacity: "0.6" }), jsx("line", { x1: "20", y1: "12", x2: "20", y2: "20", stroke: "white", strokeWidth: "0.5", opacity: "0.6" }), jsx("line", { x1: "9", y1: "15", x2: "23", y2: "15", stroke: "white", strokeWidth: "0.5", opacity: "0.6" }), jsx("line", { x1: "9", y1: "17", x2: "23", y2: "17", stroke: "white", strokeWidth: "0.5", opacity: "0.6" }), jsx("text", { x: "16", y: "25", fontSize: "4.5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "XLS" })] })),
|
|
1125
|
+
csv: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#7C3AED" }), jsx("path", { d: "M19 2v5h5", stroke: "#6D28D9", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "1.5", rx: "0.75", fill: "white", opacity: "0.8" }), jsx("rect", { x: "9", y: "15", width: "10", height: "1.5", rx: "0.75", fill: "white", opacity: "0.6" }), jsx("rect", { x: "9", y: "18", width: "12", height: "1.5", rx: "0.75", fill: "white", opacity: "0.6" }), jsx("circle", { cx: "11", cy: "13", r: "0.5", fill: "#7C3AED" }), jsx("circle", { cx: "15", cy: "13", r: "0.5", fill: "#7C3AED" }), jsx("circle", { cx: "19", cy: "13", r: "0.5", fill: "#7C3AED" }), jsx("text", { x: "16", y: "25", fontSize: "4.5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "CSV" })] })),
|
|
1126
|
+
text: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#6B7280" }), jsx("path", { d: "M19 2v5h5", stroke: "#4B5563", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "1.5", rx: "0.75", fill: "white", opacity: "0.9" }), jsx("rect", { x: "9", y: "15", width: "12", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("rect", { x: "9", y: "18", width: "10", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("rect", { x: "9", y: "21", width: "8", height: "1.5", rx: "0.75", fill: "white", opacity: "0.5" }), jsx("text", { x: "16", y: "26", fontSize: "4.5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "TXT" })] })),
|
|
1127
|
+
rtf: ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#EA580C" }), jsx("path", { d: "M19 2v5h5", stroke: "#C2410C", strokeWidth: "1", fill: "none" }), jsx("rect", { x: "9", y: "12", width: "14", height: "2", rx: "1", fill: "white", opacity: "0.9" }), jsx("rect", { x: "9", y: "16", width: "10", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("rect", { x: "9", y: "19", width: "12", height: "1.5", rx: "0.75", fill: "white", opacity: "0.7" }), jsx("text", { x: "10", y: "14", fontSize: "3", fill: "#EA580C", fontWeight: "bold", children: "B" }), jsx("text", { x: "13", y: "14", fontSize: "3", fill: "#EA580C", fontStyle: "italic", children: "I" }), jsx("text", { x: "16", y: "25", fontSize: "4.5", fill: "white", textAnchor: "middle", fontWeight: "600", children: "RTF" })] })),
|
|
1128
|
+
'file-generic': ({ size, className = '' }) => (jsxs("svg", { width: size, height: size, viewBox: "0 0 32 32", fill: "none", className: className, children: [jsx("path", { d: "M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z", fill: "#9CA3AF" }), jsx("path", { d: "M19 2v5h5", stroke: "#6B7280", strokeWidth: "1", fill: "none" }), jsx("circle", { cx: "16", cy: "16", r: "3", fill: "white", opacity: "0.8" }), jsx("circle", { cx: "16", cy: "16", r: "1", fill: "#9CA3AF" })] })),
|
|
1129
|
+
};
|
|
1130
|
+
// Main Icon component
|
|
1131
|
+
const Icon = ({ name, size = 'md', className = '', color, style, }) => {
|
|
1132
|
+
const numericSize = getSize(size);
|
|
1133
|
+
const IconComponent = iconComponents[name];
|
|
1134
|
+
if (!IconComponent) {
|
|
1135
|
+
console.warn(`Icon "${name}" not found`);
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
return (jsx("span", { className: className, style: style, children: jsx(IconComponent, { size: numericSize, className: "", color: color }) }));
|
|
1139
|
+
};
|
|
1140
|
+
/**
|
|
1141
|
+
* Get an icon as an SVG string for vanilla JavaScript applications
|
|
1142
|
+
* @param name - Icon name
|
|
1143
|
+
* @param options - Icon options (size, color, className)
|
|
1144
|
+
* @returns SVG string or null if icon not found
|
|
1145
|
+
*/
|
|
1146
|
+
const getIcon = (name, options = {}) => {
|
|
1147
|
+
const { size = 'md', color = 'currentColor', className = '' } = options;
|
|
1148
|
+
const numericSize = getSize(size);
|
|
1149
|
+
const IconComponent = iconComponents[name];
|
|
1150
|
+
if (!IconComponent) {
|
|
1151
|
+
console.warn(`Icon "${name}" not found`);
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
// For file type icons that don't use color prop, we need special handling
|
|
1155
|
+
const isFileIcon = [
|
|
1156
|
+
'pdf',
|
|
1157
|
+
'word',
|
|
1158
|
+
'excel',
|
|
1159
|
+
'csv',
|
|
1160
|
+
'text',
|
|
1161
|
+
'rtf',
|
|
1162
|
+
'file-generic',
|
|
1163
|
+
].includes(name);
|
|
1164
|
+
// Create the icon element with proper props
|
|
1165
|
+
const element = IconComponent({
|
|
1166
|
+
size: numericSize,
|
|
1167
|
+
className,
|
|
1168
|
+
color: isFileIcon ? undefined : color,
|
|
1169
|
+
});
|
|
1170
|
+
// Convert React element to SVG string
|
|
1171
|
+
// Since we're dealing with SVG elements, we can extract their outerHTML
|
|
1172
|
+
if (React.isValidElement(element)) {
|
|
1173
|
+
// For now, return a simplified SVG string based on the icon name and options
|
|
1174
|
+
// This is a basic implementation - you might want to use a proper React-to-string renderer
|
|
1175
|
+
const svgProps = element.props;
|
|
1176
|
+
const svgElement = element.type;
|
|
1177
|
+
if (svgElement === 'svg') {
|
|
1178
|
+
// Build SVG string manually for better control
|
|
1179
|
+
const attrs = [
|
|
1180
|
+
`width="${numericSize}"`,
|
|
1181
|
+
`height="${numericSize}"`,
|
|
1182
|
+
svgProps.viewBox ? `viewBox="${svgProps.viewBox}"` : '',
|
|
1183
|
+
svgProps.fill !== undefined ? `fill="${svgProps.fill}"` : '',
|
|
1184
|
+
svgProps.stroke ? `stroke="${svgProps.stroke}"` : '',
|
|
1185
|
+
svgProps.strokeWidth ? `stroke-width="${svgProps.strokeWidth}"` : '',
|
|
1186
|
+
className ? `class="${className}"` : '',
|
|
1187
|
+
]
|
|
1188
|
+
.filter(Boolean)
|
|
1189
|
+
.join(' ');
|
|
1190
|
+
// This is a simplified approach - for production you'd want proper serialization
|
|
1191
|
+
return `<svg ${attrs}>${svgProps.children || ''}</svg>`;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
// Fallback: return a basic SVG string for the icon
|
|
1195
|
+
return getBasicSvgString(name, numericSize, color, className);
|
|
1196
|
+
};
|
|
1197
|
+
/**
|
|
1198
|
+
* Fallback function to generate basic SVG strings for icons
|
|
1199
|
+
*/
|
|
1200
|
+
const getBasicSvgString = (name, size, color, className) => {
|
|
1201
|
+
const classAttr = className ? ` class="${className}"` : '';
|
|
1202
|
+
// Basic SVG templates for each icon type
|
|
1203
|
+
switch (name) {
|
|
1204
|
+
case 'plus':
|
|
1205
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`;
|
|
1206
|
+
case 'pencil':
|
|
1207
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>`;
|
|
1208
|
+
case 'trash':
|
|
1209
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><polyline points="3,6 5,6 21,6"/><path d="M19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"/></svg>`;
|
|
1210
|
+
case 'eye':
|
|
1211
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`;
|
|
1212
|
+
case 'ai':
|
|
1213
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>`;
|
|
1214
|
+
case 'user':
|
|
1215
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
|
|
1216
|
+
case 'send':
|
|
1217
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22,2 15,22 11,13 2,9 22,2"/></svg>`;
|
|
1218
|
+
case 'error':
|
|
1219
|
+
return `<svg width="${size}" height="${size}" fill="#EF4444" viewBox="0 0 20 20"${classAttr}><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>`;
|
|
1220
|
+
case 'warning':
|
|
1221
|
+
return `<svg width="${size}" height="${size}" fill="#F59E0B" viewBox="0 0 20 20"${classAttr}><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>`;
|
|
1222
|
+
case 'timeout':
|
|
1223
|
+
return `<svg width="${size}" height="${size}" fill="#EAB308" viewBox="0 0 20 20"${classAttr}><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.414L11 9.586V6z" clip-rule="evenodd"/></svg>`;
|
|
1224
|
+
case 'auth-error':
|
|
1225
|
+
return `<svg width="${size}" height="${size}" fill="#EF4444" viewBox="0 0 20 20"${classAttr}><path fill-rule="evenodd" d="M18 8a6 6 0 01-7.743 5.743L10 14l-1 1-1 1H6v2H2v-4l4.257-4.257A6 6 0 1118 8zm-6-2a1 1 0 100 2 2 2 0 012 2 1 1 0 102 0 4 4 0 00-4-4z" clip-rule="evenodd"/></svg>`;
|
|
1226
|
+
case 'claude':
|
|
1227
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 512 509.64"${classAttr}><path fill="#D77655" d="M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.612-115.613 115.612H115.612C52.026 509.639 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#FCF2EE" fill-rule="nonzero" d="M142.27 316.619l73.655-41.326 1.238-3.589-1.238-1.996-3.589-.001-12.31-.759-42.084-1.138-36.498-1.516-35.361-1.896-8.897-1.895-8.34-10.995.859-5.484 7.482-5.03 10.717.935 23.683 1.617 35.537 2.452 25.782 1.517 38.193 3.968h6.064l.86-2.451-2.073-1.517-1.618-1.517-36.776-24.922-39.81-26.338-20.852-15.166-11.273-7.683-5.687-7.204-2.451-15.721 10.237-11.273 13.75.935 3.513.936 13.928 10.716 29.749 23.027 38.848 28.612 5.687 4.727 2.275-1.617.278-1.138-2.553-4.271-21.13-38.193-22.546-38.848-10.035-16.101-2.654-9.655c-.935-3.968-1.617-7.304-1.617-11.374l11.652-15.823 6.445-2.073 15.545 2.073 6.547 5.687 9.655 22.092 15.646 34.78 24.265 47.291 7.103 14.028 3.791 12.992 1.416 3.968 2.449-.001v-2.275l1.997-26.641 3.69-32.707 3.589-42.084 1.239-11.854 5.863-14.206 11.652-7.683 9.099 4.348 7.482 10.716-1.036 6.926-4.449 28.915-8.72 45.294-5.687 30.331h3.313l3.792-3.791 15.342-20.372 25.782-32.227 11.374-12.789 13.27-14.129 8.517-6.724 16.1-.001 11.854 17.617-5.307 18.199-16.581 21.029-13.75 17.819-19.716 26.54-12.309 21.231 1.138 1.694 2.932-.278 44.536-9.479 24.062-4.347 28.714-4.928 12.992 6.066 1.416 6.167-5.106 12.613-30.71 7.583-36.018 7.204-53.636 12.689-.657.48.758.935 24.164 2.275 10.337.556h25.301l47.114 3.514 12.309 8.139 7.381 9.959-1.238 7.583-18.957 9.655-25.579-6.066-59.702-14.205-20.474-5.106-2.83-.001v1.694l17.061 16.682 31.266 28.233 39.152 36.397 1.997 8.999-5.03 7.102-5.307-.758-34.401-25.883-13.27-11.651-30.053-25.302-1.996-.001v2.654l6.926 10.136 36.574 54.975 1.895 16.859-2.653 5.485-9.479 3.311-10.414-1.895-21.408-30.054-22.092-33.844-17.819-30.331-2.173 1.238-10.515 113.261-4.929 5.788-11.374 4.348-9.478-7.204-5.03-11.652 5.03-23.027 6.066-30.052 4.928-23.886 4.449-29.674 2.654-9.858-.177-.657-2.173.278-22.37 30.71-34.021 45.977-26.919 28.815-6.445 2.553-11.173-5.789 1.037-10.337 6.243-9.2 37.257-47.392 22.47-29.371 14.508-16.961-.101-2.451h-.859l-98.954 64.251-17.618 2.275-7.583-7.103.936-11.652 3.589-3.791 29.749-20.474-.101.102.024.101z"/></svg>`;
|
|
1228
|
+
case 'openai':
|
|
1229
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 512 509.639"${classAttr}><path fill="#fff" d="M115.612 0h280.775C459.974 0 512 52.026 512 115.612v278.415c0 63.587-52.026 115.613-115.613 115.613H115.612C52.026 509.64 0 457.614 0 394.027V115.612C0 52.026 52.026 0 115.612 0z"/><path fill-rule="nonzero" d="M412.037 221.764a90.834 90.834 0 004.648-28.67 90.79 90.79 0 00-12.443-45.87c-16.37-28.496-46.738-46.089-79.605-46.089-6.466 0-12.943.683-19.264 2.04a90.765 90.765 0 00-67.881-30.515h-.576c-.059.002-.149.002-.216.002-39.807 0-75.108 25.686-87.346 63.554-25.626 5.239-47.748 21.31-60.682 44.03a91.873 91.873 0 00-12.407 46.077 91.833 91.833 0 0023.694 61.553 90.802 90.802 0 00-4.649 28.67 90.804 90.804 0 0012.442 45.87c16.369 28.504 46.74 46.087 79.61 46.087a91.81 91.81 0 0019.253-2.04 90.783 90.783 0 0067.887 30.516h.576l.234-.001c39.829 0 75.119-25.686 87.357-63.588 25.626-5.242 47.748-21.312 60.682-44.033a91.718 91.718 0 0012.383-46.035 91.83 91.83 0 00-23.693-61.553l-.004-.005zM275.102 413.161h-.094a68.146 68.146 0 01-43.611-15.8 56.936 56.936 0 002.155-1.221l72.54-41.901a11.799 11.799 0 005.962-10.251V241.651l30.661 17.704c.326.163.55.479.596.84v84.693c-.042 37.653-30.554 68.198-68.21 68.273h.001zm-146.689-62.649a68.128 68.128 0 01-9.152-34.085c0-3.904.341-7.817 1.005-11.663.539.323 1.48.897 2.155 1.285l72.54 41.901a11.832 11.832 0 0011.918-.002l88.563-51.137v35.408a1.1 1.1 0 01-.438.94l-73.33 42.339a68.43 68.43 0 01-34.11 9.12 68.359 68.359 0 01-59.15-34.11l-.001.004zm-19.083-158.36a68.044 68.044 0 0135.538-29.934c0 .625-.036 1.731-.036 2.5v83.801l-.001.07a11.79 11.79 0 005.954 10.242l88.564 51.13-30.661 17.704a1.096 1.096 0 01-1.034.093l-73.337-42.375a68.36 68.36 0 01-34.095-59.143 68.412 68.412 0 019.112-34.085l-.004-.003zm251.907 58.621l-88.563-51.137 30.661-17.697a1.097 1.097 0 011.034-.094l73.337 42.339c21.109 12.195 34.132 34.746 34.132 59.132 0 28.604-17.849 54.199-44.686 64.078v-86.308c.004-.032.004-.065.004-.096 0-4.219-2.261-8.119-5.919-10.217zm30.518-45.93c-.539-.331-1.48-.898-2.155-1.286l-72.54-41.901a11.842 11.842 0 00-5.958-1.611c-2.092 0-4.15.558-5.957 1.611l-88.564 51.137v-35.408l-.001-.061a1.1 1.1 0 01.44-.88l73.33-42.303a68.301 68.301 0 0134.108-9.129c37.704 0 68.281 30.577 68.281 68.281a68.69 68.69 0 01-.984 11.545v.005zm-191.843 63.109l-30.668-17.704a1.09 1.09 0 01-.596-.84v-84.692c.016-37.685 30.593-68.236 68.281-68.236a68.332 68.332 0 0143.689 15.804 63.09 63.09 0 00-2.155 1.222l-72.54 41.9a11.794 11.794 0 00-5.961 10.248v.068l-.05 102.23zm16.655-35.91l39.445-22.782 39.444 22.767v45.55l-39.444 22.767-39.445-22.767v-45.535z"/></svg>`;
|
|
1230
|
+
case 'gemini':
|
|
1231
|
+
return `<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="${size}" height="${size}"${classAttr}><path d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z" fill="url(#prefix__paint0_radial_980_20147)"/><defs><radialGradient id="prefix__paint0_radial_980_20147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)"><stop offset=".067" stop-color="#9168C0"/><stop offset=".343" stop-color="#5684D1"/><stop offset=".672" stop-color="#1BA1E3"/></radialGradient></defs></svg>`;
|
|
1232
|
+
case 'grok':
|
|
1233
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 512 509.641"${classAttr}><path d="M115.612 0h280.776C459.975 0 512 52.026 512 115.612v278.416c0 63.587-52.025 115.613-115.612 115.613H115.612C52.026 509.641 0 457.615 0 394.028V115.612C0 52.026 52.026 0 115.612 0z"/><path fill="#fff" d="M213.235 306.019l178.976-180.002v.169l51.695-51.763c-.924 1.32-1.86 2.605-2.785 3.89-39.281 54.164-58.46 80.649-43.07 146.922l-.09-.101c10.61 45.11-.744 95.137-37.398 131.836-46.216 46.306-120.167 56.611-181.063 14.928l42.462-19.675c38.863 15.278 81.392 8.57 111.947-22.03 30.566-30.6 37.432-75.159 22.065-112.252-2.92-7.025-11.67-8.795-17.792-4.263l-124.947 92.341zm-25.786 22.437l-.033.034L68.094 435.217c7.565-10.429 16.957-20.294 26.327-30.149 26.428-27.803 52.653-55.359 36.654-94.302-21.422-52.112-8.952-113.177 30.724-152.898 41.243-41.254 101.98-51.661 152.706-30.758 11.23 4.172 21.016 10.114 28.638 15.639l-42.359 19.584c-39.44-16.563-84.629-5.299-112.207 22.313-37.298 37.308-44.84 102.003-1.128 143.81z"/></svg>`;
|
|
1234
|
+
case 'pdf':
|
|
1235
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 32 32" fill="none"${classAttr}><path d="M6 4a2 2 0 012-2h11l5 5v19a2 2 0 01-2 2H8a2 2 0 01-2-2V4z" fill="#DC2626"/><path d="M19 2v5h5" stroke="#B91C1C" stroke-width="1" fill="none"/><text x="16" y="25" font-size="5" fill="white" text-anchor="middle" font-weight="600">PDF</text></svg>`;
|
|
1236
|
+
case 'image':
|
|
1237
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><circle cx="12" cy="12" r="10"/></svg>`;
|
|
1238
|
+
case 'noImage':
|
|
1239
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21,15 16,10 5,21"/><line x1="3" y1="3" x2="21" y2="21"/></svg>`;
|
|
1240
|
+
default:
|
|
1241
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2"${classAttr}><circle cx="12" cy="12" r="10"/></svg>`;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
/**
|
|
1245
|
+
* Universal action icon function - works for both React and vanilla JS
|
|
1246
|
+
* @param action - Action type string
|
|
1247
|
+
* @param framework - Target framework ('react' or 'vanilla')
|
|
1248
|
+
* @param options - Icon options (only used for vanilla)
|
|
1249
|
+
* @returns React component or SVG string based on framework
|
|
1250
|
+
*/
|
|
1251
|
+
const getActionIcon = (action, framework = 'react', options = {}) => {
|
|
1252
|
+
const iconName = getIconNameForAction(action);
|
|
1253
|
+
if (!iconName)
|
|
1254
|
+
return null;
|
|
1255
|
+
if (framework === 'react') {
|
|
1256
|
+
return (jsx(Icon, { name: iconName, size: options.size || 'sm', className: options.className, color: options.color }));
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
return getIcon(iconName, { size: options.size || 'sm', ...options });
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
/**
|
|
1263
|
+
* Get action icon as React component (legacy compatibility)
|
|
1264
|
+
* @param action - Action type string
|
|
1265
|
+
* @returns React component or null
|
|
1266
|
+
*/
|
|
1267
|
+
const getActionIconReact = (action) => {
|
|
1268
|
+
return getActionIcon(action, 'react');
|
|
1269
|
+
};
|
|
1270
|
+
/**
|
|
1271
|
+
* Get provider icon name for AI providers
|
|
1272
|
+
* @param provider - Provider name (openai, claude, gemini)
|
|
1273
|
+
* @returns Icon name or null if not found
|
|
1274
|
+
*/
|
|
1275
|
+
const getProviderIconName = (provider) => {
|
|
1276
|
+
if (!provider)
|
|
1277
|
+
return null;
|
|
1278
|
+
switch (provider.toLowerCase()) {
|
|
1279
|
+
case 'openai':
|
|
1280
|
+
return 'openai';
|
|
1281
|
+
case 'claude':
|
|
1282
|
+
return 'claude';
|
|
1283
|
+
case 'gemini':
|
|
1284
|
+
return 'gemini';
|
|
1285
|
+
case 'grok':
|
|
1286
|
+
return 'grok';
|
|
1287
|
+
default:
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
class ErrorBoundary extends Component {
|
|
1293
|
+
constructor(props) {
|
|
1294
|
+
super(props);
|
|
1295
|
+
this.state = { hasError: false };
|
|
1296
|
+
}
|
|
1297
|
+
static getDerivedStateFromError(error) {
|
|
1298
|
+
return { hasError: true, error: error };
|
|
1299
|
+
}
|
|
1300
|
+
componentDidCatch(error, errorInfo) {
|
|
1301
|
+
console.error('AI Command Center Error Boundary caught an error:', error, errorInfo);
|
|
1302
|
+
}
|
|
1303
|
+
render() {
|
|
1304
|
+
if (this.state.hasError) {
|
|
1305
|
+
return (this.props.fallback || (jsx("div", { className: "p-4 bg-red-50 border border-red-200 rounded-lg max-w-md mx-auto", children: jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex-shrink-0 w-5 h-5 text-red-500 mt-0.5", children: jsx("svg", { fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", clipRule: "evenodd" }) }) }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("h3", { className: "text-red-800 font-semibold text-sm mb-2", children: "Something went wrong" }), jsx("p", { className: "text-red-700 text-sm mb-3 leading-relaxed", children: "The chat interface encountered an error. Please try refreshing the page or contact support if the problem persists." }), this.state.error && (jsxs("details", { className: "text-xs text-red-600", children: [jsx("summary", { className: "cursor-pointer hover:text-red-800 font-medium mb-1", children: "View error details" }), jsx("div", { className: "mt-2 p-2 bg-red-100 rounded border border-red-200 overflow-auto", children: jsxs("pre", { className: "whitespace-pre-wrap font-mono text-xs", children: [this.state.error.message, this.state.error.stack && (jsxs(Fragment, { children: ['\n\nStack trace:\n', this.state.error.stack] }))] }) })] })), jsxs("div", { className: "mt-3 flex gap-2", children: [jsx("button", { onClick: () => window.location.reload(), className: "px-3 py-1.5 bg-red-600 text-white text-xs font-medium rounded hover:bg-red-700 transition-colors", children: "Refresh Page" }), jsx("button", { onClick: () => this.setState({ hasError: false, error: undefined }), className: "px-3 py-1.5 bg-gray-100 text-gray-700 text-xs font-medium rounded hover:bg-gray-200 transition-colors", children: "Try Again" })] })] })] }) })));
|
|
1306
|
+
}
|
|
1307
|
+
return this.props.children;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Helper: determine whether an object contains any displayable (non-empty) values
|
|
1312
|
+
function hasDisplayableFields(value) {
|
|
1313
|
+
if (value === null || value === undefined)
|
|
1314
|
+
return false;
|
|
1315
|
+
if (typeof value === 'string')
|
|
1316
|
+
return value.trim().length > 0;
|
|
1317
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
1318
|
+
return true;
|
|
1319
|
+
if (Array.isArray(value))
|
|
1320
|
+
return value.some((v) => hasDisplayableFields(v));
|
|
1321
|
+
if (typeof value === 'object') {
|
|
1322
|
+
return Object.entries(value).some(([key, v]) => {
|
|
1323
|
+
if (key.startsWith('_') || key.startsWith('$'))
|
|
1324
|
+
return false;
|
|
1325
|
+
return hasDisplayableFields(v);
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
function ObjectViewer({ data, keyName, level = 0, isHighlighted = false, theme, }) {
|
|
1331
|
+
const [isExpanded, setIsExpanded] = useState(level === 0); // Auto-expand first level
|
|
1332
|
+
const isObject = data !== null && typeof data === 'object' && !Array.isArray(data);
|
|
1333
|
+
const isArray = Array.isArray(data);
|
|
1334
|
+
const isPrimitive = !isObject && !isArray;
|
|
1335
|
+
// Helper to format field names nicely
|
|
1336
|
+
const formatFieldName = (key) => {
|
|
1337
|
+
// Skip internal fields
|
|
1338
|
+
if (key.startsWith('_') || key.startsWith('$'))
|
|
1339
|
+
return '';
|
|
1340
|
+
// Convert camelCase to Title Case
|
|
1341
|
+
return key
|
|
1342
|
+
.replace(/([A-Z])/g, ' $1') // Add space before capital letters
|
|
1343
|
+
.replace(/^./, (str) => str.toUpperCase()) // Capitalize first letter
|
|
1344
|
+
.trim();
|
|
1345
|
+
};
|
|
1346
|
+
// Helper to format values nicely
|
|
1347
|
+
const formatValue = (value) => {
|
|
1348
|
+
if (value === null)
|
|
1349
|
+
return 'None';
|
|
1350
|
+
if (value === undefined)
|
|
1351
|
+
return 'Not set';
|
|
1352
|
+
if (typeof value === 'string') {
|
|
1353
|
+
// Filter out empty strings
|
|
1354
|
+
if (value === '')
|
|
1355
|
+
return '';
|
|
1356
|
+
return value; // No quotes
|
|
1357
|
+
}
|
|
1358
|
+
if (typeof value === 'boolean')
|
|
1359
|
+
return value ? 'Yes' : 'No';
|
|
1360
|
+
if (typeof value === 'number') {
|
|
1361
|
+
// Check if this looks like a Unix timestamp (milliseconds since epoch)
|
|
1362
|
+
// Typical range: 1000000000000 (2001) to 2000000000000 (2033)
|
|
1363
|
+
if (value > 1000000000000 && value < 2000000000000) {
|
|
1364
|
+
try {
|
|
1365
|
+
const date = new Date(value);
|
|
1366
|
+
// Only format if it's a valid date and not too far in the past/future
|
|
1367
|
+
if (!isNaN(date.getTime()) && date.getFullYear() > 1970 && date.getFullYear() < 2100) {
|
|
1368
|
+
return date.toLocaleDateString('en-US', {
|
|
1369
|
+
year: 'numeric',
|
|
1370
|
+
month: 'short',
|
|
1371
|
+
day: 'numeric',
|
|
1372
|
+
hour: '2-digit',
|
|
1373
|
+
minute: '2-digit',
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
catch (e) {
|
|
1378
|
+
// Fall through to default number formatting
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return value.toString();
|
|
1382
|
+
}
|
|
1383
|
+
return String(value);
|
|
1384
|
+
};
|
|
1385
|
+
// Helper to get array summary
|
|
1386
|
+
const getArraySummary = (arr) => {
|
|
1387
|
+
if (arr.length === 0)
|
|
1388
|
+
return 'No items';
|
|
1389
|
+
if (arr.length === 1)
|
|
1390
|
+
return '1 item';
|
|
1391
|
+
return `${arr.length} items`;
|
|
1392
|
+
};
|
|
1393
|
+
({
|
|
1394
|
+
color: isHighlighted
|
|
1395
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1396
|
+
: theme?.colors.text.primary || '#f9fafb',
|
|
1397
|
+
});
|
|
1398
|
+
const keyStyle = {
|
|
1399
|
+
color: isHighlighted
|
|
1400
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1401
|
+
: theme?.colors.text.secondary || '#d1d5db',
|
|
1402
|
+
fontWeight: '500',
|
|
1403
|
+
};
|
|
1404
|
+
const valueStyle = {
|
|
1405
|
+
color: isHighlighted
|
|
1406
|
+
? 'rgba(255, 255, 255, 0.9)'
|
|
1407
|
+
: theme?.colors.text.primary || '#f9fafb',
|
|
1408
|
+
};
|
|
1409
|
+
const metaStyle = {
|
|
1410
|
+
color: isHighlighted
|
|
1411
|
+
? 'rgba(255, 255, 255, 0.6)'
|
|
1412
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1413
|
+
fontSize: '11px',
|
|
1414
|
+
};
|
|
1415
|
+
// For root level (level 0), don't show the container, just show contents
|
|
1416
|
+
if (level === 0 && isObject) {
|
|
1417
|
+
const filteredEntries = Object.entries(data).filter(([key, value]) => {
|
|
1418
|
+
// Skip internal fields
|
|
1419
|
+
if (key.startsWith('_') || key.startsWith('$'))
|
|
1420
|
+
return false;
|
|
1421
|
+
// Skip empty values
|
|
1422
|
+
if (value === '' || value === null || value === undefined)
|
|
1423
|
+
return false;
|
|
1424
|
+
return true;
|
|
1425
|
+
});
|
|
1426
|
+
return (jsx("div", { className: "space-y-2", children: filteredEntries.map(([key, value]) => (jsx(ObjectViewer, { data: value, keyName: key, level: 1, isHighlighted: isHighlighted, theme: theme }, key))) }));
|
|
1427
|
+
}
|
|
1428
|
+
if (isPrimitive) {
|
|
1429
|
+
const formattedKey = formatFieldName(keyName || '');
|
|
1430
|
+
if (!formattedKey)
|
|
1431
|
+
return null; // Skip internal fields
|
|
1432
|
+
// Skip empty values
|
|
1433
|
+
const formattedValue = formatValue(data);
|
|
1434
|
+
if (formattedValue === '')
|
|
1435
|
+
return null;
|
|
1436
|
+
return (jsxs("div", { className: "flex items-start gap-2 py-1 text-sm", style: { marginLeft: `${(level - 1) * 16}px` }, children: [jsxs("span", { style: keyStyle, className: "min-w-0 flex-shrink-0", children: [formattedKey, ":"] }), jsx("span", { style: valueStyle, className: "flex-1 break-words", children: formattedValue })] }));
|
|
1437
|
+
}
|
|
1438
|
+
const formattedKey = formatFieldName(keyName || '');
|
|
1439
|
+
if (!formattedKey && keyName)
|
|
1440
|
+
return null; // Skip internal fields
|
|
1441
|
+
if (isArray) {
|
|
1442
|
+
return (jsxs("div", { style: { marginLeft: `${(level - 1) * 16}px` }, children: [jsxs("button", { onClick: (e) => {
|
|
1443
|
+
e.stopPropagation();
|
|
1444
|
+
setIsExpanded(!isExpanded);
|
|
1445
|
+
}, className: "flex items-center gap-2 py-1 hover:opacity-80 transition-opacity text-sm w-full text-left", children: [jsx("svg", { className: `w-3 h-3 transition-transform flex-shrink-0 ${isExpanded ? 'rotate-90' : ''}`, fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", clipRule: "evenodd" }) }), jsxs("span", { style: keyStyle, className: "font-medium", children: [formattedKey, ":"] }), jsx("span", { style: metaStyle, children: getArraySummary(data) })] }), isExpanded && (jsx("div", { className: "mt-1 space-y-1", children: data.map((item, index) => {
|
|
1446
|
+
const isItemObject = item !== null &&
|
|
1447
|
+
typeof item === 'object' &&
|
|
1448
|
+
!Array.isArray(item);
|
|
1449
|
+
if (isItemObject) {
|
|
1450
|
+
return (jsx("div", { style: { marginLeft: `${(level - 1) * 16 + 16}px` }, children: jsx(ObjectViewer, { data: item, keyName: `Item ${index + 1}`, level: level + 1, isHighlighted: isHighlighted, theme: theme }) }, index));
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
return (jsxs("div", { className: "flex items-center gap-2 py-1 text-sm", style: { marginLeft: `${(level - 1) * 16 + 16}px` }, children: [jsxs("span", { style: metaStyle, className: "text-xs", children: [index + 1, "."] }), jsx("span", { style: valueStyle, children: formatValue(item) })] }, index));
|
|
1454
|
+
}
|
|
1455
|
+
}) }))] }));
|
|
1456
|
+
}
|
|
1457
|
+
// Regular object
|
|
1458
|
+
return (jsxs("div", { style: { marginLeft: `${(level - 1) * 16}px` }, children: [jsxs("button", { onClick: (e) => {
|
|
1459
|
+
e.stopPropagation();
|
|
1460
|
+
setIsExpanded(!isExpanded);
|
|
1461
|
+
}, className: "flex items-center gap-2 py-1 hover:opacity-80 transition-opacity text-sm w-full text-left", children: [jsx("svg", { className: `w-3 h-3 transition-transform flex-shrink-0 ${isExpanded ? 'rotate-90' : ''}`, fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", clipRule: "evenodd" }) }), jsx("span", { style: keyStyle, className: "font-medium", children: formattedKey }), jsxs("span", { style: metaStyle, children: [Object.entries(data).filter(([key, value]) => {
|
|
1462
|
+
// Skip internal fields
|
|
1463
|
+
if (key.startsWith('_') || key.startsWith('$'))
|
|
1464
|
+
return false;
|
|
1465
|
+
// Skip empty values
|
|
1466
|
+
if (value === '' || value === null || value === undefined)
|
|
1467
|
+
return false;
|
|
1468
|
+
return true;
|
|
1469
|
+
}).length, ' ', "fields"] })] }), isExpanded && (jsx("div", { className: "mt-1 space-y-1", children: Object.entries(data)
|
|
1470
|
+
.filter(([key, value]) => {
|
|
1471
|
+
// Skip internal fields
|
|
1472
|
+
if (key.startsWith('_') || key.startsWith('$'))
|
|
1473
|
+
return false;
|
|
1474
|
+
// Skip empty values
|
|
1475
|
+
if (value === '' || value === null || value === undefined)
|
|
1476
|
+
return false;
|
|
1477
|
+
return true;
|
|
1478
|
+
})
|
|
1479
|
+
.map(([key, value]) => (jsx(ObjectViewer, { data: value, keyName: key, level: level + 1, isHighlighted: isHighlighted, theme: theme }, key))) }))] }));
|
|
1480
|
+
}
|
|
1481
|
+
function SuggestionCard({ suggestion, onClick, isHighlighted = false, theme, showIcon = true, }) {
|
|
1482
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
1483
|
+
return (jsxs("div", { className: "rounded-lg overflow-hidden transition-all duration-200 cursor-pointer group", style: {
|
|
1484
|
+
backgroundColor: isHighlighted
|
|
1485
|
+
? theme?.colors.primary[600] || '#2563eb'
|
|
1486
|
+
: theme?.colors.surface.primary || '#1f2937',
|
|
1487
|
+
border: `1px solid ${isHighlighted
|
|
1488
|
+
? theme?.colors.primary[500] || '#3b82f6'
|
|
1489
|
+
: theme?.colors.border.primary || '#374151'}`,
|
|
1490
|
+
}, onClick: () => onClick(suggestion), children: [jsx("div", { className: "p-3", children: jsxs("div", { className: "flex items-center gap-3", children: [showIcon && (jsx("div", { className: "flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center transition-colors", style: {
|
|
1491
|
+
backgroundColor: isHighlighted
|
|
1492
|
+
? theme?.colors.primary[700] || '#1d4ed8'
|
|
1493
|
+
: theme?.colors.surface.secondary || '#374151',
|
|
1494
|
+
}, children: jsx("span", { className: "text-sm", style: {
|
|
1495
|
+
color: isHighlighted
|
|
1496
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1497
|
+
: theme?.colors.primary[400] || '#60a5fa',
|
|
1498
|
+
}, children: getActionIconReact(suggestion.actionType) }) })), jsxs("div", { className: "flex-1 min-w-0", children: [jsxs("div", { className: "flex items-center gap-2 mb-1", children: [jsx("span", { className: "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium", style: {
|
|
1499
|
+
backgroundColor: isHighlighted
|
|
1500
|
+
? theme?.colors.primary[700] || '#1d4ed8'
|
|
1501
|
+
: theme?.colors.surface.secondary || '#374151',
|
|
1502
|
+
color: isHighlighted
|
|
1503
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1504
|
+
: theme?.colors.text.secondary || '#9ca3af',
|
|
1505
|
+
}, children: suggestion.actionType.toUpperCase() }), suggestion.shortcut && (jsx("span", { className: "text-xs font-mono", style: { color: theme?.colors.text.tertiary || '#6b7280' }, children: suggestion.shortcut }))] }), jsx("div", { className: "font-medium transition-colors mb-1", style: {
|
|
1506
|
+
color: isHighlighted
|
|
1507
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1508
|
+
: theme?.colors.text.primary || '#f9fafb',
|
|
1509
|
+
}, children: suggestion.action }), suggestion.message && (jsx("p", { className: "text-sm line-clamp-2", style: {
|
|
1510
|
+
color: isHighlighted
|
|
1511
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1512
|
+
: theme?.colors.text.secondary || '#d1d5db',
|
|
1513
|
+
}, children: suggestion.message }))] }), jsx("div", { className: "flex-shrink-0 transition-colors", style: {
|
|
1514
|
+
color: isHighlighted
|
|
1515
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1516
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1517
|
+
}, children: jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z", clipRule: "evenodd" }) }) })] }) }), ((suggestion.formState &&
|
|
1518
|
+
Object.keys(suggestion.formState).length > 0) ||
|
|
1519
|
+
suggestion.path ||
|
|
1520
|
+
(suggestion.message && !isExpanded)) && (jsxs("div", { className: "border-t", style: { borderColor: theme?.colors.border.primary || '#374151' }, children: [jsxs("button", { onClick: (e) => {
|
|
1521
|
+
e.stopPropagation();
|
|
1522
|
+
setIsExpanded(!isExpanded);
|
|
1523
|
+
}, className: "w-full px-3 py-2 text-xs transition-colors flex items-center justify-center gap-1", style: {
|
|
1524
|
+
color: isHighlighted
|
|
1525
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1526
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1527
|
+
backgroundColor: isHighlighted
|
|
1528
|
+
? 'rgba(255, 255, 255, 0.1)'
|
|
1529
|
+
: 'transparent',
|
|
1530
|
+
}, onMouseEnter: (e) => {
|
|
1531
|
+
if (!isHighlighted) {
|
|
1532
|
+
e.currentTarget.style.backgroundColor =
|
|
1533
|
+
theme?.colors.surface.secondary || '#374151';
|
|
1534
|
+
}
|
|
1535
|
+
}, onMouseLeave: (e) => {
|
|
1536
|
+
if (!isHighlighted) {
|
|
1537
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
1538
|
+
}
|
|
1539
|
+
}, children: [jsx("span", { children: isExpanded ? 'Show less' : 'Show details' }), jsx("svg", { className: `w-3 h-3 transition-transform ${isExpanded ? 'rotate-180' : ''}`, fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 101.415-1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) })] }), isExpanded && (jsxs("div", { className: "px-3 pb-3 space-y-2", style: {
|
|
1540
|
+
backgroundColor: isHighlighted
|
|
1541
|
+
? 'rgba(255, 255, 255, 0.05)'
|
|
1542
|
+
: theme?.colors.surface.primary || '#1f2937',
|
|
1543
|
+
}, children: [suggestion.message && (jsxs("div", { children: [jsx("div", { className: "text-xs mb-2 font-medium", style: {
|
|
1544
|
+
color: isHighlighted
|
|
1545
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1546
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1547
|
+
}, children: "Description:" }), jsx("p", { className: "text-sm leading-relaxed", style: {
|
|
1548
|
+
color: isHighlighted
|
|
1549
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1550
|
+
: theme?.colors.text.secondary || '#d1d5db',
|
|
1551
|
+
}, children: suggestion.message })] })), suggestion.formState &&
|
|
1552
|
+
hasDisplayableFields(suggestion.formState) && (jsxs("div", { children: [jsx("div", { className: "text-xs mb-2 font-medium", style: {
|
|
1553
|
+
color: isHighlighted
|
|
1554
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1555
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1556
|
+
}, children: "Will populate:" }), jsx("div", { className: "p-2 rounded border", style: {
|
|
1557
|
+
backgroundColor: isHighlighted
|
|
1558
|
+
? 'rgba(255, 255, 255, 0.05)'
|
|
1559
|
+
: theme?.colors.surface.primary || '#1f2937',
|
|
1560
|
+
borderColor: theme?.colors.border.primary || '#374151',
|
|
1561
|
+
}, children: jsx(ObjectViewer, { data: suggestion.formState, isHighlighted: isHighlighted, theme: theme }) })] })), suggestion.queryParams &&
|
|
1562
|
+
Object.keys(suggestion.queryParams).length > 0 && (jsxs("div", { children: [jsx("div", { className: "text-xs mb-2 font-medium", style: {
|
|
1563
|
+
color: isHighlighted
|
|
1564
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1565
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
1566
|
+
}, children: "Query parameters:" }), jsx("div", { className: "p-2 rounded border", style: {
|
|
1567
|
+
backgroundColor: isHighlighted
|
|
1568
|
+
? 'rgba(255, 255, 255, 0.05)'
|
|
1569
|
+
: theme?.colors.surface.primary || '#1f2937',
|
|
1570
|
+
borderColor: theme?.colors.border.primary || '#374151',
|
|
1571
|
+
}, children: jsx(ObjectViewer, { data: suggestion.queryParams, isHighlighted: isHighlighted, theme: theme }) })] })), suggestion.path && (jsxs("div", { className: "flex items-center gap-2 text-xs", children: [jsx("svg", { className: "w-3 h-3", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z", clipRule: "evenodd" }) }), jsxs("span", { style: {
|
|
1572
|
+
color: isHighlighted
|
|
1573
|
+
? theme?.colors.text.inverse || '#ffffff'
|
|
1574
|
+
: theme?.colors.text.secondary || '#d1d5db',
|
|
1575
|
+
}, children: ["Navigate to", ' ', jsx("span", { className: "font-medium", children: suggestion.path })] })] }))] }))] }))] }));
|
|
1576
|
+
}
|
|
1577
|
+
function SuggestionsPanel({ query, onSuggestionSelect, onClose, userId, formData, serverConfig, theme, isLoading = false, placeholder = 'Search for commands...', maxSuggestions = 10, showIcons = true, className = '', context, // <-- Add context prop
|
|
1578
|
+
}) {
|
|
1579
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
1580
|
+
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
|
1581
|
+
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState(false);
|
|
1582
|
+
const containerRef = useRef(null);
|
|
1583
|
+
// Build API URL from config
|
|
1584
|
+
const buildUrlFromConfig = (config) => {
|
|
1585
|
+
const protocol = config.secure !== undefined
|
|
1586
|
+
? config.secure
|
|
1587
|
+
? 'https:'
|
|
1588
|
+
: 'http:'
|
|
1589
|
+
: window.location.protocol;
|
|
1590
|
+
const hostname = config.domain || window.location.hostname;
|
|
1591
|
+
const port = config.port
|
|
1592
|
+
? `:${config.port}`
|
|
1593
|
+
: window.location.port
|
|
1594
|
+
? `:${window.location.port}`
|
|
1595
|
+
: '';
|
|
1596
|
+
const suffix = config.suffix || '';
|
|
1597
|
+
if (!config.domain && !config.port && !config.secure) {
|
|
1598
|
+
return suffix;
|
|
1599
|
+
}
|
|
1600
|
+
return `${protocol}//${hostname}${port}${suffix}`;
|
|
1601
|
+
};
|
|
1602
|
+
// Fetch suggestions when query changes
|
|
1603
|
+
useEffect(() => {
|
|
1604
|
+
if (!query.trim()) {
|
|
1605
|
+
setSuggestions([]);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const timeoutId = setTimeout(() => {
|
|
1609
|
+
fetchSuggestions(query);
|
|
1610
|
+
}, 300); // Debounce requests
|
|
1611
|
+
return () => clearTimeout(timeoutId);
|
|
1612
|
+
}, [query, userId, formData]);
|
|
1613
|
+
// Reset highlighted index when suggestions change
|
|
1614
|
+
useEffect(() => {
|
|
1615
|
+
setHighlightedIndex(0);
|
|
1616
|
+
}, [suggestions]);
|
|
1617
|
+
const fetchSuggestions = async (searchQuery) => {
|
|
1618
|
+
setIsLoadingSuggestions(true);
|
|
1619
|
+
try {
|
|
1620
|
+
const apiUrl = serverConfig ? buildUrlFromConfig(serverConfig) : '/api';
|
|
1621
|
+
const response = await fetch(`${apiUrl}/suggest`, {
|
|
1622
|
+
method: 'POST',
|
|
1623
|
+
headers: {
|
|
1624
|
+
'Content-Type': 'application/json',
|
|
1625
|
+
},
|
|
1626
|
+
body: JSON.stringify({
|
|
1627
|
+
query: searchQuery,
|
|
1628
|
+
userId,
|
|
1629
|
+
formData: formData || {},
|
|
1630
|
+
chatHistory: [], // Empty for standalone suggestions
|
|
1631
|
+
isCommandSearch: true, // Flag to indicate this is for command search
|
|
1632
|
+
...(context ? { context } : {}), // <-- Add context to request body
|
|
1633
|
+
}),
|
|
1634
|
+
});
|
|
1635
|
+
if (!response.ok) {
|
|
1636
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1637
|
+
}
|
|
1638
|
+
// Handle streaming response
|
|
1639
|
+
const reader = response.body?.getReader();
|
|
1640
|
+
if (!reader) {
|
|
1641
|
+
throw new Error('No response body');
|
|
1642
|
+
}
|
|
1643
|
+
const decoder = new TextDecoder();
|
|
1644
|
+
let buffer = '';
|
|
1645
|
+
let collectedSuggestions = [];
|
|
1646
|
+
while (true) {
|
|
1647
|
+
const { done, value } = await reader.read();
|
|
1648
|
+
if (done)
|
|
1649
|
+
break;
|
|
1650
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1651
|
+
const lines = buffer.split('\n');
|
|
1652
|
+
buffer = lines.pop() || '';
|
|
1653
|
+
for (const line of lines) {
|
|
1654
|
+
if (line.startsWith('data: ')) {
|
|
1655
|
+
try {
|
|
1656
|
+
const data = JSON.parse(line.slice(6));
|
|
1657
|
+
if (data.suggestions) {
|
|
1658
|
+
collectedSuggestions = data.suggestions;
|
|
1659
|
+
}
|
|
1660
|
+
else if (data.suggestion) {
|
|
1661
|
+
collectedSuggestions.push(data.suggestion);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
catch (e) {
|
|
1665
|
+
console.warn('Failed to parse SSE data:', line);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
else if (line.startsWith('event: end')) {
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
// Apply max suggestions limit
|
|
1674
|
+
const limitedSuggestions = collectedSuggestions.slice(0, maxSuggestions);
|
|
1675
|
+
setSuggestions(limitedSuggestions);
|
|
1676
|
+
}
|
|
1677
|
+
catch (error) {
|
|
1678
|
+
console.error('Error fetching suggestions:', error);
|
|
1679
|
+
setSuggestions([]);
|
|
1680
|
+
}
|
|
1681
|
+
finally {
|
|
1682
|
+
setIsLoadingSuggestions(false);
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
const handleSuggestionClick = (suggestion) => {
|
|
1686
|
+
onSuggestionSelect(suggestion);
|
|
1687
|
+
};
|
|
1688
|
+
const handleKeyDown = (event) => {
|
|
1689
|
+
if (suggestions.length === 0)
|
|
1690
|
+
return;
|
|
1691
|
+
switch (event.key) {
|
|
1692
|
+
case 'ArrowDown':
|
|
1693
|
+
event.preventDefault();
|
|
1694
|
+
setHighlightedIndex((prev) => prev < suggestions.length - 1 ? prev + 1 : 0);
|
|
1695
|
+
break;
|
|
1696
|
+
case 'ArrowUp':
|
|
1697
|
+
event.preventDefault();
|
|
1698
|
+
setHighlightedIndex((prev) => prev > 0 ? prev - 1 : suggestions.length - 1);
|
|
1699
|
+
break;
|
|
1700
|
+
case 'Enter':
|
|
1701
|
+
event.preventDefault();
|
|
1702
|
+
if (suggestions[highlightedIndex]) {
|
|
1703
|
+
handleSuggestionClick(suggestions[highlightedIndex]);
|
|
1704
|
+
}
|
|
1705
|
+
break;
|
|
1706
|
+
case 'Escape':
|
|
1707
|
+
event.preventDefault();
|
|
1708
|
+
onClose?.();
|
|
1709
|
+
break;
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
// Generate custom scrollbar styles based on theme
|
|
1713
|
+
const scrollbarStyles = `
|
|
1714
|
+
.suggestions-scrollbar {
|
|
1715
|
+
scrollbar-width: thin;
|
|
1716
|
+
scrollbar-color: ${theme?.colors.primary[500] || '#3b82f6'} ${theme?.colors.surface.secondary || '#374151'};
|
|
1717
|
+
}
|
|
1718
|
+
.suggestions-scrollbar::-webkit-scrollbar {
|
|
1719
|
+
width: 8px;
|
|
1720
|
+
height: 8px;
|
|
1721
|
+
}
|
|
1722
|
+
.suggestions-scrollbar::-webkit-scrollbar-track {
|
|
1723
|
+
background: ${theme?.colors.surface.secondary || '#374151'};
|
|
1724
|
+
border-radius: 4px;
|
|
1725
|
+
}
|
|
1726
|
+
.suggestions-scrollbar::-webkit-scrollbar-thumb {
|
|
1727
|
+
background: ${theme?.colors.primary[500] || '#3b82f6'};
|
|
1728
|
+
border-radius: 4px;
|
|
1729
|
+
border: 1px solid ${theme?.colors.surface.secondary || '#374151'};
|
|
1730
|
+
}
|
|
1731
|
+
.suggestions-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
1732
|
+
background: ${theme?.colors.primary[600] || '#2563eb'};
|
|
1733
|
+
}
|
|
1734
|
+
`;
|
|
1735
|
+
return (jsxs(Fragment, { children: [jsx("style", { children: scrollbarStyles }), jsxs("div", { ref: containerRef, className: `rounded-lg overflow-hidden ${className}`, style: {
|
|
1736
|
+
backgroundColor: theme?.colors.surface.elevated || '#1f2937',
|
|
1737
|
+
border: `1px solid ${theme?.colors.border.primary || '#374151'}`,
|
|
1738
|
+
boxShadow: theme?.shadows.xl ||
|
|
1739
|
+
'0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
1740
|
+
}, onKeyDown: handleKeyDown, tabIndex: 0, children: [(isLoading || isLoadingSuggestions) && (jsx("div", { className: "p-4 border-b", style: { borderColor: theme?.colors.border.primary || '#374151' }, children: jsxs("div", { className: "flex items-center gap-2 text-sm", style: { color: theme?.colors.text.secondary || '#d1d5db' }, children: [jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2", style: { borderColor: theme?.colors.primary[500] || '#3b82f6' } }), jsx("span", { children: "Finding suggestions..." })] }) })), suggestions.length > 0 && (jsx("div", { className: "max-h-80 overflow-y-auto suggestions-scrollbar", children: jsx("div", { className: "p-2 space-y-2", children: suggestions.map((suggestion, index) => (jsx(SuggestionCard, { suggestion: suggestion, onClick: handleSuggestionClick, theme: theme, showIcon: showIcons, isHighlighted: index === highlightedIndex }, `${suggestion.actionType}-${suggestion.action}-${index}`))) }) })), !isLoading &&
|
|
1741
|
+
!isLoadingSuggestions &&
|
|
1742
|
+
suggestions.length === 0 &&
|
|
1743
|
+
query.trim() && (jsxs("div", { className: "p-8 text-center", children: [jsx("svg", { className: "w-6 h-6 mx-auto mb-2", style: { color: theme?.colors.text.tertiary || '#9ca3af' }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }), jsxs("p", { className: "text-sm", style: { color: theme?.colors.text.secondary || '#d1d5db' }, children: ["No suggestions found for \"", query, "\""] }), jsx("p", { className: "text-xs mt-1", style: { color: theme?.colors.text.tertiary || '#9ca3af' }, children: "Try a different search term" })] })), !query.trim() && (jsxs("div", { className: "p-8 text-center", children: [jsx("svg", { className: "w-6 h-6 mx-auto mb-2", style: { color: theme?.colors.text.tertiary || '#9ca3af' }, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM22 16c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM17 6l-8 2.5" }) }), jsx("p", { className: "text-sm", style: { color: theme?.colors.text.secondary || '#d1d5db' }, children: "Start typing to search for commands..." }), jsx("p", { className: "text-xs mt-1", style: { color: theme?.colors.text.tertiary || '#9ca3af' }, children: "Use \u2191\u2193 to navigate, Enter to select" })] }))] })] }));
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const ACCEPTED_FILE_TYPES = {
|
|
1747
|
+
images: {
|
|
1748
|
+
mimeTypes: [
|
|
1749
|
+
'image/jpeg',
|
|
1750
|
+
'image/jpg',
|
|
1751
|
+
'image/png',
|
|
1752
|
+
'image/gif',
|
|
1753
|
+
'image/webp',
|
|
1754
|
+
],
|
|
1755
|
+
extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
|
|
1756
|
+
description: 'images (JPG, PNG, GIF, WebP)',
|
|
1757
|
+
},
|
|
1758
|
+
pdfs: {
|
|
1759
|
+
mimeTypes: ['application/pdf'],
|
|
1760
|
+
extensions: ['.pdf'],
|
|
1761
|
+
description: 'PDF documents',
|
|
1762
|
+
},
|
|
1763
|
+
textFiles: {
|
|
1764
|
+
mimeTypes: ['text/plain', 'text/rtf'],
|
|
1765
|
+
extensions: ['.txt', '.rtf'],
|
|
1766
|
+
description: 'text files (TXT, RTF)',
|
|
1767
|
+
},
|
|
1768
|
+
wordDocs: {
|
|
1769
|
+
mimeTypes: [
|
|
1770
|
+
'application/msword',
|
|
1771
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1772
|
+
],
|
|
1773
|
+
extensions: ['.doc', '.docx'],
|
|
1774
|
+
description: 'Word documents (DOC, DOCX)',
|
|
1775
|
+
},
|
|
1776
|
+
spreadsheets: {
|
|
1777
|
+
mimeTypes: [
|
|
1778
|
+
'application/vnd.ms-excel',
|
|
1779
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1780
|
+
'text/csv',
|
|
1781
|
+
],
|
|
1782
|
+
extensions: ['.xls', '.xlsx', '.csv'],
|
|
1783
|
+
description: 'spreadsheets and CSV files',
|
|
1784
|
+
},
|
|
1785
|
+
configFiles: {
|
|
1786
|
+
mimeTypes: ['application/json', 'text/xml', 'application/xml'],
|
|
1787
|
+
extensions: ['.json', '.xml'],
|
|
1788
|
+
description: 'configuration files (JSON, XML)',
|
|
1789
|
+
},
|
|
1790
|
+
};
|
|
1791
|
+
// Currently accepted file types - easy to modify
|
|
1792
|
+
const CURRENT_ACCEPTED_TYPES = [
|
|
1793
|
+
'images',
|
|
1794
|
+
// 'pdfs',
|
|
1795
|
+
'textFiles',
|
|
1796
|
+
// 'wordDocs',
|
|
1797
|
+
// 'spreadsheets',
|
|
1798
|
+
'configFiles',
|
|
1799
|
+
];
|
|
1800
|
+
/**
|
|
1801
|
+
* File processing utility
|
|
1802
|
+
*/
|
|
1803
|
+
const FileHandler = {
|
|
1804
|
+
// Get all accepted MIME types and extensions
|
|
1805
|
+
getAcceptedTypes() {
|
|
1806
|
+
const mimeTypes = [];
|
|
1807
|
+
const extensions = [];
|
|
1808
|
+
const descriptions = [];
|
|
1809
|
+
CURRENT_ACCEPTED_TYPES.forEach((typeKey) => {
|
|
1810
|
+
const config = ACCEPTED_FILE_TYPES[typeKey];
|
|
1811
|
+
if (config) {
|
|
1812
|
+
mimeTypes.push(...config.mimeTypes);
|
|
1813
|
+
extensions.push(...config.extensions);
|
|
1814
|
+
descriptions.push(config.description);
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
return { mimeTypes, extensions, descriptions };
|
|
1818
|
+
},
|
|
1819
|
+
// Check if a file is accepted
|
|
1820
|
+
isFileAccepted(file) {
|
|
1821
|
+
const { mimeTypes, extensions } = this.getAcceptedTypes();
|
|
1822
|
+
// Check MIME type
|
|
1823
|
+
if (mimeTypes.some((type) => file.type === type || file.type.startsWith(type.split('/')[0] + '/'))) {
|
|
1824
|
+
return true;
|
|
1825
|
+
}
|
|
1826
|
+
// Check file extension as fallback
|
|
1827
|
+
const fileName = file.name.toLowerCase();
|
|
1828
|
+
return extensions.some((ext) => fileName.endsWith(ext.toLowerCase()));
|
|
1829
|
+
},
|
|
1830
|
+
// Process a list of files and separate accepted from rejected
|
|
1831
|
+
processFiles(files) {
|
|
1832
|
+
const acceptedFiles = [];
|
|
1833
|
+
const rejectedFiles = [];
|
|
1834
|
+
files.forEach((file) => {
|
|
1835
|
+
if (this.isFileAccepted(file)) {
|
|
1836
|
+
acceptedFiles.push(file);
|
|
1837
|
+
}
|
|
1838
|
+
else {
|
|
1839
|
+
rejectedFiles.push(file);
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
return { acceptedFiles, rejectedFiles };
|
|
1843
|
+
},
|
|
1844
|
+
// Get user-friendly description of accepted types
|
|
1845
|
+
getAcceptedTypesDescription() {
|
|
1846
|
+
const { descriptions } = this.getAcceptedTypes();
|
|
1847
|
+
return descriptions.join(', ');
|
|
1848
|
+
},
|
|
1849
|
+
// Get accept attribute for HTML file input
|
|
1850
|
+
getInputAcceptAttribute() {
|
|
1851
|
+
const { mimeTypes, extensions } = this.getAcceptedTypes();
|
|
1852
|
+
return [...mimeTypes, ...extensions].join(',');
|
|
1853
|
+
},
|
|
1854
|
+
// Get appropriate icon for file type
|
|
1855
|
+
getFileIcon(file) {
|
|
1856
|
+
const fileName = file.name.toLowerCase();
|
|
1857
|
+
const fileType = file.type.toLowerCase();
|
|
1858
|
+
// PDF files
|
|
1859
|
+
if (fileType === 'application/pdf' || fileName.endsWith('.pdf')) {
|
|
1860
|
+
return jsx(Icon, { name: "pdf", size: 40 });
|
|
1861
|
+
}
|
|
1862
|
+
// Word documents
|
|
1863
|
+
if (fileType.includes('word') ||
|
|
1864
|
+
fileName.endsWith('.doc') ||
|
|
1865
|
+
fileName.endsWith('.docx')) {
|
|
1866
|
+
return jsx(Icon, { name: "word", size: 40 });
|
|
1867
|
+
}
|
|
1868
|
+
// Excel files
|
|
1869
|
+
if (fileType.includes('excel') ||
|
|
1870
|
+
fileType.includes('spreadsheet') ||
|
|
1871
|
+
fileName.endsWith('.xls') ||
|
|
1872
|
+
fileName.endsWith('.xlsx')) {
|
|
1873
|
+
return jsx(Icon, { name: "excel", size: 40 });
|
|
1874
|
+
}
|
|
1875
|
+
// CSV files
|
|
1876
|
+
if (fileType === 'text/csv' || fileName.endsWith('.csv')) {
|
|
1877
|
+
return jsx(Icon, { name: "csv", size: 40 });
|
|
1878
|
+
}
|
|
1879
|
+
// RTF files
|
|
1880
|
+
if (fileType === 'text/rtf' || fileName.endsWith('.rtf')) {
|
|
1881
|
+
return jsx(Icon, { name: "rtf", size: 40 });
|
|
1882
|
+
}
|
|
1883
|
+
// Plain text files
|
|
1884
|
+
if (fileType === 'text/plain' || fileName.endsWith('.txt')) {
|
|
1885
|
+
return jsx(Icon, { name: "text", size: 40 });
|
|
1886
|
+
}
|
|
1887
|
+
// Default generic file icon
|
|
1888
|
+
return jsx(Icon, { name: "file-generic", size: 40 });
|
|
1889
|
+
},
|
|
1890
|
+
// Format file size for display
|
|
1891
|
+
formatFileSize(bytes) {
|
|
1892
|
+
if (bytes === 0)
|
|
1893
|
+
return '0 B';
|
|
1894
|
+
const k = 1024;
|
|
1895
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1896
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1897
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
1898
|
+
},
|
|
1899
|
+
// Get file type description
|
|
1900
|
+
getFileTypeDescription(file) {
|
|
1901
|
+
const fileName = file.name.toLowerCase();
|
|
1902
|
+
const fileType = file.type.toLowerCase();
|
|
1903
|
+
if (fileType === 'application/pdf' || fileName.endsWith('.pdf'))
|
|
1904
|
+
return 'PDF Document';
|
|
1905
|
+
if (fileType.includes('word') ||
|
|
1906
|
+
fileName.endsWith('.doc') ||
|
|
1907
|
+
fileName.endsWith('.docx'))
|
|
1908
|
+
return 'Word Document';
|
|
1909
|
+
if (fileType.includes('excel') ||
|
|
1910
|
+
fileType.includes('spreadsheet') ||
|
|
1911
|
+
fileName.endsWith('.xls') ||
|
|
1912
|
+
fileName.endsWith('.xlsx'))
|
|
1913
|
+
return 'Excel Spreadsheet';
|
|
1914
|
+
if (fileType === 'text/csv' || fileName.endsWith('.csv'))
|
|
1915
|
+
return 'CSV File';
|
|
1916
|
+
if (fileType === 'text/rtf' || fileName.endsWith('.rtf'))
|
|
1917
|
+
return 'Rich Text Document';
|
|
1918
|
+
if (fileType === 'text/plain' || fileName.endsWith('.txt'))
|
|
1919
|
+
return 'Text File';
|
|
1920
|
+
if (fileType.startsWith('image/'))
|
|
1921
|
+
return 'Image';
|
|
1922
|
+
return 'Document';
|
|
1923
|
+
},
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
function useModelSwitcher({ initialProvider = DEFAULT_CONFIG.service, initialModel = DEFAULT_CONFIG.model, customModels, } = {}) {
|
|
1927
|
+
// Use default models or custom models
|
|
1928
|
+
const availableModels = customModels || DEFAULT_MODELS;
|
|
1929
|
+
// Simple state management - no API calls
|
|
1930
|
+
const [currentModel, setCurrentModel] = useState({
|
|
1931
|
+
provider: initialProvider,
|
|
1932
|
+
model: initialModel,
|
|
1933
|
+
});
|
|
1934
|
+
const [usageStats, setUsageStats] = useState({
|
|
1935
|
+
requests: 0,
|
|
1936
|
+
totalTokens: 0,
|
|
1937
|
+
totalComputeUnits: 0,
|
|
1938
|
+
modelSwitches: 0,
|
|
1939
|
+
uptime: 0,
|
|
1940
|
+
providers: [],
|
|
1941
|
+
currentProvider: initialProvider,
|
|
1942
|
+
currentModel: initialModel,
|
|
1943
|
+
});
|
|
1944
|
+
// Track session start time for uptime calculation
|
|
1945
|
+
const [sessionStartTime] = useState(Date.now());
|
|
1946
|
+
// Switch model function - just updates local state
|
|
1947
|
+
const switchModel = useCallback((model, provider) => {
|
|
1948
|
+
const newProvider = provider || currentModel.provider;
|
|
1949
|
+
// Update current model
|
|
1950
|
+
setCurrentModel({
|
|
1951
|
+
provider: newProvider,
|
|
1952
|
+
model: model,
|
|
1953
|
+
});
|
|
1954
|
+
// Update usage stats
|
|
1955
|
+
setUsageStats(prev => ({
|
|
1956
|
+
...prev,
|
|
1957
|
+
modelSwitches: prev.modelSwitches + 1,
|
|
1958
|
+
currentProvider: newProvider,
|
|
1959
|
+
currentModel: model,
|
|
1960
|
+
}));
|
|
1961
|
+
return { success: true, current: { provider: newProvider, model } };
|
|
1962
|
+
}, [currentModel.provider]);
|
|
1963
|
+
// Update usage stats when requests are made (called from streaming hook)
|
|
1964
|
+
const trackUsage = useCallback((tokens, computeUnits) => {
|
|
1965
|
+
setUsageStats(prev => ({
|
|
1966
|
+
...prev,
|
|
1967
|
+
requests: prev.requests + 1,
|
|
1968
|
+
totalTokens: prev.totalTokens + tokens,
|
|
1969
|
+
totalComputeUnits: prev.totalComputeUnits + computeUnits,
|
|
1970
|
+
uptime: Math.floor((Date.now() - sessionStartTime) / 1000),
|
|
1971
|
+
providers: prev.providers.map(p => p.name === currentModel.provider
|
|
1972
|
+
? { ...p, requests: p.requests + 1, computeUnits: p.computeUnits + computeUnits }
|
|
1973
|
+
: p).concat(
|
|
1974
|
+
// Add provider if not exists
|
|
1975
|
+
prev.providers.some(p => p.name === currentModel.provider)
|
|
1976
|
+
? []
|
|
1977
|
+
: [{
|
|
1978
|
+
name: currentModel.provider,
|
|
1979
|
+
requests: 1,
|
|
1980
|
+
computeUnits: computeUnits,
|
|
1981
|
+
models: [currentModel.model],
|
|
1982
|
+
}]),
|
|
1983
|
+
}));
|
|
1984
|
+
}, [currentModel.provider, currentModel.model, sessionStartTime]);
|
|
1985
|
+
// Reset usage stats
|
|
1986
|
+
const resetUsageStats = useCallback(() => {
|
|
1987
|
+
setUsageStats({
|
|
1988
|
+
requests: 0,
|
|
1989
|
+
totalTokens: 0,
|
|
1990
|
+
totalComputeUnits: 0,
|
|
1991
|
+
modelSwitches: 0,
|
|
1992
|
+
uptime: 0,
|
|
1993
|
+
providers: [],
|
|
1994
|
+
currentProvider: currentModel.provider,
|
|
1995
|
+
currentModel: currentModel.model,
|
|
1996
|
+
});
|
|
1997
|
+
}, [currentModel.provider, currentModel.model]);
|
|
1998
|
+
// Get current model capabilities
|
|
1999
|
+
const getCurrentCapabilities = useCallback(() => {
|
|
2000
|
+
const allModels = Object.values(availableModels).flat();
|
|
2001
|
+
const modelInfo = allModels.find(m => m.id === currentModel.model && m.provider === currentModel.provider);
|
|
2002
|
+
return {
|
|
2003
|
+
supportsImages: modelInfo?.supportsImages || false,
|
|
2004
|
+
computeWeight: modelInfo?.computeWeight || 1.0,
|
|
2005
|
+
};
|
|
2006
|
+
}, [currentModel, availableModels]);
|
|
2007
|
+
// Get current model info for streaming API
|
|
2008
|
+
const getCurrentModelInfo = useCallback(() => ({
|
|
2009
|
+
provider: currentModel.provider,
|
|
2010
|
+
model: currentModel.model,
|
|
2011
|
+
capabilities: getCurrentCapabilities(),
|
|
2012
|
+
}), [currentModel, getCurrentCapabilities]);
|
|
2013
|
+
// Flatten models for backward compatibility
|
|
2014
|
+
const models = Object.values(availableModels).flat();
|
|
2015
|
+
const modelsByProvider = availableModels;
|
|
2016
|
+
const currentCapabilities = getCurrentCapabilities();
|
|
2017
|
+
return {
|
|
2018
|
+
// Current state
|
|
2019
|
+
models,
|
|
2020
|
+
modelsByProvider,
|
|
2021
|
+
currentModel,
|
|
2022
|
+
currentCapabilities,
|
|
2023
|
+
usageStats,
|
|
2024
|
+
isLoading: false, // No loading since no API calls
|
|
2025
|
+
error: null,
|
|
2026
|
+
// Actions
|
|
2027
|
+
switchModel,
|
|
2028
|
+
trackUsage,
|
|
2029
|
+
resetUsageStats,
|
|
2030
|
+
getCurrentModelInfo, // New: for passing to streaming API
|
|
2031
|
+
// Backward compatibility
|
|
2032
|
+
modelState: {
|
|
2033
|
+
models,
|
|
2034
|
+
current: currentModel,
|
|
2035
|
+
capabilities: currentCapabilities,
|
|
2036
|
+
usage: usageStats,
|
|
2037
|
+
timestamp: Date.now(),
|
|
2038
|
+
},
|
|
2039
|
+
fetchModelState: () => Promise.resolve(), // No-op
|
|
2040
|
+
fetchModels: () => Promise.resolve(),
|
|
2041
|
+
fetchUsageStats: () => Promise.resolve(),
|
|
2042
|
+
fetchCurrentCapabilities: () => Promise.resolve(),
|
|
2043
|
+
integrationMode: 'local',
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/**
|
|
2048
|
+
* ModelSwitcher component for switching between AI models
|
|
2049
|
+
* Now uses the simplified useModelSwitcher hook directly
|
|
2050
|
+
* Uses Tailwind classes for styling
|
|
2051
|
+
*/
|
|
2052
|
+
function ModelSwitcher({ className = '', models, defaultModel, showUsageStats = false, // Default to false to match modal design
|
|
2053
|
+
theme, onModelSelectionChange, }) {
|
|
2054
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
2055
|
+
const [dropdownPosition, setDropdownPosition] = useState('above');
|
|
2056
|
+
const [dropdownStyle, setDropdownStyle] = useState({});
|
|
2057
|
+
const [hoveredModel, setHoveredModel] = useState(null);
|
|
2058
|
+
const dropdownRef = useRef(null);
|
|
2059
|
+
const buttonRef = useRef(null);
|
|
2060
|
+
const currentModelRef = useRef(null);
|
|
2061
|
+
// Use the simplified model switcher hook
|
|
2062
|
+
const { modelsByProvider, currentModel, currentCapabilities, usageStats, isLoading, error, switchModel, integrationMode, getCurrentModelInfo, } = useModelSwitcher({
|
|
2063
|
+
customModels: models,
|
|
2064
|
+
initialProvider: defaultModel?.provider,
|
|
2065
|
+
initialModel: defaultModel?.model,
|
|
2066
|
+
});
|
|
2067
|
+
const lastModelRef = useRef(null);
|
|
2068
|
+
useEffect(() => {
|
|
2069
|
+
if (onModelSelectionChange && currentModel && currentCapabilities) {
|
|
2070
|
+
const newModel = {
|
|
2071
|
+
provider: currentModel.provider,
|
|
2072
|
+
model: currentModel.model,
|
|
2073
|
+
capabilities: currentCapabilities,
|
|
2074
|
+
};
|
|
2075
|
+
// Simple shallow compare (customize if needed)
|
|
2076
|
+
const last = lastModelRef.current;
|
|
2077
|
+
if (!last ||
|
|
2078
|
+
last.provider !== newModel.provider ||
|
|
2079
|
+
last.model !== newModel.model ||
|
|
2080
|
+
JSON.stringify(last.capabilities) !==
|
|
2081
|
+
JSON.stringify(newModel.capabilities)) {
|
|
2082
|
+
onModelSelectionChange(newModel);
|
|
2083
|
+
lastModelRef.current = newModel;
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
}, [currentModel, currentCapabilities, onModelSelectionChange]);
|
|
2087
|
+
// Handle click outside to close dropdown
|
|
2088
|
+
useEffect(() => {
|
|
2089
|
+
const handleClickOutside = (event) => {
|
|
2090
|
+
if (dropdownRef.current &&
|
|
2091
|
+
!dropdownRef.current.contains(event.target)) {
|
|
2092
|
+
setIsOpen(false);
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
const handleResize = () => {
|
|
2096
|
+
if (isOpen) {
|
|
2097
|
+
calculateDropdownPosition();
|
|
2098
|
+
}
|
|
2099
|
+
};
|
|
2100
|
+
if (isOpen) {
|
|
2101
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
2102
|
+
window.addEventListener('resize', handleResize);
|
|
2103
|
+
}
|
|
2104
|
+
return () => {
|
|
2105
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
2106
|
+
window.removeEventListener('resize', handleResize);
|
|
2107
|
+
};
|
|
2108
|
+
}, [isOpen]);
|
|
2109
|
+
// Calculate optimal dropdown position using fixed positioning
|
|
2110
|
+
const calculateDropdownPosition = () => {
|
|
2111
|
+
if (!buttonRef.current)
|
|
2112
|
+
return;
|
|
2113
|
+
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
2114
|
+
const viewportWidth = window.innerWidth;
|
|
2115
|
+
const viewportHeight = window.innerHeight;
|
|
2116
|
+
// Dropdown width is min-w-80 (320px)
|
|
2117
|
+
const dropdownWidth = 320;
|
|
2118
|
+
const dropdownHeight = 300; // Approximate height
|
|
2119
|
+
const margin = 8; // Minimum margin from viewport edge
|
|
2120
|
+
// Calculate horizontal position
|
|
2121
|
+
let left = buttonRect.left;
|
|
2122
|
+
// Check if dropdown would go off right edge
|
|
2123
|
+
if (left + dropdownWidth > viewportWidth - margin) {
|
|
2124
|
+
// Try right-aligning to button
|
|
2125
|
+
const rightAlignedLeft = buttonRect.right - dropdownWidth;
|
|
2126
|
+
if (rightAlignedLeft >= margin) {
|
|
2127
|
+
left = rightAlignedLeft;
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
// Not enough space either way, clamp to viewport
|
|
2131
|
+
left = Math.max(margin, Math.min(left, viewportWidth - dropdownWidth - margin));
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
// Calculate vertical position
|
|
2135
|
+
let top;
|
|
2136
|
+
let bottom;
|
|
2137
|
+
const spaceAbove = buttonRect.top;
|
|
2138
|
+
const spaceBelow = viewportHeight - buttonRect.bottom;
|
|
2139
|
+
if (spaceAbove >= dropdownHeight || spaceAbove > spaceBelow) {
|
|
2140
|
+
// Position above
|
|
2141
|
+
bottom = viewportHeight - buttonRect.top + 4; // 4px gap
|
|
2142
|
+
setDropdownPosition('above');
|
|
2143
|
+
}
|
|
2144
|
+
else {
|
|
2145
|
+
// Position below
|
|
2146
|
+
top = buttonRect.bottom + 4; // 4px gap
|
|
2147
|
+
setDropdownPosition('below');
|
|
2148
|
+
}
|
|
2149
|
+
setDropdownStyle({
|
|
2150
|
+
position: 'fixed',
|
|
2151
|
+
left: `${left}px`,
|
|
2152
|
+
top: top !== undefined ? `${top}px` : undefined,
|
|
2153
|
+
bottom: bottom !== undefined ? `${bottom}px` : undefined,
|
|
2154
|
+
right: undefined,
|
|
2155
|
+
});
|
|
2156
|
+
};
|
|
2157
|
+
// Handle dropdown toggle with position calculation
|
|
2158
|
+
const handleToggleDropdown = () => {
|
|
2159
|
+
if (!isOpen) {
|
|
2160
|
+
calculateDropdownPosition();
|
|
2161
|
+
}
|
|
2162
|
+
setIsOpen(!isOpen);
|
|
2163
|
+
// Scroll to current model when opening
|
|
2164
|
+
if (!isOpen) {
|
|
2165
|
+
setTimeout(() => {
|
|
2166
|
+
if (currentModelRef.current) {
|
|
2167
|
+
currentModelRef.current.scrollIntoView({
|
|
2168
|
+
behavior: 'smooth',
|
|
2169
|
+
block: 'nearest',
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
}, 100);
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
// Simplified model switching
|
|
2176
|
+
const handleModelSwitch = async (modelId, provider) => {
|
|
2177
|
+
try {
|
|
2178
|
+
setIsOpen(false);
|
|
2179
|
+
switchModel(modelId, provider);
|
|
2180
|
+
}
|
|
2181
|
+
catch (err) {
|
|
2182
|
+
console.error('Failed to switch model:', err);
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
// Get current model name for display
|
|
2186
|
+
const getCurrentModelName = () => {
|
|
2187
|
+
if (!currentModel)
|
|
2188
|
+
return 'AI Model';
|
|
2189
|
+
// Find the actual model object to get the name
|
|
2190
|
+
const allModels = Object.values(modelsByProvider).flat();
|
|
2191
|
+
const modelObj = allModels.find((m) => m.id === currentModel.model && m.provider === currentModel.provider);
|
|
2192
|
+
return modelObj?.name || currentModel.model;
|
|
2193
|
+
};
|
|
2194
|
+
// Get model-specific usage for tooltip
|
|
2195
|
+
const getModelUsage = (modelId, provider) => {
|
|
2196
|
+
if (!usageStats)
|
|
2197
|
+
return null;
|
|
2198
|
+
const providerStats = usageStats.providers.find((p) => p.name === provider);
|
|
2199
|
+
if (!providerStats)
|
|
2200
|
+
return null;
|
|
2201
|
+
return {
|
|
2202
|
+
requests: providerStats.requests,
|
|
2203
|
+
computeUnits: providerStats.computeUnits,
|
|
2204
|
+
isActive: currentModel?.model === modelId && currentModel?.provider === provider,
|
|
2205
|
+
};
|
|
2206
|
+
};
|
|
2207
|
+
const getProviderIcon = (provider) => {
|
|
2208
|
+
const iconName = getProviderIconName(provider || '') || 'ai';
|
|
2209
|
+
return jsx(Icon, { name: iconName, size: "xs" });
|
|
2210
|
+
};
|
|
2211
|
+
const getProviderColorClass = (provider) => {
|
|
2212
|
+
if (!provider)
|
|
2213
|
+
return 'text-gray-500';
|
|
2214
|
+
switch (provider.toLowerCase()) {
|
|
2215
|
+
case 'openai':
|
|
2216
|
+
return 'text-green-500';
|
|
2217
|
+
case 'claude':
|
|
2218
|
+
return 'text-amber-500';
|
|
2219
|
+
case 'gemini':
|
|
2220
|
+
return 'text-blue-500';
|
|
2221
|
+
default:
|
|
2222
|
+
return 'text-gray-500';
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const formatUptime = (seconds) => {
|
|
2226
|
+
const hours = Math.floor(seconds / 3600);
|
|
2227
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
2228
|
+
if (hours > 0) {
|
|
2229
|
+
return `${hours}h ${minutes}m`;
|
|
2230
|
+
}
|
|
2231
|
+
return `${minutes}m`;
|
|
2232
|
+
};
|
|
2233
|
+
if (error) {
|
|
2234
|
+
return (jsx("span", { className: `text-xs text-red-500 cursor-help ${className}`, title: error, children: "\u26A0\uFE0F Model Error" }));
|
|
2235
|
+
}
|
|
2236
|
+
// Generate custom scrollbar styles based on theme
|
|
2237
|
+
const scrollbarStyles = `
|
|
2238
|
+
.custom-scrollbar {
|
|
2239
|
+
scrollbar-width: thin;
|
|
2240
|
+
scrollbar-color: ${theme?.colors.primary[500] || '#3b82f6'} ${theme?.colors.surface.secondary || '#f3f4f6'};
|
|
2241
|
+
}
|
|
2242
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
2243
|
+
width: 8px;
|
|
2244
|
+
height: 8px;
|
|
2245
|
+
}
|
|
2246
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
2247
|
+
background: ${theme?.colors.surface.secondary || '#f3f4f6'};
|
|
2248
|
+
border-radius: 4px;
|
|
2249
|
+
}
|
|
2250
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
2251
|
+
background: ${theme?.colors.primary[500] || '#3b82f6'};
|
|
2252
|
+
border-radius: 4px;
|
|
2253
|
+
border: 1px solid ${theme?.colors.surface.secondary || '#f3f4f6'};
|
|
2254
|
+
}
|
|
2255
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
2256
|
+
background: ${theme?.colors.primary[600] || '#2563eb'};
|
|
2257
|
+
}
|
|
2258
|
+
`;
|
|
2259
|
+
return (jsxs(Fragment, { children: [jsx("style", { children: scrollbarStyles }), jsxs("div", { className: `relative inline-block ${className}`, ref: dropdownRef, children: [jsx("button", { ref: buttonRef, onClick: handleToggleDropdown, disabled: isLoading, className: `flex items-center gap-2 text-xs text-gray-500 bg-transparent border-none cursor-pointer p-1 rounded transition-all duration-200 font-inherit hover:text-blue-500 disabled:opacity-50 disabled:cursor-not-allowed`, title: `Switch AI Model (${integrationMode} mode)`, children: currentModel ? (jsxs(Fragment, { children: [jsx("span", { className: getProviderColorClass(currentModel.provider), children: getProviderIcon(currentModel.provider) }), jsx("span", { children: getCurrentModelName() }), jsx(Icon, { name: "plus", size: "xs", className: isLoading ? 'animate-spin' : '' })] })) : (jsxs(Fragment, { children: [jsx(Icon, { name: "ai", size: "xs" }), jsx("span", { children: "AI Model" }), isLoading && (jsx(Icon, { name: "plus", size: "xs", className: "animate-spin" }))] })) }), isOpen && (jsxs("div", { className: "z-50 min-w-80 rounded-lg overflow-hidden", style: {
|
|
2260
|
+
...dropdownStyle,
|
|
2261
|
+
backgroundColor: theme?.colors.surface.elevated || '#ffffff',
|
|
2262
|
+
borderColor: theme?.colors.border.primary || '#e5e7eb',
|
|
2263
|
+
borderWidth: '1px',
|
|
2264
|
+
borderStyle: 'solid',
|
|
2265
|
+
borderRadius: theme?.borderRadius.lg || '0.5rem',
|
|
2266
|
+
boxShadow: theme?.shadows.xl ||
|
|
2267
|
+
'0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
|
2268
|
+
}, children: [showUsageStats && usageStats && (jsx("div", { className: "px-3 pt-2 pb-3 border-b", style: {
|
|
2269
|
+
borderColor: theme?.colors.border.primary || '#e5e7eb',
|
|
2270
|
+
}, children: jsxs("div", { className: "flex items-center gap-4 text-xs", children: [jsxs("span", { className: "font-medium text-blue-600", children: [usageStats.requests, " requests"] }), jsxs("span", { className: "font-medium text-green-600", children: [usageStats.modelSwitches, " switches"] }), jsxs("span", { className: "font-medium text-purple-600", children: [Math.round(usageStats.totalComputeUnits), " compute units"] }), jsxs("span", { className: "font-medium text-orange-600", children: [formatUptime(usageStats.uptime), " uptime"] })] }) })), jsx("div", { className: "max-h-64 overflow-y-auto custom-scrollbar", children: jsx("div", { className: "py-2", children: Object.entries(modelsByProvider).map(([provider, models]) => (jsxs("div", { className: "py-1", children: [jsxs("div", { className: "py-3 px-3 flex items-center gap-2 border-b border-opacity-50", style: {
|
|
2271
|
+
borderColor: theme?.colors.border.primary || '#374151',
|
|
2272
|
+
}, children: [jsx("span", { className: getProviderColorClass(provider), children: getProviderIcon(provider) }), jsx("span", { className: "text-sm font-semibold tracking-wide", style: {
|
|
2273
|
+
color: theme?.colors.text.primary || '#f9fafb',
|
|
2274
|
+
}, children: provider.toUpperCase() })] }), models.map((model) => {
|
|
2275
|
+
const modelUsage = getModelUsage(model.id, provider);
|
|
2276
|
+
const isCurrentModel = currentModel?.model === model.id &&
|
|
2277
|
+
currentModel?.provider === provider;
|
|
2278
|
+
return (jsxs("div", { className: "relative", children: [jsx("button", { ref: isCurrentModel ? currentModelRef : null, onClick: () => handleModelSwitch(model.id, provider), disabled: isLoading || !model.available, onMouseEnter: (e) => {
|
|
2279
|
+
setHoveredModel(`${provider}-${model.id}`);
|
|
2280
|
+
if (model.available && !isCurrentModel) {
|
|
2281
|
+
e.currentTarget.style.backgroundColor =
|
|
2282
|
+
theme?.colors.surface.secondary || '#f3f4f6';
|
|
2283
|
+
}
|
|
2284
|
+
}, onMouseLeave: (e) => {
|
|
2285
|
+
setHoveredModel(null);
|
|
2286
|
+
if (model.available && !isCurrentModel) {
|
|
2287
|
+
e.currentTarget.style.backgroundColor =
|
|
2288
|
+
'transparent';
|
|
2289
|
+
}
|
|
2290
|
+
}, className: "w-full text-left py-2 px-4 text-xs border-none bg-transparent cursor-pointer transition-all duration-200 relative", style: {
|
|
2291
|
+
backgroundColor: isCurrentModel
|
|
2292
|
+
? theme?.colors.surface.secondary || '#f3f4f6'
|
|
2293
|
+
: 'transparent',
|
|
2294
|
+
color: isCurrentModel
|
|
2295
|
+
? theme?.colors.text.primary || '#111827'
|
|
2296
|
+
: model.available
|
|
2297
|
+
? theme?.colors.text.primary || '#374151'
|
|
2298
|
+
: theme?.colors.text.tertiary || '#9ca3af',
|
|
2299
|
+
opacity: model.available ? 1 : 0.5,
|
|
2300
|
+
cursor: model.available
|
|
2301
|
+
? 'pointer'
|
|
2302
|
+
: 'not-allowed',
|
|
2303
|
+
border: isCurrentModel
|
|
2304
|
+
? `1px solid ${theme?.colors.border.primary || '#d1d5db'}`
|
|
2305
|
+
: '1px solid transparent',
|
|
2306
|
+
borderRadius: theme?.borderRadius.md || '0.375rem',
|
|
2307
|
+
}, title: !model.available
|
|
2308
|
+
? `Unavailable: ${model.error || 'No API key configured'}`
|
|
2309
|
+
: `Switch to ${model.name} (CW: ${model.computeWeight})`, children: jsxs("div", { className: "flex items-center justify-between", children: [jsxs("div", { className: "flex-1", children: [jsx("div", { className: `font-medium mb-0.5 ${model.available ? '' : 'line-through'}`, children: model.name }), jsxs("div", { className: "text-xs flex items-center gap-2", style: {
|
|
2310
|
+
color: theme?.colors.text.tertiary || '#9ca3af',
|
|
2311
|
+
}, children: [jsx("span", { children: model.id }), jsxs("div", { className: "flex items-center gap-1", children: [jsx("div", { className: "w-1.5 h-1.5 rounded-full", style: {
|
|
2312
|
+
backgroundColor: isCurrentModel
|
|
2313
|
+
? theme?.colors.text.secondary ||
|
|
2314
|
+
'#6b7280'
|
|
2315
|
+
: theme?.colors.text.tertiary ||
|
|
2316
|
+
'#9ca3af',
|
|
2317
|
+
} }), jsx("span", { className: "text-xs font-medium", style: {
|
|
2318
|
+
color: isCurrentModel
|
|
2319
|
+
? theme?.colors.text.secondary ||
|
|
2320
|
+
'#6b7280'
|
|
2321
|
+
: theme?.colors.text.tertiary ||
|
|
2322
|
+
'#9ca3af',
|
|
2323
|
+
}, children: model.computeWeight })] })] })] }), jsxs("div", { className: "flex items-center gap-2", children: [jsx("div", { className: "flex items-center justify-center w-5 h-5", title: model.supportsImages
|
|
2324
|
+
? 'Supports image input'
|
|
2325
|
+
: 'Text only', children: model.supportsImages ? (
|
|
2326
|
+
// Image support icon
|
|
2327
|
+
jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", style: {
|
|
2328
|
+
color: isCurrentModel
|
|
2329
|
+
? theme?.colors.text.secondary ||
|
|
2330
|
+
'#6b7280'
|
|
2331
|
+
: theme?.colors.text.tertiary ||
|
|
2332
|
+
'#9ca3af',
|
|
2333
|
+
}, children: [jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2", stroke: "currentColor", strokeWidth: "2", fill: "none" }), jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5", fill: "currentColor" }), jsx("polyline", { points: "21,15 16,10 5,21", stroke: "currentColor", strokeWidth: "2", fill: "none" })] })) : (
|
|
2334
|
+
// No image support icon (crossed out image)
|
|
2335
|
+
jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", style: {
|
|
2336
|
+
color: theme?.colors.text.tertiary ||
|
|
2337
|
+
'#9ca3af',
|
|
2338
|
+
opacity: 0.5,
|
|
2339
|
+
}, children: [jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2", stroke: "currentColor", strokeWidth: "2", fill: "none" }), jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5", fill: "currentColor" }), jsx("polyline", { points: "21,15 16,10 5,21", stroke: "currentColor", strokeWidth: "2", fill: "none" }), jsx("line", { x1: "3", y1: "3", x2: "21", y2: "21", stroke: "currentColor", strokeWidth: "2" })] })) }), modelUsage && modelUsage.requests > 0 && (jsx("span", { className: "text-xs text-blue-600 bg-blue-50 px-1 py-0.5 rounded", children: modelUsage.requests })), !model.available && (jsx("span", { className: "text-xs px-2 py-0.5 rounded", style: {
|
|
2340
|
+
backgroundColor: theme?.colors.surface.secondary ||
|
|
2341
|
+
'#f3f4f6',
|
|
2342
|
+
color: theme?.colors.text.tertiary ||
|
|
2343
|
+
'#9ca3af',
|
|
2344
|
+
}, title: "API key required", children: "Unavailable" }))] })] }) }), hoveredModel === `${provider}-${model.id}` &&
|
|
2345
|
+
modelUsage && (jsxs("div", { className: "absolute right-full top-0 mr-2 z-50 bg-gray-900 text-white text-xs rounded p-2 whitespace-nowrap shadow-lg", children: [jsx("div", { className: "font-medium mb-0.5", children: model.name }), jsxs("div", { className: "text-gray-300", children: [modelUsage.requests, " requests \u2022", ' ', Math.round(modelUsage.computeUnits), " CU", jsx("br", {}), modelUsage.isActive ? 'Active' : 'Available', model.supportsImages
|
|
2346
|
+
? ' • Images supported'
|
|
2347
|
+
: ' • No image support'] }), jsx("div", { className: "absolute top-2 right-[-4px] w-2 h-2 bg-gray-900 transform rotate-45" })] }))] }, model.id));
|
|
2348
|
+
})] }, provider))) }) })] }))] })] }));
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
/**
|
|
2352
|
+
* formatTimestamp - formats the timestamp to a human readable format
|
|
2353
|
+
* @param timestamp - The timestamp to format
|
|
2354
|
+
* @returns The formatted timestamp
|
|
2355
|
+
*/
|
|
2356
|
+
const formatTimestamp = (timestamp) => {
|
|
2357
|
+
if (!timestamp)
|
|
2358
|
+
return '';
|
|
2359
|
+
const date = new Date(timestamp);
|
|
2360
|
+
const now = new Date();
|
|
2361
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
2362
|
+
if (isToday) {
|
|
2363
|
+
return date.toLocaleTimeString([], {
|
|
2364
|
+
hour: '2-digit',
|
|
2365
|
+
minute: '2-digit',
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
else {
|
|
2369
|
+
return date.toLocaleDateString([], {
|
|
2370
|
+
month: 'short',
|
|
2371
|
+
day: 'numeric',
|
|
2372
|
+
hour: '2-digit',
|
|
2373
|
+
minute: '2-digit',
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
/**
|
|
2378
|
+
* Helper to extract text content from message content blocks
|
|
2379
|
+
*/
|
|
2380
|
+
const getTextFromMessage$1 = (msg) => {
|
|
2381
|
+
return msg.content
|
|
2382
|
+
.filter((block) => block.type === 'text')
|
|
2383
|
+
.map((block) => block.content)
|
|
2384
|
+
.join('\n');
|
|
2385
|
+
};
|
|
2386
|
+
/**
|
|
2387
|
+
* Conversation component - displays a single chat conversation
|
|
2388
|
+
* @param chat - The chat messages to display
|
|
2389
|
+
* @param onSuggestionClick - Function to handle suggestion clicks
|
|
2390
|
+
* @param theme - Chat theme
|
|
2391
|
+
* @param showProfileBubbles - Whether to show profile bubbles
|
|
2392
|
+
* @param className - Additional CSS classes
|
|
2393
|
+
* @param autoScroll - Whether to auto-scroll to bottom on new messages
|
|
2394
|
+
* @param maxHeight - Maximum height of the conversation container
|
|
2395
|
+
*/
|
|
2396
|
+
function Conversation({ chat: externalChat, onSuggestionClick, theme, showProfileBubbles = false, className = '', autoScroll = true, maxHeight = '100%',
|
|
2397
|
+
// AI functionality props
|
|
2398
|
+
userId = 'demo-user', serverConfig, formData, setFormState, onNavigate, chatLevel = 'full',
|
|
2399
|
+
// External chat management
|
|
2400
|
+
chats: externalChats, setChats: externalSetChats, currentChatId: externalCurrentChatId, setCurrentChatId: externalSetCurrentChatId,
|
|
2401
|
+
// Input-related props
|
|
2402
|
+
query: externalQuery = '', setQuery: externalSetQuery, onSend: externalOnSend, isLoading: externalIsLoading = false, attachedFiles: externalAttachedFiles = [], setAttachedFiles: externalSetAttachedFiles,
|
|
2403
|
+
// Model configuration
|
|
2404
|
+
models, defaultModel, showUsageStats, maxFileSize, features = {},
|
|
2405
|
+
// Model selection props
|
|
2406
|
+
currentModelSelection: externalCurrentModelSelection, onModelSelectionChange: externalOnModelSelectionChange,
|
|
2407
|
+
// Show input field (always shown by default)
|
|
2408
|
+
showInput = true,
|
|
2409
|
+
// Initial query handling
|
|
2410
|
+
initialQuery, setInitialQuery,
|
|
2411
|
+
// Clear chat functionality
|
|
2412
|
+
showClearChat = false, onClearChat,
|
|
2413
|
+
// Additional context for AI requests
|
|
2414
|
+
additionalContext, }) {
|
|
2415
|
+
const chatRef = useRef(null);
|
|
2416
|
+
// Internal state for AI functionality
|
|
2417
|
+
const [query, setQuery] = useState(externalQuery);
|
|
2418
|
+
const [attachedFiles, setAttachedFiles] = useState(externalAttachedFiles);
|
|
2419
|
+
const [currentModelSelection, setCurrentModelSelection] = useState(externalCurrentModelSelection);
|
|
2420
|
+
// Use the simplified model switcher hook
|
|
2421
|
+
const { trackUsage } = useModelSwitcher({
|
|
2422
|
+
customModels: models,
|
|
2423
|
+
initialProvider: defaultModel?.provider,
|
|
2424
|
+
initialModel: defaultModel?.model,
|
|
2425
|
+
});
|
|
2426
|
+
// Use streaming AI hook for AI functionality
|
|
2427
|
+
const streamingAI = useStreamingAI({
|
|
2428
|
+
userId,
|
|
2429
|
+
serverConfig,
|
|
2430
|
+
formData,
|
|
2431
|
+
chats: externalChats,
|
|
2432
|
+
setChats: externalSetChats,
|
|
2433
|
+
currentChatId: externalCurrentChatId,
|
|
2434
|
+
setCurrentChatId: externalSetCurrentChatId,
|
|
2435
|
+
modelSelection: currentModelSelection,
|
|
2436
|
+
onUsageTracked: trackUsage,
|
|
2437
|
+
chatLevel,
|
|
2438
|
+
additionalContext,
|
|
2439
|
+
});
|
|
2440
|
+
// Handle model selection changes
|
|
2441
|
+
const handleModelSelectionChange = useCallback((modelInfo) => {
|
|
2442
|
+
setCurrentModelSelection(modelInfo);
|
|
2443
|
+
externalOnModelSelectionChange?.(modelInfo);
|
|
2444
|
+
}, [externalOnModelSelectionChange]);
|
|
2445
|
+
// Handle sending messages
|
|
2446
|
+
const handleSend = useCallback((additionalContext, queryToSend) => {
|
|
2447
|
+
const finalQuery = queryToSend || query;
|
|
2448
|
+
streamingAI.sendQuery(finalQuery, attachedFiles.length > 0 ? attachedFiles : undefined, additionalContext);
|
|
2449
|
+
setQuery('');
|
|
2450
|
+
setAttachedFiles([]); // Clear files after sending
|
|
2451
|
+
externalSetQuery?.('');
|
|
2452
|
+
externalSetAttachedFiles?.([]);
|
|
2453
|
+
}, [query, attachedFiles, streamingAI, externalSetQuery, externalSetAttachedFiles]);
|
|
2454
|
+
// Handle suggestion clicks
|
|
2455
|
+
const handleSuggestionClick = useCallback((s) => {
|
|
2456
|
+
if (setFormState && s.formState)
|
|
2457
|
+
setFormState(s.formState, s.actionType);
|
|
2458
|
+
if (s.path && onNavigate)
|
|
2459
|
+
onNavigate(s.path);
|
|
2460
|
+
onSuggestionClick(s);
|
|
2461
|
+
}, [setFormState, onNavigate, onSuggestionClick]);
|
|
2462
|
+
// Use external or internal state
|
|
2463
|
+
const chat = externalChat || streamingAI.chat;
|
|
2464
|
+
const isLoading = externalIsLoading || streamingAI.isLoading;
|
|
2465
|
+
const finalQuery = externalQuery || query;
|
|
2466
|
+
const finalSetQuery = externalSetQuery || setQuery;
|
|
2467
|
+
const finalOnSend = externalOnSend || handleSend;
|
|
2468
|
+
const finalAttachedFiles = externalAttachedFiles.length > 0 ? externalAttachedFiles : attachedFiles;
|
|
2469
|
+
const finalSetAttachedFiles = externalSetAttachedFiles || setAttachedFiles;
|
|
2470
|
+
// Auto-scroll to bottom when new messages arrive
|
|
2471
|
+
useEffect(() => {
|
|
2472
|
+
if (autoScroll && chatRef.current) {
|
|
2473
|
+
chatRef.current.scrollTo({
|
|
2474
|
+
top: chatRef.current.scrollHeight,
|
|
2475
|
+
behavior: 'smooth',
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}, [chat, autoScroll]);
|
|
2479
|
+
// Handle initial query auto-send
|
|
2480
|
+
const initialQuerySentRef = useRef(false);
|
|
2481
|
+
useEffect(() => {
|
|
2482
|
+
if (initialQuery &&
|
|
2483
|
+
!streamingAI.isLoading &&
|
|
2484
|
+
!initialQuerySentRef.current) {
|
|
2485
|
+
// Set the query first
|
|
2486
|
+
setQuery(initialQuery.query || '');
|
|
2487
|
+
// Use a delay to ensure state is updated and component is ready
|
|
2488
|
+
const timer = setTimeout(() => {
|
|
2489
|
+
// Check if we have a meaningful query or context to send
|
|
2490
|
+
const hasQuery = initialQuery.query && initialQuery.query.trim().length > 0;
|
|
2491
|
+
const hasContext = initialQuery.context && initialQuery.context.trim().length > 0;
|
|
2492
|
+
if (hasQuery || hasContext) {
|
|
2493
|
+
handleSend(initialQuery.context, initialQuery.query);
|
|
2494
|
+
initialQuerySentRef.current = true; // Mark as sent
|
|
2495
|
+
}
|
|
2496
|
+
// Always clear the initial query after processing
|
|
2497
|
+
setInitialQuery?.({ query: '', context: '' });
|
|
2498
|
+
}, 200);
|
|
2499
|
+
return () => clearTimeout(timer);
|
|
2500
|
+
}
|
|
2501
|
+
}, [initialQuery, streamingAI.isLoading, handleSend, setInitialQuery]);
|
|
2502
|
+
// Reset the sent flag when initialQuery changes
|
|
2503
|
+
useEffect(() => {
|
|
2504
|
+
if (initialQuery) {
|
|
2505
|
+
initialQuerySentRef.current = false;
|
|
2506
|
+
}
|
|
2507
|
+
}, [initialQuery]);
|
|
2508
|
+
// Scrollbar styles - dynamically themed
|
|
2509
|
+
const scrollbarStyles = `
|
|
2510
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
2511
|
+
width: 6px !important;
|
|
2512
|
+
}
|
|
2513
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
2514
|
+
background: ${theme.colors.background.secondary} !important;
|
|
2515
|
+
border-radius: 3px !important;
|
|
2516
|
+
}
|
|
2517
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
2518
|
+
background: ${theme.colors.border.secondary} !important;
|
|
2519
|
+
border-radius: 3px !important;
|
|
2520
|
+
}
|
|
2521
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
2522
|
+
background: ${theme.colors.text.tertiary} !important;
|
|
2523
|
+
}
|
|
2524
|
+
.custom-scrollbar::-webkit-scrollbar-corner {
|
|
2525
|
+
background: ${theme.colors.background.secondary} !important;
|
|
2526
|
+
}
|
|
2527
|
+
/* Firefox scrollbar */
|
|
2528
|
+
.custom-scrollbar {
|
|
2529
|
+
scrollbar-width: thin !important;
|
|
2530
|
+
scrollbar-color: ${theme.colors.border.secondary} ${theme.colors.background.secondary} !important;
|
|
2531
|
+
}
|
|
2532
|
+
`;
|
|
2533
|
+
return (jsxs(Fragment, { children: [jsx("style", { children: scrollbarStyles }), jsxs("div", { className: "flex flex-col h-full", style: {
|
|
2534
|
+
maxHeight: maxHeight,
|
|
2535
|
+
}, children: [jsx("div", { ref: chatRef, className: `overflow-y-auto space-y-4 min-h-0 custom-scrollbar px-3 py-4 flex-1 ${className}`, style: {
|
|
2536
|
+
backgroundColor: theme.colors.background.primary,
|
|
2537
|
+
}, children: jsx(ChatBubbles, { chat: chat, onSuggestionClick: handleSuggestionClick, theme: theme, showProfileBubbles: showProfileBubbles }) }), showClearChat && onClearChat && chat.length > 0 && (jsx("div", { className: "px-4 pb-2", style: { flexShrink: 0 }, children: jsxs("button", { onClick: onClearChat, className: "w-full text-sm py-2 px-3 rounded-lg transition-colors border flex items-center justify-center gap-2", style: {
|
|
2538
|
+
color: theme.colors.text.secondary,
|
|
2539
|
+
backgroundColor: theme.colors.surface.primary,
|
|
2540
|
+
borderColor: theme.colors.border.primary,
|
|
2541
|
+
}, onMouseEnter: (e) => {
|
|
2542
|
+
e.currentTarget.style.color = theme.colors.state.error.text;
|
|
2543
|
+
e.currentTarget.style.backgroundColor = theme.colors.state.error.background;
|
|
2544
|
+
e.currentTarget.style.borderColor = theme.colors.state.error.border;
|
|
2545
|
+
}, onMouseLeave: (e) => {
|
|
2546
|
+
e.currentTarget.style.color = theme.colors.text.secondary;
|
|
2547
|
+
e.currentTarget.style.backgroundColor = theme.colors.surface.primary;
|
|
2548
|
+
e.currentTarget.style.borderColor = theme.colors.border.primary;
|
|
2549
|
+
}, title: "Clear chat history", "aria-label": "Clear chat", children: [jsx(Icon, { name: "trash", size: "sm" }), "Clear Chat History"] }) })), showInput && (jsx("div", { style: { flexShrink: 0 }, children: jsx(ChatInput, { query: finalQuery, setQuery: finalSetQuery, onSend: finalOnSend, isLoading: isLoading, inputRef: useRef(null), attachedFiles: finalAttachedFiles, setAttachedFiles: finalSetAttachedFiles, models: models, defaultModel: defaultModel, showUsageStats: showUsageStats, maxFileSize: maxFileSize, features: features, currentModelSelection: currentModelSelection, onModelSelectionChange: handleModelSelectionChange, theme: theme, isOpen: true }) }))] })] }));
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* ErrorDisplay component - used to display error messages
|
|
2553
|
+
* @param msg - The chat message
|
|
2554
|
+
*/
|
|
2555
|
+
function ErrorDisplay({ msg }) {
|
|
2556
|
+
return (jsx("div", { className: "flex flex-col gap-3 max-w-full overflow-hidden", children: jsxs("div", { className: "flex-1 min-w-0 max-w-md", children: [jsx("div", { className: `font-medium break-words ${msg.errorType === 'rate_limit'
|
|
2557
|
+
? 'text-orange-800 text-base'
|
|
2558
|
+
: msg.errorType === 'timeout_error' ||
|
|
2559
|
+
msg.errorType === 'token_limit'
|
|
2560
|
+
? 'text-yellow-800 text-base'
|
|
2561
|
+
: 'text-red-800 text-sm'}`, children: getTextFromMessage$1(msg) }), (msg.errorType === 'timeout_error' ||
|
|
2562
|
+
msg.errorType === 'token_limit') && (jsxs("div", { className: "mt-3 space-y-2", children: [jsx("div", { className: "text-sm text-yellow-700 bg-yellow-50 rounded-lg px-3 py-2 border border-yellow-200 max-w-full", children: jsxs("div", { className: "flex items-start gap-2", children: [jsx("svg", { className: "w-4 h-4 flex-shrink-0 mt-0.5", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", clipRule: "evenodd" }) }), jsxs("div", { className: "min-w-0 flex-1", children: [jsx("div", { className: "font-medium mb-1", children: "What to try:" }), jsxs("ul", { className: "text-xs space-y-1 text-yellow-600", children: [jsx("li", { children: "\u2022 Try a shorter or simpler request" }), jsx("li", { children: "\u2022 Check your internet connection" }), jsx("li", { children: "\u2022 Wait a moment and try again" })] })] })] }) }), msg.retryAfter && (jsxs("div", { className: "text-xs text-yellow-600 bg-yellow-50 rounded px-2 py-1 border border-yellow-200 max-w-full break-words", children: ["\uD83D\uDCA1 Recommended wait time: ", msg.retryAfter, " seconds"] }))] })), msg.errorType === 'rate_limit' && (jsxs("div", { className: "mt-3 space-y-2", children: [msg.retryAfter && (jsx("div", { className: "text-sm text-orange-700 bg-orange-50 rounded-lg px-3 py-2 border border-orange-200 max-w-full", children: jsxs("div", { className: "flex items-center gap-2", children: [jsx("svg", { className: "w-4 h-4 flex-shrink-0", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.414L11 9.586V6z", clipRule: "evenodd" }) }), jsxs("span", { className: "font-medium min-w-0 break-words", children: ["Please wait approximately ", msg.retryAfter, " seconds before trying again"] })] }) })), getTextFromMessage$1(msg).includes('organization') && (jsx("div", { className: "text-sm text-blue-700 bg-blue-50 rounded-lg px-3 py-2 border border-blue-200 max-w-full", children: jsxs("div", { className: "flex items-start gap-2", children: [jsx("svg", { className: "w-4 h-4 flex-shrink-0 mt-0.5", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", clipRule: "evenodd" }) }), jsxs("div", { className: "min-w-0 flex-1", children: [jsx("div", { className: "font-medium mb-1", children: "What you can do:" }), jsxs("ul", { className: "text-xs space-y-1 text-blue-600", children: [jsx("li", { children: "\u2022 Wait for the rate limit to reset and try again" }), jsx("li", { children: "\u2022 Contact your organization administrator" }), jsx("li", { children: "\u2022 Consider upgrading your organization's rate limits" })] })] })] }) })), !getTextFromMessage$1(msg).includes('organization') && (jsx("div", { className: "text-xs text-orange-600 bg-orange-50 rounded px-2 py-1 border border-orange-200 max-w-full break-words", children: "\uD83D\uDCA1 Tip: Try shortening your request or wait a moment before trying again" }))] })), msg.errorType === 'auth_error' && (jsx("div", { className: "mt-2 text-sm text-red-700 bg-red-50 rounded-lg px-3 py-2 border border-red-200 max-w-full", children: jsxs("div", { className: "flex items-start gap-2", children: [jsx("svg", { className: "w-4 h-4 flex-shrink-0 mt-0.5", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", clipRule: "evenodd" }) }), jsxs("div", { className: "min-w-0 flex-1", children: [jsx("div", { className: "font-medium mb-1", children: "Authentication Issue:" }), jsx("div", { className: "text-xs break-words", children: "Please contact your system administrator to verify your API configuration." })] })] }) }))] }) }));
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* ChatBubbles component
|
|
2566
|
+
* @param chat - The chat messages
|
|
2567
|
+
* @param onSuggestionClick - Function to handle the suggestion click event
|
|
2568
|
+
* @param theme - Chat theme
|
|
2569
|
+
* @param showProfileBubbles - Whether to show profile bubbles (default: false)
|
|
2570
|
+
*/
|
|
2571
|
+
function ChatBubbles({ chat, onSuggestionClick, theme, showProfileBubbles = false, }) {
|
|
2572
|
+
const getProfileIcon = (sender, msg) => {
|
|
2573
|
+
switch (sender) {
|
|
2574
|
+
case 'user':
|
|
2575
|
+
return (jsx("div", { className: "flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center", children: jsx(Icon, { name: "user", size: "sm", className: "text-blue-600" }) }));
|
|
2576
|
+
case 'system':
|
|
2577
|
+
// For error messages, use appropriate error icon instead of generic system icon
|
|
2578
|
+
if (msg?.isError) {
|
|
2579
|
+
const errorIconName = msg.errorType === 'rate_limit' || msg.errorType === 'token_limit'
|
|
2580
|
+
? 'warning'
|
|
2581
|
+
: msg.errorType === 'timeout_error'
|
|
2582
|
+
? 'timeout'
|
|
2583
|
+
: msg.errorType === 'auth_error'
|
|
2584
|
+
? 'auth-error'
|
|
2585
|
+
: 'error';
|
|
2586
|
+
const bgColor = msg.errorType === 'rate_limit' || msg.errorType === 'token_limit'
|
|
2587
|
+
? 'bg-orange-100'
|
|
2588
|
+
: msg.errorType === 'timeout_error'
|
|
2589
|
+
? 'bg-yellow-100'
|
|
2590
|
+
: 'bg-red-100';
|
|
2591
|
+
return (jsx("div", { className: `flex-shrink-0 w-8 h-8 ${bgColor} rounded-full flex items-center justify-center`, children: jsx(Icon, { name: errorIconName, size: 10 }) }));
|
|
2592
|
+
}
|
|
2593
|
+
// Regular system messages use the system icon
|
|
2594
|
+
return (jsx("div", { className: "flex-shrink-0 w-8 h-8 bg-gray-100 rounded-full flex items-center justify-center", children: jsx(Icon, { name: "system", size: "sm", className: "text-gray-900" }) }));
|
|
2595
|
+
default: // AI
|
|
2596
|
+
return (jsx("div", { className: "flex-shrink-0 w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center", children: jsx(Icon, { name: "ai", size: "sm", className: "text-gray-900" }) }));
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
return (jsx(Fragment, { children: chat.map((msg, idx) => (jsx("div", { className: msg.sender === 'user'
|
|
2600
|
+
? 'flex justify-end'
|
|
2601
|
+
: msg.sender === 'system'
|
|
2602
|
+
? 'flex justify-start'
|
|
2603
|
+
: 'flex justify-start', children: jsxs("div", { className: `flex gap-3 ${msg.sender === 'user' ? 'flex-row-reverse' : 'flex-row'}`, children: [showProfileBubbles && getProfileIcon(msg.sender, msg), jsxs("div", { className: `${msg.sender === 'user'
|
|
2604
|
+
? 'flex flex-col items-end'
|
|
2605
|
+
: 'flex-1 min-w-0'}`, children: [jsxs("div", { className: "rounded-lg px-4 py-2 whitespace-pre-wrap break-words", style: {
|
|
2606
|
+
backgroundColor: msg.sender === 'user'
|
|
2607
|
+
? theme.colors.primary[500]
|
|
2608
|
+
: msg.sender === 'system'
|
|
2609
|
+
? msg.isError
|
|
2610
|
+
? theme.colors.state.error.background
|
|
2611
|
+
: theme.colors.primary[900]
|
|
2612
|
+
.replace('rgb(', 'rgba(')
|
|
2613
|
+
.replace(')', ', 0.2)') // 20% opacity
|
|
2614
|
+
: theme.colors.surface.secondary,
|
|
2615
|
+
color: msg.sender === 'user'
|
|
2616
|
+
? theme.colors.text.inverse
|
|
2617
|
+
: msg.sender === 'system'
|
|
2618
|
+
? msg.isError
|
|
2619
|
+
? theme.colors.state.error.text
|
|
2620
|
+
: theme.colors.primary[100]
|
|
2621
|
+
: theme.colors.text.primary,
|
|
2622
|
+
borderColor: msg.sender === 'system' && msg.isError
|
|
2623
|
+
? theme.colors.state.error.border
|
|
2624
|
+
: msg.sender === 'system'
|
|
2625
|
+
? theme.colors.primary[800]
|
|
2626
|
+
: 'transparent',
|
|
2627
|
+
borderWidth: msg.sender === 'system' ? '1px' : '0',
|
|
2628
|
+
}, children: [msg.sender === 'system' && msg.isError && (jsx(ErrorDisplay, { msg: msg })), msg.sender === 'system' && !msg.isError && (jsxs("div", { className: "flex items-center gap-2 text-blue-600", children: [jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", clipRule: "evenodd" }) }), getTextFromMessage$1(msg)] })), msg.sender !== 'system' && (jsxs(Fragment, { children: [msg.isLoading && msg.aiStatus !== 'none' && (jsxs("div", { className: `flex items-center gap-2 mb-3 ${msg.aiStatus === 'thinking'
|
|
2629
|
+
? 'text-gray-400 text-sm'
|
|
2630
|
+
: 'text-gray-400 text-sm'} ${msg.sender === 'user' ? '' : 'justify-end'}`, children: [jsxs("span", { className: "font-medium text-sm", children: [msg.aiStatus === 'thinking' &&
|
|
2631
|
+
'Analyzing your request...', msg.aiStatus === 'typing' &&
|
|
2632
|
+
'Generating response...', msg.aiStatus === 'suggesting' &&
|
|
2633
|
+
'Finding relevant actions...'] }), jsx("div", { className: "animate-spin rounded-full h-4 w-4 border-b-2 border-current" })] })), msg.content.map((contentBlock, blockIdx) => (jsxs("div", { children: [contentBlock.type === 'text' &&
|
|
2634
|
+
contentBlock.content && (jsx("div", { className: `leading-relaxed ${blockIdx > 0 ? 'mt-3' : ''}`, style: {
|
|
2635
|
+
color: msg.sender === 'user'
|
|
2636
|
+
? theme.colors.text.inverse // White text on colored background
|
|
2637
|
+
: theme.colors.text.primary, // Primary text color for AI
|
|
2638
|
+
}, children: contentBlock.content })), contentBlock.type === 'loading-suggestions' && (jsx("div", { className: `space-y-3 ${blockIdx > 0 ? 'mt-3' : ''}`, children: jsxs("div", { className: "flex items-center gap-2 text-sm border-t pt-3", style: {
|
|
2639
|
+
color: theme.colors.text.tertiary,
|
|
2640
|
+
borderColor: theme.colors.border.primary,
|
|
2641
|
+
}, children: [jsx("svg", { className: "w-4 h-4 animate-spin", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }), jsx("span", { className: "font-medium", children: contentBlock.content })] }) })), contentBlock.type === 'suggestions' &&
|
|
2642
|
+
contentBlock.content.length > 0 && (jsxs("div", { className: `space-y-3 ${blockIdx > 0 ? 'mt-3' : ''}`, children: [jsxs("div", { className: "flex items-center gap-2 text-sm border-t pt-3", style: {
|
|
2643
|
+
color: theme.colors.text.tertiary,
|
|
2644
|
+
borderColor: theme.colors.border.primary,
|
|
2645
|
+
}, children: [jsx("svg", { className: "w-4 h-4", fill: "currentColor", viewBox: "0 0 20 20", children: jsx("path", { fillRule: "evenodd", d: "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z", clipRule: "evenodd" }) }), jsxs("span", { className: "font-medium", children: ["I found ", contentBlock.content.length, " relevant action", contentBlock.content.length !== 1 ? 's' : '', ":"] })] }), jsx("div", { className: "space-y-2", children: contentBlock.content.map((suggestion, i) => (jsx(SuggestionCard, { suggestion: suggestion, onClick: onSuggestionClick, theme: theme }, i))) }), jsx("div", { className: "text-xs italic pt-2 border-t", style: {
|
|
2646
|
+
color: theme.colors.text.tertiary,
|
|
2647
|
+
borderColor: theme.colors.border.primary + '80', // 50% opacity
|
|
2648
|
+
}, children: "\uD83D\uDCA1 Click any suggestion to execute the action" })] }))] }, blockIdx))), msg.sender === 'user' && msg.files && (jsx(FileAttachmentDisplay, { files: msg.files }))] }))] }), jsx("div", { className: `text-xs mt-1 ${msg.sender === 'user' ? 'text-right' : 'text-left'}`, style: { color: theme.colors.text.tertiary }, children: formatTimestamp(msg.timestamp) })] })] }) }, idx))) }));
|
|
2649
|
+
}
|
|
2650
|
+
/**
|
|
2651
|
+
* FileAttachmentDisplay component - displays attached files in chat messages
|
|
2652
|
+
* @param files - Array of attached files
|
|
2653
|
+
*/
|
|
2654
|
+
function FileAttachmentDisplay({ files }) {
|
|
2655
|
+
const [previewFile, setPreviewFile] = React.useState(null);
|
|
2656
|
+
if (!files || files.length === 0)
|
|
2657
|
+
return null;
|
|
2658
|
+
const handleFileClick = (file) => {
|
|
2659
|
+
if (file.type.startsWith('image/')) {
|
|
2660
|
+
setPreviewFile(file);
|
|
2661
|
+
}
|
|
2662
|
+
else if (file.type === 'application/pdf') {
|
|
2663
|
+
// Open PDF in new tab
|
|
2664
|
+
const url = URL.createObjectURL(file);
|
|
2665
|
+
window.open(url, '_blank');
|
|
2666
|
+
}
|
|
2667
|
+
else {
|
|
2668
|
+
// Download other files
|
|
2669
|
+
downloadFile(file);
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
const downloadFile = (file) => {
|
|
2673
|
+
const url = URL.createObjectURL(file);
|
|
2674
|
+
const a = document.createElement('a');
|
|
2675
|
+
a.href = url;
|
|
2676
|
+
a.download = file.name;
|
|
2677
|
+
document.body.appendChild(a);
|
|
2678
|
+
a.click();
|
|
2679
|
+
document.body.removeChild(a);
|
|
2680
|
+
URL.revokeObjectURL(url);
|
|
2681
|
+
};
|
|
2682
|
+
return (jsxs(Fragment, { children: [jsx("div", { className: "mt-2 space-y-2", children: files.map((file, index) => (jsxs("div", { className: "flex items-center gap-2 p-2 bg-white/10 rounded-lg border border-white/20 cursor-pointer hover:bg-white/20 transition-colors group", onClick: () => handleFileClick(file), title: file.type.startsWith('image/')
|
|
2683
|
+
? 'Click to preview image'
|
|
2684
|
+
: file.type === 'application/pdf'
|
|
2685
|
+
? 'Click to open PDF'
|
|
2686
|
+
: 'Click to download file', children: [jsxs("div", { className: "flex-shrink-0 relative", children: [file.type.startsWith('image/') ? (jsx("div", { className: "w-10 h-10 rounded overflow-hidden bg-white/20", children: jsx("img", { src: URL.createObjectURL(file), alt: `Attached ${index + 1}`, className: "w-full h-full object-cover" }) })) : (jsx("div", { className: "w-10 h-10 rounded bg-white/20 flex items-center justify-center", children: FileHandler.getFileIcon(file) })), jsx("div", { className: "absolute inset-0 bg-white/20 rounded opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center", children: file.type.startsWith('image/') ? (jsx(Icon, { name: "eye", size: "sm", className: "text-white" })) : (jsxs("svg", { width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", className: "text-white", children: [jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), jsx("polyline", { points: "7,10 12,15 17,10" }), jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })] })) })] }), jsxs("div", { className: "min-w-0 max-w-xs", children: [jsx("div", { className: "font-medium text-sm text-white truncate", children: file.name }), jsxs("div", { className: "text-xs text-white/70 truncate", children: [FileHandler.getFileTypeDescription(file), " \u2022", ' ', FileHandler.formatFileSize(file.size)] })] }), jsx("div", { className: "flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity", children: jsx("div", { className: "text-xs text-white/70", children: file.type.startsWith('image/')
|
|
2687
|
+
? 'Preview'
|
|
2688
|
+
: file.type === 'application/pdf'
|
|
2689
|
+
? 'Open'
|
|
2690
|
+
: 'Download' }) })] }, index))) }), previewFile && (jsx("div", { className: "fixed inset-0 z-50 bg-black/80 flex items-center justify-center p-4", onClick: () => setPreviewFile(null), children: jsxs("div", { className: "relative max-w-full max-h-full", children: [jsx("img", { src: URL.createObjectURL(previewFile), alt: previewFile.name, className: "max-w-full max-h-full object-contain rounded-lg", onClick: (e) => e.stopPropagation() }), jsx("button", { className: "absolute top-4 right-4 w-8 h-8 bg-black/50 hover:bg-black/70 text-white rounded-full flex items-center justify-center transition-colors text-xl font-bold", onClick: () => setPreviewFile(null), children: "\u00D7" }), jsxs("div", { className: "absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/70 text-white p-3 rounded-lg max-w-sm", children: [jsx("div", { className: "font-medium text-center", children: previewFile.name }), jsxs("div", { className: "text-sm text-gray-300 text-center", children: [FileHandler.getFileTypeDescription(previewFile), " \u2022", ' ', FileHandler.formatFileSize(previewFile.size)] })] })] }) }))] }));
|
|
2691
|
+
}
|
|
2692
|
+
function ChatInput({ query, setQuery, onSend, isLoading, inputRef, attachedFiles = [], setAttachedFiles,
|
|
2693
|
+
// Model configuration
|
|
2694
|
+
models, defaultModel, showUsageStats, maxFileSize, features = {},
|
|
2695
|
+
// Model selection props
|
|
2696
|
+
currentModelSelection, onModelSelectionChange,
|
|
2697
|
+
// Theme props
|
|
2698
|
+
theme, isOpen, }) {
|
|
2699
|
+
const [focused, setFocused] = React.useState(false);
|
|
2700
|
+
const [isDragOver, setIsDragOver] = React.useState(false);
|
|
2701
|
+
const [pasteIndicator, setPasteIndicator] = React.useState(false);
|
|
2702
|
+
const [incompatibleFiles, setIncompatibleFiles] = React.useState([]);
|
|
2703
|
+
const [imageTipDismissed, setImageTipDismissed] = React.useState(false);
|
|
2704
|
+
const fileInputRef = useRef(null);
|
|
2705
|
+
const imageInputRef = useRef(null);
|
|
2706
|
+
// Compute imageSupported from capabilities
|
|
2707
|
+
const imageSupported = useMemo(() => {
|
|
2708
|
+
const supported = currentModelSelection?.capabilities?.supportsImages ?? false;
|
|
2709
|
+
return supported;
|
|
2710
|
+
}, [currentModelSelection]);
|
|
2711
|
+
// In ChatInput, use the flags
|
|
2712
|
+
// Only allow image uploads if features.enableImageUploads !== false
|
|
2713
|
+
const imageUploadsEnabled = features.enableImageUploads !== false;
|
|
2714
|
+
// Only allow file uploads if features.fileUploads !== false
|
|
2715
|
+
const fileUploadsEnabled = features.fileUploads !== false;
|
|
2716
|
+
// Enforce max file size
|
|
2717
|
+
const maxFileSizeBytes = maxFileSize
|
|
2718
|
+
? parseFileSize(maxFileSize)
|
|
2719
|
+
: 50 * 1024 * 1024;
|
|
2720
|
+
// Helper to parse file size strings like '50mb'
|
|
2721
|
+
function parseFileSize(size) {
|
|
2722
|
+
if (!size)
|
|
2723
|
+
return 50 * 1024 * 1024;
|
|
2724
|
+
const match = size.toLowerCase().match(/(\d+)(kb|mb|gb)?/);
|
|
2725
|
+
if (!match)
|
|
2726
|
+
return 50 * 1024 * 1024;
|
|
2727
|
+
const value = parseInt(match[1], 10);
|
|
2728
|
+
const unit = match[2];
|
|
2729
|
+
if (unit === 'gb')
|
|
2730
|
+
return value * 1024 * 1024 * 1024;
|
|
2731
|
+
if (unit === 'mb')
|
|
2732
|
+
return value * 1024 * 1024;
|
|
2733
|
+
if (unit === 'kb')
|
|
2734
|
+
return value * 1024;
|
|
2735
|
+
return value;
|
|
2736
|
+
}
|
|
2737
|
+
// Centralized file processing handler
|
|
2738
|
+
const handleFileProcessing = (files, source) => {
|
|
2739
|
+
if (!fileUploadsEnabled) {
|
|
2740
|
+
setIncompatibleFiles(['File uploads are disabled']);
|
|
2741
|
+
return;
|
|
2742
|
+
}
|
|
2743
|
+
// Enforce max file size
|
|
2744
|
+
const tooLarge = files.filter((file) => file.size > maxFileSizeBytes);
|
|
2745
|
+
const okFiles = files.filter((file) => file.size <= maxFileSizeBytes);
|
|
2746
|
+
if (tooLarge.length > 0) {
|
|
2747
|
+
setIncompatibleFiles([
|
|
2748
|
+
...tooLarge.map((f) => `${f.name} (exceeds max size)`),
|
|
2749
|
+
]);
|
|
2750
|
+
}
|
|
2751
|
+
// Only allow image files if image uploads are enabled
|
|
2752
|
+
const filteredFiles = okFiles.filter((file) => {
|
|
2753
|
+
if (file.type.startsWith('image/')) {
|
|
2754
|
+
return imageUploadsEnabled;
|
|
2755
|
+
}
|
|
2756
|
+
return true;
|
|
2757
|
+
});
|
|
2758
|
+
if (filteredFiles.length > 0 && setAttachedFiles) {
|
|
2759
|
+
setAttachedFiles([...attachedFiles, ...filteredFiles]);
|
|
2760
|
+
if (source === 'paste') {
|
|
2761
|
+
setPasteIndicator(true);
|
|
2762
|
+
setTimeout(() => setPasteIndicator(false), 1000);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
// Show warning for rejected files
|
|
2766
|
+
if (filteredFiles.length < okFiles.length) {
|
|
2767
|
+
setIncompatibleFiles([
|
|
2768
|
+
...incompatibleFiles,
|
|
2769
|
+
...okFiles
|
|
2770
|
+
.filter((f) => !filteredFiles.includes(f))
|
|
2771
|
+
.map((f) => `${f.name} (images not allowed)`),
|
|
2772
|
+
]);
|
|
2773
|
+
setTimeout(() => setIncompatibleFiles([]), 10000);
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
// Helper to filter files by type for specific buttons
|
|
2777
|
+
const filterFilesByType = (files, types) => {
|
|
2778
|
+
return files.filter((file) => {
|
|
2779
|
+
return types.some((type) => {
|
|
2780
|
+
// Use FileHandler to check if file is accepted
|
|
2781
|
+
return FileHandler.isFileAccepted(file);
|
|
2782
|
+
});
|
|
2783
|
+
});
|
|
2784
|
+
};
|
|
2785
|
+
// Handle all file uploads (paperclip button)
|
|
2786
|
+
const handleAllFileUpload = (event) => {
|
|
2787
|
+
const files = Array.from(event.target.files || []);
|
|
2788
|
+
handleFileProcessing(files, 'upload');
|
|
2789
|
+
// Reset input
|
|
2790
|
+
if (event.target) {
|
|
2791
|
+
event.target.value = '';
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
// Handle image-only uploads (image button)
|
|
2795
|
+
const handleImageUpload = (event) => {
|
|
2796
|
+
if (!imageSupported) {
|
|
2797
|
+
// Show warning if trying to upload images with unsupported model
|
|
2798
|
+
setIncompatibleFiles(['Images not supported by current model']);
|
|
2799
|
+
setTimeout(() => setIncompatibleFiles([]), 5000);
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
const files = Array.from(event.target.files || []);
|
|
2803
|
+
const imageFiles = filterFilesByType(files, ['images']);
|
|
2804
|
+
handleFileProcessing(imageFiles, 'upload');
|
|
2805
|
+
// Reset input
|
|
2806
|
+
if (event.target) {
|
|
2807
|
+
event.target.value = '';
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
// Handle audio recording (microphone button) - placeholder for now
|
|
2811
|
+
const handleAudioRecord = () => {
|
|
2812
|
+
// TODO: Implement audio recording functionality
|
|
2813
|
+
console.log('Audio recording not yet implemented');
|
|
2814
|
+
};
|
|
2815
|
+
// Remove attached file
|
|
2816
|
+
const removeFile = (index) => {
|
|
2817
|
+
if (setAttachedFiles) {
|
|
2818
|
+
setAttachedFiles(attachedFiles.filter((_, i) => i !== index));
|
|
2819
|
+
}
|
|
2820
|
+
};
|
|
2821
|
+
// Focus textarea when parent is clicked (but not on icons)
|
|
2822
|
+
const handleContainerClick = (e) => {
|
|
2823
|
+
if (e.target.tagName === 'TEXTAREA')
|
|
2824
|
+
return;
|
|
2825
|
+
inputRef.current?.focus();
|
|
2826
|
+
};
|
|
2827
|
+
// Handle paste events to capture files from clipboard
|
|
2828
|
+
const handlePaste = (event) => {
|
|
2829
|
+
const clipboardData = event.clipboardData;
|
|
2830
|
+
const items = Array.from(clipboardData.items);
|
|
2831
|
+
// Convert clipboard items to files
|
|
2832
|
+
const filePromises = items
|
|
2833
|
+
.filter((item) => item.kind === 'file')
|
|
2834
|
+
.map((item) => {
|
|
2835
|
+
return new Promise((resolve) => {
|
|
2836
|
+
const blob = item.getAsFile();
|
|
2837
|
+
if (blob) {
|
|
2838
|
+
const timestamp = new Date()
|
|
2839
|
+
.toISOString()
|
|
2840
|
+
.slice(0, 19)
|
|
2841
|
+
.replace(/[:.]/g, '-');
|
|
2842
|
+
const extension = blob.type.split('/')[1] || 'unknown';
|
|
2843
|
+
const file = new File([blob], `pasted-file-${timestamp}.${extension}`, {
|
|
2844
|
+
type: blob.type,
|
|
2845
|
+
});
|
|
2846
|
+
resolve(file);
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
});
|
|
2850
|
+
// Only prevent default if there are files to paste
|
|
2851
|
+
if (filePromises.length > 0) {
|
|
2852
|
+
event.preventDefault(); // Prevent default paste behavior for files
|
|
2853
|
+
Promise.all(filePromises).then((files) => {
|
|
2854
|
+
handleFileProcessing(files, 'paste');
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
// If no files, let the default paste behavior handle text content
|
|
2858
|
+
};
|
|
2859
|
+
// Handle drag and drop for files
|
|
2860
|
+
const handleDragOver = (event) => {
|
|
2861
|
+
event.preventDefault();
|
|
2862
|
+
setIsDragOver(true);
|
|
2863
|
+
};
|
|
2864
|
+
const handleDragLeave = (event) => {
|
|
2865
|
+
event.preventDefault();
|
|
2866
|
+
setIsDragOver(false);
|
|
2867
|
+
};
|
|
2868
|
+
const handleDrop = (event) => {
|
|
2869
|
+
event.preventDefault();
|
|
2870
|
+
setIsDragOver(false);
|
|
2871
|
+
const files = Array.from(event.dataTransfer.files);
|
|
2872
|
+
handleFileProcessing(files, 'drop');
|
|
2873
|
+
};
|
|
2874
|
+
const modelSelectionHandler = React.useCallback((modelInfo) => {
|
|
2875
|
+
if (onModelSelectionChange) {
|
|
2876
|
+
onModelSelectionChange(modelInfo);
|
|
2877
|
+
}
|
|
2878
|
+
}, [onModelSelectionChange]);
|
|
2879
|
+
return (jsxs("div", { className: `px-3 py-2 border-t flex flex-col gap-2 transition-all duration-500 delay-500 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`, style: { borderColor: theme.colors.border.primary }, children: [attachedFiles.length > 0 && (jsx("div", { className: "flex flex-wrap gap-2 mb-2", children: attachedFiles.map((file, index) => (jsxs("div", { className: "relative group", children: [jsx("div", { className: "w-16 h-16 rounded-lg border border-gray-200 bg-gray-50 flex items-center justify-center overflow-hidden", children: file.type.startsWith('image/') ? (jsx("img", { src: URL.createObjectURL(file), alt: `Attached ${index + 1}`, className: "w-full h-full object-cover" })) : (jsx("div", { className: "flex items-center justify-center", children: FileHandler.getFileIcon(file) })) }), jsx("button", { onClick: () => removeFile(index), className: "absolute -top-2 -right-2 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors", title: "Remove file", children: "\u00D7" }), jsx("div", { className: "absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-full opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10 mt-2", children: jsxs("div", { className: "bg-gray-900 text-white text-xs rounded-lg px-2 py-1 whitespace-nowrap shadow-lg", children: [jsx("div", { className: "font-medium truncate max-w-32", title: file.name, children: file.name }), jsxs("div", { className: "text-gray-300", children: [FileHandler.getFileTypeDescription(file), " \u2022", ' ', FileHandler.formatFileSize(file.size)] }), jsx("div", { className: "absolute -top-1 left-1/2 transform -translate-x-1/2 w-2 h-2 bg-gray-900 rotate-45" })] }) })] }, index))) })), incompatibleFiles.length > 0 && (jsxs("div", { className: "relative flex flex-col gap-1 text-sm text-orange-700 bg-orange-50 border border-orange-200 rounded px-3 py-2 mb-2", children: [jsx("button", { onClick: () => setIncompatibleFiles([]), className: "absolute top-1 right-1 w-5 h-5 flex items-center justify-center text-orange-500 hover:text-orange-700 hover:bg-orange-200 rounded-full transition-colors", title: "Dismiss", type: "button", children: "\u00D7" }), jsxs("div", { className: "flex items-center gap-2 pr-6", children: [jsx("span", { children: "\u26A0\uFE0F" }), jsx("span", { className: "font-medium", children: incompatibleFiles.length === 1
|
|
2880
|
+
? 'File not supported'
|
|
2881
|
+
: 'Some files not supported' })] }), jsxs("div", { className: "text-xs", children: [jsx("div", { className: "mb-1", children: "These files were skipped:" }), jsxs("ul", { className: "list-disc list-inside ml-2 space-y-0.5", children: [incompatibleFiles.slice(0, 3).map((fileName, index) => (jsx("li", { className: "truncate", children: fileName }, index))), incompatibleFiles.length > 3 && (jsxs("li", { children: ["... and ", incompatibleFiles.length - 3, " more"] }))] }), jsxs("div", { className: "mt-1 text-orange-600", children: ["\uD83D\uDCA1 Only ", FileHandler.getAcceptedTypesDescription(), " are supported", incompatibleFiles.some((name) => name.includes('images not supported by current model')) && (jsxs("div", { className: "mt-1", children: ["\uD83D\uDD04 ", jsx("strong", { children: "Switch to GPT-4O, Claude 3, or Gemini" }), " to enable image uploads"] }))] })] })] })), jsxs("div", { className: `relative flex flex-col gap-1 rounded border px-2 pt-1 pb-1 cursor-text transition-all duration-200 border-2 focus-within:shadow-sm ${isDragOver ? 'border-dashed bg-opacity-20' : 'border-transparent'}`, style: {
|
|
2882
|
+
backgroundColor: theme.colors.surface.primary,
|
|
2883
|
+
borderColor: isDragOver
|
|
2884
|
+
? theme.colors.primary[400]
|
|
2885
|
+
: theme.colors.border.primary,
|
|
2886
|
+
color: theme.colors.text.primary,
|
|
2887
|
+
}, onFocus: () => {
|
|
2888
|
+
// Focus within handling
|
|
2889
|
+
const target = event?.target;
|
|
2890
|
+
if (target) {
|
|
2891
|
+
target.style.borderColor = theme.colors.primary[500];
|
|
2892
|
+
}
|
|
2893
|
+
}, onClick: handleContainerClick, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [isDragOver && (jsx("div", { className: "absolute top-2 left-2 right-2 bottom-2 flex items-center justify-center py-1 bg-blue-100 rounded text-blue-600 text-sm font-medium z-10 pointer-events-none", children: "\uD83D\uDCC2 Drop files here" })), jsx("textarea", { ref: inputRef, value: query, onChange: (e) => setQuery(e.target.value), onFocus: () => setFocused(true), onBlur: () => setFocused(false), onKeyDown: (e) => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), onSend()), onPaste: handlePaste, placeholder: "Describe what you need... (Try: 'I want to know how much time has been updated')", className: "w-full outline-none resize-none min-h-[3.5rem] max-h-32 border-none px-0 bg-transparent", style: {
|
|
2894
|
+
color: theme.colors.text.primary,
|
|
2895
|
+
}, rows: 3 }), jsxs("div", { className: "flex gap-2 items-center justify-between", children: [jsxs("div", { className: "flex gap-1", children: [features.fileUploads !== false && (jsx("button", { className: "p-1 cursor-pointer disabled:opacity-50 disabled:cursor-default transition-colors", style: { color: theme.colors.text.tertiary }, onMouseEnter: (e) => {
|
|
2896
|
+
e.currentTarget.style.color = theme.colors.primary[500];
|
|
2897
|
+
}, onMouseLeave: (e) => {
|
|
2898
|
+
e.currentTarget.style.color = theme.colors.text.tertiary;
|
|
2899
|
+
}, title: `Attach files - Supports: ${FileHandler.getAcceptedTypesDescription()}`, tabIndex: -1, type: "button", onClick: () => fileInputRef.current?.click(), children: jsx(Icon, { name: "paperclip", size: "sm" }) })), features.enableImageUploads !== false &&
|
|
2900
|
+
features.imageAnalysis !== false && (jsx("button", { className: `p-1 cursor-pointer disabled:opacity-50 disabled:cursor-default transition-colors ${!imageSupported ? 'opacity-50 cursor-not-allowed' : ''}`, style: { color: theme.colors.text.tertiary }, onMouseEnter: (e) => {
|
|
2901
|
+
if (imageSupported) {
|
|
2902
|
+
e.currentTarget.style.color = theme.colors.primary[500];
|
|
2903
|
+
}
|
|
2904
|
+
}, onMouseLeave: (e) => {
|
|
2905
|
+
e.currentTarget.style.color = theme.colors.text.tertiary;
|
|
2906
|
+
}, title: imageSupported
|
|
2907
|
+
? 'Attach images - AI can analyze image content'
|
|
2908
|
+
: "Current model doesn't support images. Switch to GPT-4O, Claude 3, or Gemini to use image attachments.", tabIndex: -1, type: "button", onClick: () => imageSupported && imageInputRef.current?.click(), disabled: !imageSupported, children: jsx(Icon, { name: "image", size: "sm" }) })), jsx("button", { className: "p-1 cursor-pointer disabled:opacity-50 disabled:cursor-default transition-colors", style: { color: theme.colors.text.tertiary }, title: "Record audio (coming soon)", tabIndex: -1, type: "button", onClick: handleAudioRecord, disabled: true, children: jsx(Icon, { name: "microphone", size: "sm" }) })] }), jsx("button", { onClick: () => onSend(), disabled: (!query.trim() && attachedFiles.length === 0) || isLoading, className: "rounded-full transition disabled:opacity-50 p-2", style: {
|
|
2909
|
+
color: theme.colors.text.primary,
|
|
2910
|
+
}, onMouseEnter: (e) => {
|
|
2911
|
+
if (!e.currentTarget.disabled) {
|
|
2912
|
+
e.currentTarget.style.backgroundColor =
|
|
2913
|
+
theme.colors.surface.secondary;
|
|
2914
|
+
}
|
|
2915
|
+
}, onMouseLeave: (e) => {
|
|
2916
|
+
if (!e.currentTarget.disabled) {
|
|
2917
|
+
e.currentTarget.style.backgroundColor = 'transparent';
|
|
2918
|
+
}
|
|
2919
|
+
}, title: "Send", tabIndex: -1, type: "button", children: jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsx("g", { clipPath: "url(#clip0_892_5571)", children: jsx("path", { d: "M13.3334 10.0001L10.0001 6.66675M10.0001 6.66675L6.66675 10.0001M10.0001 6.66675V13.3334M18.3334 10.0001C18.3334 14.6025 14.6025 18.3334 10.0001 18.3334C5.39771 18.3334 1.66675 14.6025 1.66675 10.0001C1.66675 5.39771 5.39771 1.66675 10.0001 1.66675C14.6025 1.66675 18.3334 5.39771 18.3334 10.0001Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }), jsx("defs", { children: jsx("clipPath", { id: "clip0_892_5571", children: jsx("rect", { width: "20", height: "20", fill: "white" }) }) })] }) })] })] }), jsx("input", { ref: fileInputRef, type: "file", accept: FileHandler.getInputAcceptAttribute(), multiple: true, onChange: handleAllFileUpload, className: "hidden", "aria-label": "Upload any file" }), jsx("input", { ref: imageInputRef, type: "file", accept: "image/*", multiple: true, onChange: handleImageUpload, className: "hidden", "aria-label": "Upload images" }), features.modelSwitching !== false &&
|
|
2920
|
+
features.imageAnalysis !== false &&
|
|
2921
|
+
features.enableImageUploads !== false &&
|
|
2922
|
+
!imageSupported &&
|
|
2923
|
+
!imageTipDismissed && (jsxs("div", { className: "relative px-2 py-1 bg-amber-50 border border-amber-200 rounded text-amber-700 text-xs flex items-center gap-2", children: [jsx(Icon, { name: "image", size: "xs", className: "opacity-50" }), jsxs("span", { className: "flex-1 pr-4", children: ["\uD83D\uDCA1 ", jsx("strong", { children: "Tip:" }), " Switch to GPT-4O, Claude 3, or Gemini in the model switcher below to enable image attachments"] }), jsx("button", { onClick: () => setImageTipDismissed(true), className: "absolute top-1 right-1 w-4 h-4 flex items-center justify-center text-amber-500 hover:text-amber-700 hover:bg-amber-200 rounded-full transition-colors text-xs font-bold", title: "Dismiss tip", type: "button", children: "\u00D7" })] })), jsxs("div", { className: "flex items-center justify-between text-xs px-0 pt-0", style: { color: theme.colors.text.tertiary }, children: [jsx("div", { className: "flex gap-4", children: features.modelSwitching !== false && (jsx(ModelSwitcher, { models: models, defaultModel: defaultModel, showUsageStats: features.usageTracking !== false && showUsageStats !== false, theme: theme, onModelSelectionChange: modelSelectionHandler })) }), jsx("div", { className: "flex items-center gap-2", children: jsxs("span", { children: [jsx("kbd", { className: "px-1 rounded", style: {
|
|
2924
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
2925
|
+
color: theme.colors.text.secondary,
|
|
2926
|
+
}, children: "\u23CE" }), ' ', "to send,", ' ', jsx("kbd", { className: "px-1 rounded", style: {
|
|
2927
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
2928
|
+
color: theme.colors.text.secondary,
|
|
2929
|
+
}, children: "\u21E7\u23CE" }), ' ', "for new line"] }) })] })] }));
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
// Helper function to detect system theme preference
|
|
2933
|
+
const getSystemThemePreference = () => {
|
|
2934
|
+
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
2935
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
2936
|
+
? 'dark'
|
|
2937
|
+
: 'light';
|
|
2938
|
+
}
|
|
2939
|
+
return 'dark'; // Default to dark if can't detect
|
|
2940
|
+
};
|
|
2941
|
+
// Helper function to merge custom theme with defaults
|
|
2942
|
+
const mergeTheme = (customTheme, baseTheme) => {
|
|
2943
|
+
return {
|
|
2944
|
+
...baseTheme,
|
|
2945
|
+
...customTheme,
|
|
2946
|
+
colors: {
|
|
2947
|
+
...baseTheme.colors,
|
|
2948
|
+
...customTheme.colors,
|
|
2949
|
+
primary: {
|
|
2950
|
+
...baseTheme.colors.primary,
|
|
2951
|
+
...customTheme.colors?.primary,
|
|
2952
|
+
},
|
|
2953
|
+
background: {
|
|
2954
|
+
...baseTheme.colors.background,
|
|
2955
|
+
...customTheme.colors?.background,
|
|
2956
|
+
},
|
|
2957
|
+
text: {
|
|
2958
|
+
...baseTheme.colors.text,
|
|
2959
|
+
...customTheme.colors?.text,
|
|
2960
|
+
},
|
|
2961
|
+
border: {
|
|
2962
|
+
...baseTheme.colors.border,
|
|
2963
|
+
...customTheme.colors?.border,
|
|
2964
|
+
},
|
|
2965
|
+
surface: {
|
|
2966
|
+
...baseTheme.colors.surface,
|
|
2967
|
+
...customTheme.colors?.surface,
|
|
2968
|
+
},
|
|
2969
|
+
state: {
|
|
2970
|
+
...baseTheme.colors.state,
|
|
2971
|
+
...customTheme.colors?.state,
|
|
2972
|
+
},
|
|
2973
|
+
},
|
|
2974
|
+
};
|
|
2975
|
+
};
|
|
2976
|
+
/**
|
|
2977
|
+
* Helper to extract text content from message content blocks
|
|
2978
|
+
*/
|
|
2979
|
+
const getTextFromMessage = (msg) => {
|
|
2980
|
+
return msg.content
|
|
2981
|
+
.filter((block) => block.type === 'text')
|
|
2982
|
+
.map((block) => block.content)
|
|
2983
|
+
.join('\n');
|
|
2984
|
+
};
|
|
2985
|
+
/**
|
|
2986
|
+
* ChatPanel component
|
|
2987
|
+
* @param isOpen - Whether the chat panel is open
|
|
2988
|
+
* @param setIsOpen - Function to set the chat panel open state
|
|
2989
|
+
* @param onClose - Function to close the chat panel
|
|
2990
|
+
* @param onOpen - Function to open the chat panel
|
|
2991
|
+
* @param userId - User ID - used to identify the user in the streaming server
|
|
2992
|
+
* @param formData - Form data - used to reference the existing form data in the streaming server
|
|
2993
|
+
* @param setFormState - Function to set the form state
|
|
2994
|
+
* @param onNavigate - Function to navigate to a new path
|
|
2995
|
+
* @param theme - Theme
|
|
2996
|
+
* @param showHistory - Whether to show chat history sidebar (default: false)
|
|
2997
|
+
* @param showProfileBubbles - Whether to show profile bubbles in chat (default: false)
|
|
2998
|
+
* @param serverConfig - Flexible server configuration object
|
|
2999
|
+
* @param showFloatingButton - Whether to show the floating button
|
|
3000
|
+
* @param floatingButtonIcon - Icon for the floating button
|
|
3001
|
+
* @param floatingButtonPosition - Position of the floating button
|
|
3002
|
+
* @param floatingButtonSize - Size of the floating button
|
|
3003
|
+
* @param floatingButtonClassName - Class name for the floating button
|
|
3004
|
+
* @param chats - Optional external chat history
|
|
3005
|
+
* @param setChats - Optional function to set external chat history
|
|
3006
|
+
* @param currentChatId - Optional external current chat ID
|
|
3007
|
+
* @param setCurrentChatId - Optional function to set external current chat ID
|
|
3008
|
+
* @param chatLevel - Optional chat level - 'full' or 'basic' or 'none'
|
|
3009
|
+
* @param initialQuery - Optional initial query to send when modal opens
|
|
3010
|
+
* @param setInitialQuery - Optional function to set/clear the initial query
|
|
3011
|
+
*/
|
|
3012
|
+
function ChatPanel({ isOpen, setIsOpen, onClose, onOpen, userId, formData, setFormState, onNavigate, theme = defaultTheme, // Legacy support
|
|
3013
|
+
chatTheme, // New theme system
|
|
3014
|
+
themeMode = 'system', // Default to system preference
|
|
3015
|
+
showHistory = false, // Default to hidden
|
|
3016
|
+
showProfileBubbles = false, // Default to hidden
|
|
3017
|
+
modalPosition = 'left', // Default to left position
|
|
3018
|
+
serverConfig, models, defaultModel, showUsageStats, maxFileSize, features = {}, showFloatingButton = false, floatingButtonIcon, floatingButtonPosition = 'bottom-right', floatingButtonSize = 'md', floatingButtonClassName = '', chats, setChats, currentChatId, setCurrentChatId, chatLevel, initialQuery, setInitialQuery, }) {
|
|
3019
|
+
const handleSuggestionClick = (s) => {
|
|
3020
|
+
if (setFormState && s.formState)
|
|
3021
|
+
setFormState(s.formState, s.actionType);
|
|
3022
|
+
if (s.path)
|
|
3023
|
+
onNavigate(s.path);
|
|
3024
|
+
onClose();
|
|
3025
|
+
};
|
|
3026
|
+
const containerRef = useRef(null);
|
|
3027
|
+
// SIMPLIFIED ANIMATION STATE
|
|
3028
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
3029
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
3030
|
+
// Handle opening animation
|
|
3031
|
+
useEffect(() => {
|
|
3032
|
+
if (isOpen && !isVisible) {
|
|
3033
|
+
setIsVisible(true);
|
|
3034
|
+
setIsAnimating(true);
|
|
3035
|
+
requestAnimationFrame(() => {
|
|
3036
|
+
setTimeout(() => {
|
|
3037
|
+
setIsAnimating(false);
|
|
3038
|
+
}, 300);
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
}, [isOpen, isVisible]);
|
|
3042
|
+
// Handle closing animation
|
|
3043
|
+
const handleClose = useCallback(() => {
|
|
3044
|
+
if (!isAnimating) {
|
|
3045
|
+
setIsAnimating(true);
|
|
3046
|
+
// Flip external open state immediately so closing classes apply
|
|
3047
|
+
onClose();
|
|
3048
|
+
setIsOpen?.(false);
|
|
3049
|
+
// Keep mounted for duration to allow slide-out
|
|
3050
|
+
setTimeout(() => {
|
|
3051
|
+
setIsVisible(false);
|
|
3052
|
+
setIsAnimating(false);
|
|
3053
|
+
}, 300);
|
|
3054
|
+
}
|
|
3055
|
+
}, [isAnimating, onClose, setIsOpen]);
|
|
3056
|
+
// Handle opening
|
|
3057
|
+
const handleOpen = useCallback(() => {
|
|
3058
|
+
if (!isAnimating) {
|
|
3059
|
+
onOpen?.();
|
|
3060
|
+
setIsOpen?.(true);
|
|
3061
|
+
}
|
|
3062
|
+
}, [isAnimating, onOpen, setIsOpen]);
|
|
3063
|
+
// Determine if panel should be rendered
|
|
3064
|
+
const shouldRenderPanel = isOpen || isVisible;
|
|
3065
|
+
// Determine animation classes
|
|
3066
|
+
const getAnimationClasses = () => {
|
|
3067
|
+
if (!isOpen && isVisible) {
|
|
3068
|
+
return modalPosition === 'left' ? '-translate-x-full opacity-0' : 'translate-x-full opacity-0';
|
|
3069
|
+
}
|
|
3070
|
+
else if (isOpen && isVisible) {
|
|
3071
|
+
return 'translate-x-0 opacity-100';
|
|
3072
|
+
}
|
|
3073
|
+
else {
|
|
3074
|
+
return modalPosition === 'left' ? '-translate-x-full opacity-0' : 'translate-x-full opacity-0';
|
|
3075
|
+
}
|
|
3076
|
+
};
|
|
3077
|
+
// Resolve theme mode and create final theme
|
|
3078
|
+
const resolvedThemeMode = useMemo(() => {
|
|
3079
|
+
if (themeMode === 'system') {
|
|
3080
|
+
return getSystemThemePreference();
|
|
3081
|
+
}
|
|
3082
|
+
return themeMode;
|
|
3083
|
+
}, [themeMode]);
|
|
3084
|
+
const finalTheme = useMemo(() => {
|
|
3085
|
+
const baseTheme = resolvedThemeMode === 'dark' ? defaultDarkTheme : defaultLightTheme;
|
|
3086
|
+
return chatTheme ? mergeTheme(chatTheme, baseTheme) : baseTheme;
|
|
3087
|
+
}, [resolvedThemeMode, chatTheme]);
|
|
3088
|
+
// Generate custom scrollbar styles based on theme
|
|
3089
|
+
const scrollbarStyles = `
|
|
3090
|
+
.custom-scrollbar {
|
|
3091
|
+
scrollbar-width: thin;
|
|
3092
|
+
scrollbar-color: ${finalTheme.colors.primary[500]} ${finalTheme.colors.surface.secondary};
|
|
3093
|
+
}
|
|
3094
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
3095
|
+
width: 8px;
|
|
3096
|
+
height: 8px;
|
|
3097
|
+
}
|
|
3098
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
3099
|
+
background: ${finalTheme.colors.surface.secondary};
|
|
3100
|
+
border-radius: 4px;
|
|
3101
|
+
}
|
|
3102
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
3103
|
+
background: ${finalTheme.colors.primary[500]};
|
|
3104
|
+
border-radius: 4px;
|
|
3105
|
+
border: 1px solid ${finalTheme.colors.surface.secondary};
|
|
3106
|
+
}
|
|
3107
|
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
3108
|
+
background: ${finalTheme.colors.primary[600]};
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
/* Animation classes */
|
|
3112
|
+
.chat-panel-enter {
|
|
3113
|
+
transform: translateX(${modalPosition === 'left' ? '-100%' : '100%'});
|
|
3114
|
+
opacity: 0;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
.chat-panel-enter-active {
|
|
3118
|
+
transform: translateX(0);
|
|
3119
|
+
opacity: 1;
|
|
3120
|
+
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
.chat-panel-exit {
|
|
3124
|
+
transform: translateX(0);
|
|
3125
|
+
opacity: 1;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
.chat-panel-exit-active {
|
|
3129
|
+
transform: translateX(${modalPosition === 'left' ? '-100%' : '100%'});
|
|
3130
|
+
opacity: 0;
|
|
3131
|
+
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
/* Background overlay animations */
|
|
3135
|
+
.overlay-enter {
|
|
3136
|
+
opacity: 0;
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
.overlay-enter-active {
|
|
3140
|
+
opacity: 1;
|
|
3141
|
+
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
.overlay-exit {
|
|
3145
|
+
opacity: 1;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
.overlay-exit-active {
|
|
3149
|
+
opacity: 0;
|
|
3150
|
+
transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
/* Close button animations */
|
|
3154
|
+
.close-button-enter {
|
|
3155
|
+
transform: scale(0.8) rotate(-90deg);
|
|
3156
|
+
opacity: 0;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
.close-button-enter-active {
|
|
3160
|
+
transform: scale(1) rotate(0deg);
|
|
3161
|
+
opacity: 1;
|
|
3162
|
+
transition: all 400ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
.close-button-exit {
|
|
3166
|
+
transform: scale(1) rotate(0deg);
|
|
3167
|
+
opacity: 1;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.close-button-exit-active {
|
|
3171
|
+
transform: scale(0.8) rotate(90deg);
|
|
3172
|
+
opacity: 0;
|
|
3173
|
+
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
3174
|
+
}
|
|
3175
|
+
`;
|
|
3176
|
+
return (jsxs(Fragment, { children: [jsx("style", { children: scrollbarStyles }), jsxs(ErrorBoundary, { children: [!isOpen && showFloatingButton && (jsx(FloatingChatButton, { onClick: handleOpen, theme: theme, icon: floatingButtonIcon, position: floatingButtonPosition, size: floatingButtonSize, className: floatingButtonClassName })), shouldRenderPanel && (jsxs("div", { className: "fixed inset-0 z-50", children: [jsx("div", { className: `absolute inset-0 bg-black/20 transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`, onClick: handleClose }), jsx("div", { className: `fixed z-[60] top-6 transition-all duration-300 ${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-75'}`, style: { ...(modalPosition === 'left'
|
|
3177
|
+
? { left: showHistory ? 'min(90vw, 700px)' : 'min(80vw, 550px)', marginLeft: '16px' }
|
|
3178
|
+
: { right: showHistory ? 'min(90vw, 700px)' : 'min(80vw, 550px)', marginRight: '16px' }) }, children: jsx("button", { onClick: handleClose, className: "w-10 h-10 rounded-full shadow-lg transition-all duration-200 flex items-center justify-center text-xl font-bold hover:scale-110", style: {
|
|
3179
|
+
backgroundColor: finalTheme.colors.surface.elevated,
|
|
3180
|
+
color: finalTheme.colors.text.secondary,
|
|
3181
|
+
border: `2px solid ${finalTheme.colors.border.primary}`,
|
|
3182
|
+
boxShadow: finalTheme.shadows.lg,
|
|
3183
|
+
}, onMouseEnter: (e) => {
|
|
3184
|
+
e.currentTarget.style.backgroundColor =
|
|
3185
|
+
finalTheme.colors.surface.secondary;
|
|
3186
|
+
e.currentTarget.style.color = finalTheme.colors.text.primary;
|
|
3187
|
+
e.currentTarget.style.borderColor =
|
|
3188
|
+
finalTheme.colors.border.secondary;
|
|
3189
|
+
e.currentTarget.style.transform = 'scale(1.05)';
|
|
3190
|
+
}, onMouseLeave: (e) => {
|
|
3191
|
+
e.currentTarget.style.backgroundColor =
|
|
3192
|
+
finalTheme.colors.surface.elevated;
|
|
3193
|
+
e.currentTarget.style.color =
|
|
3194
|
+
finalTheme.colors.text.secondary;
|
|
3195
|
+
e.currentTarget.style.borderColor =
|
|
3196
|
+
finalTheme.colors.border.primary;
|
|
3197
|
+
e.currentTarget.style.transform = 'scale(1)';
|
|
3198
|
+
}, title: "Close", children: "\u00D7" }) }), jsxs("div", { className: `fixed top-0 h-full w-full z-50 flex flex-col relative
|
|
3199
|
+
${modalPosition === 'left' ? 'left-0 rounded-r-lg' : 'right-0 rounded-l-lg'}
|
|
3200
|
+
${showHistory ? 'sm:w-[90vw] md:w-[700px]' : 'sm:w-[80vw] md:w-[550px]'}
|
|
3201
|
+
transition-all duration-300 ease-out
|
|
3202
|
+
${getAnimationClasses()}
|
|
3203
|
+
`, style: {
|
|
3204
|
+
backgroundColor: finalTheme.colors.background.primary,
|
|
3205
|
+
color: finalTheme.colors.text.primary,
|
|
3206
|
+
boxShadow: modalPosition === 'left'
|
|
3207
|
+
? '8px 0 32px rgba(0, 0, 0, 0.3), 4px 0 16px rgba(0, 0, 0, 0.2)'
|
|
3208
|
+
: '-8px 0 32px rgba(0, 0, 0, 0.3), -4px 0 16px rgba(0, 0, 0, 0.2)',
|
|
3209
|
+
}, ref: containerRef, onClick: (e) => e.stopPropagation(), children: [jsx("div", { className: `flex items-center justify-between px-6 py-4 border-b min-h-[72px] transition-all duration-500 delay-100 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'}`, style: {
|
|
3210
|
+
borderColor: finalTheme.colors.border.primary,
|
|
3211
|
+
backgroundColor: finalTheme.colors.background.primary,
|
|
3212
|
+
}, children: jsxs("div", { className: "flex items-center gap-3", children: [jsx("span", { className: `inline-flex items-center justify-center rounded-full w-8 h-8 transition-all duration-500 delay-200 ${isOpen ? 'opacity-100 scale-100 rotate-0' : 'opacity-0 scale-75 rotate-12'}`, style: { backgroundColor: finalTheme.colors.primary[600] }, children: jsx(Icon, { name: "ai", size: "sm", style: { color: finalTheme.colors.text.primary } }) }), jsxs("div", { className: `transition-all duration-500 delay-300 ${isOpen ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-2'}`, children: [jsx("div", { className: "font-bold text-lg leading-tight", style: { color: finalTheme.colors.text.primary }, children: "AI Logistics & Customs Expert" }), jsx("div", { className: "text-xs", style: { color: finalTheme.colors.text.tertiary }, children: "Ready to assist you with your queries" })] })] }) }), jsxs("div", { className: `flex flex-1 min-h-0 h-full transition-all duration-500 delay-200 ${isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'}`, children: [showHistory && (jsx(ChatHistorySidebar, { chats: chats || {}, currentChatId: currentChatId || 'default', switchChat: () => { }, newChat: () => { }, theme: finalTheme, isOpen: isOpen })), jsx("div", { className: "flex-1 flex flex-col min-h-0 h-full", children: jsx("div", { className: `flex-1 px-6 py-4 min-h-0 transition-all duration-500 delay-300 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'}`, style: {
|
|
3213
|
+
backgroundColor: finalTheme.colors.background.primary,
|
|
3214
|
+
}, children: jsx(Conversation, { onSuggestionClick: handleSuggestionClick, theme: finalTheme, showProfileBubbles: showProfileBubbles, autoScroll: true, showInput: true,
|
|
3215
|
+
// AI functionality props
|
|
3216
|
+
userId: userId, serverConfig: serverConfig, formData: formData, setFormState: setFormState, onNavigate: onNavigate, chatLevel: chatLevel,
|
|
3217
|
+
// External chat management
|
|
3218
|
+
chats: chats, setChats: setChats, currentChatId: currentChatId, setCurrentChatId: setCurrentChatId,
|
|
3219
|
+
// Model configuration
|
|
3220
|
+
models: models, defaultModel: defaultModel, showUsageStats: showUsageStats, maxFileSize: maxFileSize, features: features,
|
|
3221
|
+
// Initial query handling
|
|
3222
|
+
initialQuery: initialQuery, setInitialQuery: setInitialQuery }) }) })] })] })] }))] })] }));
|
|
3223
|
+
}
|
|
3224
|
+
/**
|
|
3225
|
+
* ChatHistorySidebar component with animation support
|
|
3226
|
+
*/
|
|
3227
|
+
function ChatHistorySidebar({ chats, currentChatId, switchChat, newChat, theme, isOpen, }) {
|
|
3228
|
+
return (jsx("div", { className: `flex-shrink-0 flex-grow-0 border-r flex flex-col h-full p-2 w-[150px] transition-all duration-500 delay-400 ${isOpen ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-4'}`, style: {
|
|
3229
|
+
backgroundColor: theme.colors.background.secondary,
|
|
3230
|
+
borderColor: theme.colors.border.primary,
|
|
3231
|
+
}, children: jsxs("div", { className: "flex-1 overflow-y-auto flex flex-col items-center gap-2 p-2 custom-scrollbar", children: [jsx("button", { className: "rounded-lg w-full py-2 font-bold text-xl mb-2 shadow transition-colors", style: {
|
|
3232
|
+
backgroundColor: theme.colors.primary[600],
|
|
3233
|
+
color: theme.colors.text.inverse,
|
|
3234
|
+
borderColor: theme.colors.primary[700],
|
|
3235
|
+
}, onMouseEnter: (e) => {
|
|
3236
|
+
e.currentTarget.style.backgroundColor = theme.colors.primary[700];
|
|
3237
|
+
}, onMouseLeave: (e) => {
|
|
3238
|
+
e.currentTarget.style.backgroundColor = theme.colors.primary[600];
|
|
3239
|
+
}, onClick: newChat, children: "+" }), Object.keys(chats)
|
|
3240
|
+
.sort((a, b) => Number(b) - Number(a))
|
|
3241
|
+
.map((chatId) => (jsxs("div", { className: "flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-colors w-full", style: {
|
|
3242
|
+
backgroundColor: chatId === currentChatId
|
|
3243
|
+
? theme.colors.primary[900]
|
|
3244
|
+
: theme.colors.surface.primary,
|
|
3245
|
+
color: chatId === currentChatId
|
|
3246
|
+
? theme.colors.primary[100]
|
|
3247
|
+
: theme.colors.text.secondary,
|
|
3248
|
+
fontWeight: chatId === currentChatId ? 'bold' : 'normal',
|
|
3249
|
+
}, onMouseEnter: (e) => {
|
|
3250
|
+
if (chatId !== currentChatId) {
|
|
3251
|
+
e.currentTarget.style.backgroundColor =
|
|
3252
|
+
theme.colors.surface.secondary;
|
|
3253
|
+
}
|
|
3254
|
+
}, onMouseLeave: (e) => {
|
|
3255
|
+
if (chatId !== currentChatId) {
|
|
3256
|
+
e.currentTarget.style.backgroundColor =
|
|
3257
|
+
theme.colors.surface.primary;
|
|
3258
|
+
}
|
|
3259
|
+
}, onClick: () => switchChat(chatId), title: (chats[chatId]?.[0]
|
|
3260
|
+
? getTextFromMessage(chats[chatId][0])
|
|
3261
|
+
: '')?.slice(0, 30) ||
|
|
3262
|
+
`Chat started at ${new Date(Number(chatId)).toLocaleString()}`, children: [jsx("span", { className: "flex items-center justify-center w-7 h-7 rounded-full bg-blue-50", children: jsx("svg", { width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", children: jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) }) }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "truncate font-semibold", children: (chats[chatId]?.[0]
|
|
3263
|
+
? getTextFromMessage(chats[chatId][0])
|
|
3264
|
+
: '')?.slice(0, 18) || 'New chat' }), jsx("div", { className: "text-xs text-gray-400", children: (() => {
|
|
3265
|
+
const now = Date.now();
|
|
3266
|
+
const diff = Math.floor((now - Number(chatId)) / 1000);
|
|
3267
|
+
if (diff < 60)
|
|
3268
|
+
return 'Just now';
|
|
3269
|
+
if (diff < 3600)
|
|
3270
|
+
return `${Math.floor(diff / 60)}m ago`;
|
|
3271
|
+
if (diff < 86400)
|
|
3272
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
3273
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
3274
|
+
})() })] })] }, chatId)))] }) }));
|
|
3275
|
+
}
|
|
3276
|
+
/**
|
|
3277
|
+
* FloatingChatButton component
|
|
3278
|
+
* @param onClick - Function to handle the click event
|
|
3279
|
+
* @param theme - Theme
|
|
3280
|
+
* @param icon - Icon for the floating button
|
|
3281
|
+
* @param className - Class name for the floating button
|
|
3282
|
+
* @param position - Position of the floating button
|
|
3283
|
+
* @param size - Size of the floating button
|
|
3284
|
+
*/
|
|
3285
|
+
function FloatingChatButton({ onClick, theme = defaultTheme, icon, className = '', position = 'bottom-right', size = 'md', }) {
|
|
3286
|
+
// Note: Theme is now handled via Tailwind classes directly
|
|
3287
|
+
// Position classes
|
|
3288
|
+
const positionClasses = {
|
|
3289
|
+
'bottom-right': 'bottom-6 right-6',
|
|
3290
|
+
'bottom-left': 'bottom-6 left-6',
|
|
3291
|
+
'top-right': 'top-6 right-6',
|
|
3292
|
+
'top-left': 'top-6 left-6',
|
|
3293
|
+
};
|
|
3294
|
+
// Size classes
|
|
3295
|
+
const sizeClasses = {
|
|
3296
|
+
sm: 'w-12 h-12',
|
|
3297
|
+
md: 'w-14 h-14',
|
|
3298
|
+
lg: 'w-16 h-16',
|
|
3299
|
+
};
|
|
3300
|
+
const iconSizes = {
|
|
3301
|
+
sm: { width: 20, height: 20 },
|
|
3302
|
+
md: { width: 24, height: 24 },
|
|
3303
|
+
lg: { width: 28, height: 28 },
|
|
3304
|
+
};
|
|
3305
|
+
return (jsx("div", { className: `fixed z-50 ${positionClasses[position]} ${className}`, children: jsx("button", { onClick: onClick, className: `
|
|
3306
|
+
${sizeClasses[size]}
|
|
3307
|
+
bg-blue-600 hover:bg-blue-700
|
|
3308
|
+
text-white rounded-full shadow-lg hover:shadow-xl
|
|
3309
|
+
flex items-center justify-center
|
|
3310
|
+
transition-all duration-200 ease-in-out
|
|
3311
|
+
hover:scale-105 active:scale-95
|
|
3312
|
+
focus:outline-none focus:ring-4 focus:ring-blue-200
|
|
3313
|
+
`, title: "Open AI Assistant", "aria-label": "Open AI Assistant", children: icon || (jsx(Icon, { name: "ai", size: iconSizes[size].width, className: "text-white" })) }) }));
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
/**
|
|
3317
|
+
* Generic helper that patches an existing object with an AI-generated payload.
|
|
3318
|
+
*
|
|
3319
|
+
* • Skips undefined / null values so they never blank user input
|
|
3320
|
+
* • Coerces strings → numbers when the draft already stores a number
|
|
3321
|
+
* • Allows per-field key mapping & transform overrides
|
|
3322
|
+
*/
|
|
3323
|
+
function mergeWithAi(draft, ai, config = {}) {
|
|
3324
|
+
if (!ai || Object.keys(ai).length === 0)
|
|
3325
|
+
return draft;
|
|
3326
|
+
const { keyMap = {}, transforms = {} } = config;
|
|
3327
|
+
// Clone current state so we don’t mutate in-place
|
|
3328
|
+
const next = { ...draft };
|
|
3329
|
+
for (const rawKey of Object.keys(ai)) {
|
|
3330
|
+
// Map AI key → draft key if necessary
|
|
3331
|
+
const k = keyMap[rawKey] ?? rawKey;
|
|
3332
|
+
const incoming = ai[rawKey];
|
|
3333
|
+
if (incoming == null)
|
|
3334
|
+
continue;
|
|
3335
|
+
// Field-specific transform takes precedence
|
|
3336
|
+
if (k in transforms) {
|
|
3337
|
+
next[k] = transforms[k](incoming, next);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
// Primitive coercion: if existing field is numeric, cast to number
|
|
3341
|
+
if (typeof next[k] === 'number') {
|
|
3342
|
+
// @ts-expect-error – numeric cast
|
|
3343
|
+
next[k] = Number(incoming);
|
|
3344
|
+
}
|
|
3345
|
+
else {
|
|
3346
|
+
// @ts-expect-error – dynamic key assignment
|
|
3347
|
+
next[k] = incoming;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return next;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
/**
|
|
3354
|
+
* React hook that merges an AI-generated payload into local form state.
|
|
3355
|
+
*
|
|
3356
|
+
* Usage:
|
|
3357
|
+
* const { pending, applyPending } = useAiMerge({ ai, draft, onMerged })
|
|
3358
|
+
* <WarningModal isOpen={!!pending} onConfirm={applyPending} />
|
|
3359
|
+
*/
|
|
3360
|
+
function useAiMerge({ ai, draft, onMerged, config }) {
|
|
3361
|
+
const [pending, setPending] = useState(null);
|
|
3362
|
+
useEffect(() => {
|
|
3363
|
+
if (!ai || Object.keys(ai).length === 0)
|
|
3364
|
+
return;
|
|
3365
|
+
const merged = mergeWithAi(draft, ai, config);
|
|
3366
|
+
// if nothing changed, no need to do anything
|
|
3367
|
+
const changed = JSON.stringify(draft) !== JSON.stringify(merged);
|
|
3368
|
+
if (!changed)
|
|
3369
|
+
return;
|
|
3370
|
+
// naïve ‘dirty’ check – consumer can decide whether to overwrite immediately
|
|
3371
|
+
setPending(ai);
|
|
3372
|
+
}, [ai]);
|
|
3373
|
+
const applyPending = () => {
|
|
3374
|
+
if (!pending)
|
|
3375
|
+
return;
|
|
3376
|
+
const merged = mergeWithAi(draft, pending, config);
|
|
3377
|
+
onMerged(merged);
|
|
3378
|
+
setPending(null);
|
|
3379
|
+
};
|
|
3380
|
+
const clearPending = () => setPending(null);
|
|
3381
|
+
return { pending, applyPending, clearPending };
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
export { ChatPanel, Icon as CommandIcon, Conversation, ErrorBoundary, ModelSwitcher, SuggestionCard, SuggestionsPanel, defaultTheme as defaultCommandTheme, mergeWithAi, useAiMerge, useDebouncedSuggestions, useModelSwitcher, useStreamingAI, useSuggestions };
|
|
3385
|
+
//# sourceMappingURL=index.esm.js.map
|