nextjs-chatbot-ui 1.1.0 → 1.1.2

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.
@@ -1,566 +1,701 @@
1
- 'use client';
2
-
3
- import React, { useState } from 'react';
4
- import { DatabaseType, DatabaseConnection, ColumnSelection, DatabaseConfig, AdminSetupProps } from '../types/admin';
5
- import clsx from 'clsx';
6
-
7
- const AdminSetup: React.FC<AdminSetupProps> = ({
8
- onSave,
9
- onTestConnection,
10
- onFetchColumns,
11
- }) => {
12
- const [isModalOpen, setIsModalOpen] = useState(false);
13
- const [currentStep, setCurrentStep] = useState<'connection' | 'columns'>('connection');
14
- const [isConnecting, setIsConnecting] = useState(false);
15
- const [connectionError, setConnectionError] = useState<string | null>(null);
16
- const [availableColumns, setAvailableColumns] = useState<string[]>([]);
17
- const [isLoadingColumns, setIsLoadingColumns] = useState(false);
18
-
19
- const [dbType, setDbType] = useState<DatabaseType>('mongodb');
20
- const [connection, setConnection] = useState<DatabaseConnection>({
21
- type: 'mongodb',
22
- host: '',
23
- port: 27017,
24
- database: '',
25
- username: '',
26
- password: '',
27
- connectionString: '',
28
- ssl: false,
29
- });
30
-
31
- const [columnSelection, setColumnSelection] = useState<ColumnSelection>({
32
- embeddingColumns: [],
33
- llmColumns: [],
34
- chromaColumns: [],
35
- });
36
-
37
- const handleDbTypeChange = (type: DatabaseType) => {
38
- setDbType(type);
39
- setConnection({
40
- ...connection,
41
- type,
42
- port: type === 'mongodb' ? 27017 : 5432,
43
- });
44
- setConnectionError(null);
45
- };
46
-
47
- const handleConnectionChange = (field: keyof DatabaseConnection, value: any) => {
48
- setConnection({
49
- ...connection,
50
- [field]: value,
51
- });
52
- setConnectionError(null);
53
- };
54
-
55
- const handleTestConnection = async () => {
56
- if (!onTestConnection) {
57
- // Default test - just validate fields
58
- if (!connection.host || !connection.database) {
59
- setConnectionError('Please fill in all required fields');
60
- return;
61
- }
62
- setConnectionError(null);
63
- return;
64
- }
65
-
66
- setIsConnecting(true);
67
- setConnectionError(null);
68
-
69
- try {
70
- const isValid = await onTestConnection(connection);
71
- if (isValid) {
72
- setConnectionError(null);
73
- // Fetch columns after successful connection
74
- await handleFetchColumns();
75
- } else {
76
- setConnectionError('Connection failed. Please check your credentials.');
77
- }
78
- } catch (error: any) {
79
- setConnectionError(error.message || 'Connection failed. Please try again.');
80
- } finally {
81
- setIsConnecting(false);
82
- }
83
- };
84
-
85
- const handleFetchColumns = async () => {
86
- if (!onFetchColumns) {
87
- // Mock columns for demo
88
- setAvailableColumns(['id', 'title', 'content', 'description', 'category', 'tags', 'created_at', 'updated_at']);
89
- setIsLoadingColumns(false);
90
- return;
91
- }
92
-
93
- setIsLoadingColumns(true);
94
- try {
95
- const columns = await onFetchColumns(connection);
96
- setAvailableColumns(columns);
97
- } catch (error: any) {
98
- setConnectionError(error.message || 'Failed to fetch columns');
99
- } finally {
100
- setIsLoadingColumns(false);
101
- }
102
- };
103
-
104
- const handleConnectAndNext = async () => {
105
- // Validate connection fields
106
- if (dbType === 'mongodb') {
107
- if (!connection.connectionString && (!connection.host || !connection.database)) {
108
- setConnectionError('Please provide connection string or host and database');
109
- return;
110
- }
111
- } else {
112
- if (!connection.host || !connection.database || !connection.username || !connection.password) {
113
- setConnectionError('Please fill in all required fields');
114
- return;
115
- }
116
- }
117
-
118
- // Test connection
119
- await handleTestConnection();
120
-
121
- // If no error, move to next step
122
- if (!connectionError) {
123
- setCurrentStep('columns');
124
- }
125
- };
126
-
127
- const handleColumnToggle = (column: string, category: keyof ColumnSelection) => {
128
- setColumnSelection((prev) => {
129
- const currentColumns = prev[category];
130
- const isSelected = currentColumns.includes(column);
131
-
132
- return {
133
- ...prev,
134
- [category]: isSelected
135
- ? currentColumns.filter((c) => c !== column)
136
- : [...currentColumns, column],
137
- };
138
- });
139
- };
140
-
141
- const handleSave = () => {
142
- const config: DatabaseConfig = {
143
- connection,
144
- columnSelection,
145
- };
146
-
147
- if (onSave) {
148
- onSave(config);
149
- }
150
-
151
- // Close modal and reset
152
- setIsModalOpen(false);
153
- setCurrentStep('connection');
154
- setConnectionError(null);
155
- };
156
-
157
- const handleClose = () => {
158
- setIsModalOpen(false);
159
- setCurrentStep('connection');
160
- setConnectionError(null);
161
- setColumnSelection({
162
- embeddingColumns: [],
163
- llmColumns: [],
164
- chromaColumns: [],
165
- });
166
- };
167
-
168
- return (
169
- <>
170
- {/* Sidebar Button/Item - This can be integrated into admin sidebar */}
171
- <button
172
- onClick={() => setIsModalOpen(true)}
173
- className="w-full flex items-center gap-3 px-4 py-2.5 text-left hover:bg-gray-100 rounded-lg transition-colors"
174
- >
175
- <svg
176
- xmlns="http://www.w3.org/2000/svg"
177
- className="h-5 w-5 text-gray-600"
178
- fill="none"
179
- viewBox="0 0 24 24"
180
- stroke="currentColor"
181
- >
182
- <path
183
- strokeLinecap="round"
184
- strokeLinejoin="round"
185
- strokeWidth={2}
186
- d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
187
- />
188
- </svg>
189
- <span className="text-sm font-medium text-gray-700">Database Setup</span>
190
- </button>
191
-
192
- {/* Modal */}
193
- {isModalOpen && (
194
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
195
- <div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
196
- {/* Modal Header */}
197
- <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
198
- <h2 className="text-xl font-semibold text-gray-900">
199
- {currentStep === 'connection' ? 'Database Connection' : 'Select Columns'}
200
- </h2>
201
- <button
202
- onClick={handleClose}
203
- className="text-gray-400 hover:text-gray-600 transition-colors"
204
- >
205
- <svg
206
- xmlns="http://www.w3.org/2000/svg"
207
- className="h-6 w-6"
208
- fill="none"
209
- viewBox="0 0 24 24"
210
- stroke="currentColor"
211
- >
212
- <path
213
- strokeLinecap="round"
214
- strokeLinejoin="round"
215
- strokeWidth={2}
216
- d="M6 18L18 6M6 6l12 12"
217
- />
218
- </svg>
219
- </button>
220
- </div>
221
-
222
- {/* Modal Content */}
223
- <div className="flex-1 overflow-y-auto px-6 py-4">
224
- {currentStep === 'connection' ? (
225
- <div className="space-y-6">
226
- {/* Database Type Selection */}
227
- <div>
228
- <label className="block text-sm font-medium text-gray-700 mb-3">
229
- Database Type
230
- </label>
231
- <div className="grid grid-cols-2 gap-4">
232
- <button
233
- onClick={() => handleDbTypeChange('mongodb')}
234
- className={clsx(
235
- 'p-4 border-2 rounded-lg transition-all text-left',
236
- dbType === 'mongodb'
237
- ? 'border-blue-500 bg-blue-50'
238
- : 'border-gray-200 hover:border-gray-300'
239
- )}
240
- >
241
- <div className="flex items-center gap-3">
242
- <div className={clsx(
243
- 'w-5 h-5 rounded-full border-2 flex items-center justify-center',
244
- dbType === 'mongodb' ? 'border-blue-500' : 'border-gray-300'
245
- )}>
246
- {dbType === 'mongodb' && (
247
- <div className="w-3 h-3 rounded-full bg-blue-500" />
248
- )}
249
- </div>
250
- <div>
251
- <div className="font-semibold text-gray-900">MongoDB</div>
252
- <div className="text-xs text-gray-500">NoSQL Database</div>
253
- </div>
254
- </div>
255
- </button>
256
- <button
257
- onClick={() => handleDbTypeChange('postgres')}
258
- className={clsx(
259
- 'p-4 border-2 rounded-lg transition-all text-left',
260
- dbType === 'postgres'
261
- ? 'border-blue-500 bg-blue-50'
262
- : 'border-gray-200 hover:border-gray-300'
263
- )}
264
- >
265
- <div className="flex items-center gap-3">
266
- <div className={clsx(
267
- 'w-5 h-5 rounded-full border-2 flex items-center justify-center',
268
- dbType === 'postgres' ? 'border-blue-500' : 'border-gray-300'
269
- )}>
270
- {dbType === 'postgres' && (
271
- <div className="w-3 h-3 rounded-full bg-blue-500" />
272
- )}
273
- </div>
274
- <div>
275
- <div className="font-semibold text-gray-900">PostgreSQL</div>
276
- <div className="text-xs text-gray-500">SQL Database</div>
277
- </div>
278
- </div>
279
- </button>
280
- </div>
281
- </div>
282
-
283
- {/* Connection Fields */}
284
- {dbType === 'mongodb' ? (
285
- <div className="space-y-4">
286
- <div>
287
- <label className="block text-sm font-medium text-gray-700 mb-2">
288
- Connection String (Recommended)
289
- </label>
290
- <input
291
- type="text"
292
- value={connection.connectionString || ''}
293
- onChange={(e) => handleConnectionChange('connectionString', e.target.value)}
294
- placeholder="mongodb://username:password@host:port/database"
295
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
296
- />
297
- <p className="mt-1 text-xs text-gray-500">
298
- Or fill individual fields below
299
- </p>
300
- </div>
301
- <div className="border-t border-gray-200 pt-4">
302
- <div className="grid grid-cols-2 gap-4">
303
- <div>
304
- <label className="block text-sm font-medium text-gray-700 mb-2">
305
- Host *
306
- </label>
307
- <input
308
- type="text"
309
- value={connection.host}
310
- onChange={(e) => handleConnectionChange('host', e.target.value)}
311
- placeholder="localhost"
312
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
313
- />
314
- </div>
315
- <div>
316
- <label className="block text-sm font-medium text-gray-700 mb-2">
317
- Port *
318
- </label>
319
- <input
320
- type="number"
321
- value={connection.port}
322
- onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 27017)}
323
- placeholder="27017"
324
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
325
- />
326
- </div>
327
- </div>
328
- <div className="mt-4">
329
- <label className="block text-sm font-medium text-gray-700 mb-2">
330
- Database Name *
331
- </label>
332
- <input
333
- type="text"
334
- value={connection.database}
335
- onChange={(e) => handleConnectionChange('database', e.target.value)}
336
- placeholder="my_database"
337
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
338
- />
339
- </div>
340
- <div className="mt-4">
341
- <label className="flex items-center gap-2">
342
- <input
343
- type="checkbox"
344
- checked={connection.ssl || false}
345
- onChange={(e) => handleConnectionChange('ssl', e.target.checked)}
346
- className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
347
- />
348
- <span className="text-sm text-gray-700">Enable SSL</span>
349
- </label>
350
- </div>
351
- </div>
352
- </div>
353
- ) : (
354
- <div className="space-y-4">
355
- <div className="grid grid-cols-2 gap-4">
356
- <div>
357
- <label className="block text-sm font-medium text-gray-700 mb-2">
358
- Host *
359
- </label>
360
- <input
361
- type="text"
362
- value={connection.host}
363
- onChange={(e) => handleConnectionChange('host', e.target.value)}
364
- placeholder="localhost"
365
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
366
- />
367
- </div>
368
- <div>
369
- <label className="block text-sm font-medium text-gray-700 mb-2">
370
- Port *
371
- </label>
372
- <input
373
- type="number"
374
- value={connection.port}
375
- onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 5432)}
376
- placeholder="5432"
377
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
378
- />
379
- </div>
380
- </div>
381
- <div>
382
- <label className="block text-sm font-medium text-gray-700 mb-2">
383
- Database Name *
384
- </label>
385
- <input
386
- type="text"
387
- value={connection.database}
388
- onChange={(e) => handleConnectionChange('database', e.target.value)}
389
- placeholder="my_database"
390
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
391
- />
392
- </div>
393
- <div className="grid grid-cols-2 gap-4">
394
- <div>
395
- <label className="block text-sm font-medium text-gray-700 mb-2">
396
- Username *
397
- </label>
398
- <input
399
- type="text"
400
- value={connection.username || ''}
401
- onChange={(e) => handleConnectionChange('username', e.target.value)}
402
- placeholder="postgres"
403
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
404
- />
405
- </div>
406
- <div>
407
- <label className="block text-sm font-medium text-gray-700 mb-2">
408
- Password *
409
- </label>
410
- <input
411
- type="password"
412
- value={connection.password || ''}
413
- onChange={(e) => handleConnectionChange('password', e.target.value)}
414
- placeholder="••••••••"
415
- className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
416
- />
417
- </div>
418
- </div>
419
- <div>
420
- <label className="flex items-center gap-2">
421
- <input
422
- type="checkbox"
423
- checked={connection.ssl || false}
424
- onChange={(e) => handleConnectionChange('ssl', e.target.checked)}
425
- className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
426
- />
427
- <span className="text-sm text-gray-700">Enable SSL</span>
428
- </label>
429
- </div>
430
- </div>
431
- )}
432
-
433
- {connectionError && (
434
- <div className="bg-red-50 border border-red-200 rounded-lg p-4">
435
- <p className="text-sm text-red-800">{connectionError}</p>
436
- </div>
437
- )}
438
- </div>
439
- ) : (
440
- <div className="space-y-6">
441
- {isLoadingColumns ? (
442
- <div className="flex items-center justify-center py-8">
443
- <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
444
- </div>
445
- ) : (
446
- <>
447
- <div>
448
- <p className="text-sm text-gray-600 mb-4">
449
- Select which columns to use for embedding, LLM processing, and ChromaDB storage.
450
- </p>
451
- </div>
452
-
453
- {/* Embedding Columns */}
454
- <div>
455
- <label className="block text-sm font-medium text-gray-700 mb-3">
456
- Embedding Columns
457
- </label>
458
- <div className="grid grid-cols-2 gap-2 max-h-32 overflow-y-auto border border-gray-200 rounded-lg p-3">
459
- {availableColumns.map((column) => (
460
- <label
461
- key={`embedding-${column}`}
462
- className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 p-2 rounded"
463
- >
464
- <input
465
- type="checkbox"
466
- checked={columnSelection.embeddingColumns.includes(column)}
467
- onChange={() => handleColumnToggle(column, 'embeddingColumns')}
468
- className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
469
- />
470
- <span className="text-sm text-gray-700">{column}</span>
471
- </label>
472
- ))}
473
- </div>
474
- </div>
475
-
476
- {/* LLM Columns */}
477
- <div>
478
- <label className="block text-sm font-medium text-gray-700 mb-3">
479
- LLM Columns
480
- </label>
481
- <div className="grid grid-cols-2 gap-2 max-h-32 overflow-y-auto border border-gray-200 rounded-lg p-3">
482
- {availableColumns.map((column) => (
483
- <label
484
- key={`llm-${column}`}
485
- className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 p-2 rounded"
486
- >
487
- <input
488
- type="checkbox"
489
- checked={columnSelection.llmColumns.includes(column)}
490
- onChange={() => handleColumnToggle(column, 'llmColumns')}
491
- className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
492
- />
493
- <span className="text-sm text-gray-700">{column}</span>
494
- </label>
495
- ))}
496
- </div>
497
- </div>
498
-
499
- {/* ChromaDB Columns */}
500
- <div>
501
- <label className="block text-sm font-medium text-gray-700 mb-3">
502
- ChromaDB Columns
503
- </label>
504
- <div className="grid grid-cols-2 gap-2 max-h-32 overflow-y-auto border border-gray-200 rounded-lg p-3">
505
- {availableColumns.map((column) => (
506
- <label
507
- key={`chroma-${column}`}
508
- className="flex items-center gap-2 cursor-pointer hover:bg-gray-50 p-2 rounded"
509
- >
510
- <input
511
- type="checkbox"
512
- checked={columnSelection.chromaColumns.includes(column)}
513
- onChange={() => handleColumnToggle(column, 'chromaColumns')}
514
- className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
515
- />
516
- <span className="text-sm text-gray-700">{column}</span>
517
- </label>
518
- ))}
519
- </div>
520
- </div>
521
- </>
522
- )}
523
- </div>
524
- )}
525
- </div>
526
-
527
- {/* Modal Footer */}
528
- <div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
529
- <button
530
- onClick={currentStep === 'columns' ? () => setCurrentStep('connection') : handleClose}
531
- className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
532
- >
533
- {currentStep === 'columns' ? 'Back' : 'Cancel'}
534
- </button>
535
- <div className="flex gap-3">
536
- {currentStep === 'connection' ? (
537
- <button
538
- onClick={handleConnectAndNext}
539
- disabled={isConnecting || isLoadingColumns}
540
- className="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
541
- >
542
- {isConnecting ? 'Connecting...' : 'Connect & Next'}
543
- </button>
544
- ) : (
545
- <button
546
- onClick={handleSave}
547
- disabled={
548
- columnSelection.embeddingColumns.length === 0 &&
549
- columnSelection.llmColumns.length === 0 &&
550
- columnSelection.chromaColumns.length === 0
551
- }
552
- className="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
553
- >
554
- Save Configuration
555
- </button>
556
- )}
557
- </div>
558
- </div>
559
- </div>
560
- </div>
561
- )}
562
- </>
563
- );
564
- };
565
-
566
- export default AdminSetup;
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { DatabaseType, DatabaseConnection, ColumnSelection, DatabaseConfig, AdminSetupProps } from '../types/admin';
5
+ import clsx from 'clsx';
6
+
7
+ const AdminSetup: React.FC<AdminSetupProps> = ({
8
+ onSave,
9
+ onTestConnection,
10
+ onFetchColumns,
11
+ }) => {
12
+ const [isModalOpen, setIsModalOpen] = useState(false);
13
+ const [currentStep, setCurrentStep] = useState<'connection' | 'columns'>('connection');
14
+ const [isConnecting, setIsConnecting] = useState(false);
15
+ const [connectionError, setConnectionError] = useState<string | null>(null);
16
+ const [connectionSuccess, setConnectionSuccess] = useState(false);
17
+ const [availableColumns, setAvailableColumns] = useState<string[]>([]);
18
+ const [isLoadingColumns, setIsLoadingColumns] = useState(false);
19
+
20
+ const [dbType, setDbType] = useState<DatabaseType>('mongodb');
21
+ const [connection, setConnection] = useState<DatabaseConnection>({
22
+ type: 'mongodb',
23
+ host: '',
24
+ port: 27017,
25
+ database: '',
26
+ username: '',
27
+ password: '',
28
+ connectionString: '',
29
+ ssl: false,
30
+ });
31
+
32
+ const [columnSelection, setColumnSelection] = useState<ColumnSelection>({
33
+ embeddingColumns: [],
34
+ llmColumns: [],
35
+ chromaColumns: [],
36
+ });
37
+
38
+ const handleDbTypeChange = (type: DatabaseType) => {
39
+ setDbType(type);
40
+ setConnection({
41
+ ...connection,
42
+ type,
43
+ port: type === 'mongodb' ? 27017 : 5432,
44
+ });
45
+ setConnectionError(null);
46
+ };
47
+
48
+ const handleConnectionChange = (field: keyof DatabaseConnection, value: any) => {
49
+ setConnection({
50
+ ...connection,
51
+ [field]: value,
52
+ });
53
+ setConnectionError(null);
54
+ };
55
+
56
+ const handleTestConnection = async (): Promise<boolean> => {
57
+ setIsConnecting(true);
58
+ setConnectionError(null);
59
+ setConnectionSuccess(false);
60
+
61
+ try {
62
+ let isValid: boolean;
63
+
64
+ if (onTestConnection) {
65
+ // Use provided handler
66
+ isValid = await onTestConnection(connection);
67
+ } else {
68
+ // Default: Try to call backend API
69
+ try {
70
+ const response = await fetch('/api/database/test', {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ },
75
+ body: JSON.stringify(connection),
76
+ });
77
+
78
+ if (!response.ok) {
79
+ const errorData = await response.json().catch(() => ({}));
80
+ throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
81
+ }
82
+
83
+ const data = await response.json();
84
+ isValid = data.success === true || response.ok;
85
+ } catch (fetchError: any) {
86
+ // If API endpoint doesn't exist, show helpful error
87
+ if (fetchError.message?.includes('Failed to fetch') || fetchError.message?.includes('404')) {
88
+ throw new Error(
89
+ 'Backend API endpoint not found. Please implement POST /api/database/test endpoint or provide onTestConnection handler.'
90
+ );
91
+ }
92
+ throw fetchError;
93
+ }
94
+ }
95
+
96
+ if (isValid) {
97
+ setConnectionError(null);
98
+ setConnectionSuccess(true);
99
+ return true;
100
+ } else {
101
+ setConnectionError('Connection failed. Please check your credentials and try again.');
102
+ setConnectionSuccess(false);
103
+ return false;
104
+ }
105
+ } catch (error: any) {
106
+ const errorMessage = error.message || 'Connection failed. Please try again.';
107
+ setConnectionError(errorMessage);
108
+ setConnectionSuccess(false);
109
+ console.error('Database connection error:', error);
110
+ return false;
111
+ } finally {
112
+ setIsConnecting(false);
113
+ }
114
+ };
115
+
116
+ const handleFetchColumns = async (): Promise<string[]> => {
117
+ setIsLoadingColumns(true);
118
+ setConnectionError(null);
119
+
120
+ try {
121
+ let columns: string[];
122
+
123
+ if (onFetchColumns) {
124
+ // Use provided handler
125
+ columns = await onFetchColumns(connection);
126
+ } else {
127
+ // Default: Try to call backend API
128
+ try {
129
+ const response = await fetch('/api/database/columns', {
130
+ method: 'POST',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ },
134
+ body: JSON.stringify(connection),
135
+ });
136
+
137
+ if (!response.ok) {
138
+ const errorData = await response.json().catch(() => ({}));
139
+ throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
140
+ }
141
+
142
+ const data = await response.json();
143
+ columns = data.columns || data.columnNames || [];
144
+ } catch (fetchError: any) {
145
+ // If API endpoint doesn't exist, show helpful error
146
+ if (fetchError.message?.includes('Failed to fetch') || fetchError.message?.includes('404')) {
147
+ throw new Error(
148
+ 'Backend API endpoint not found. Please implement POST /api/database/columns endpoint or provide onFetchColumns handler.'
149
+ );
150
+ }
151
+ throw fetchError;
152
+ }
153
+ }
154
+
155
+ if (columns && columns.length > 0) {
156
+ setAvailableColumns(columns);
157
+ setIsLoadingColumns(false);
158
+ return columns;
159
+ } else {
160
+ setConnectionError('No columns found in the database. Please check your database connection and table selection.');
161
+ setIsLoadingColumns(false);
162
+ return [];
163
+ }
164
+ } catch (error: any) {
165
+ const errorMessage = error.message || 'Failed to fetch columns. Please try again.';
166
+ setConnectionError(errorMessage);
167
+ setIsLoadingColumns(false);
168
+ console.error('Fetch columns error:', error);
169
+ return [];
170
+ }
171
+ };
172
+
173
+ const handleConnectAndNext = async () => {
174
+ // Validate connection fields
175
+ if (dbType === 'mongodb') {
176
+ if (!connection.connectionString && (!connection.host || !connection.database)) {
177
+ setConnectionError('Please provide connection string or host and database');
178
+ setConnectionSuccess(false);
179
+ return;
180
+ }
181
+ } else {
182
+ if (!connection.host || !connection.database || !connection.username || !connection.password) {
183
+ setConnectionError('Please fill in all required fields');
184
+ setConnectionSuccess(false);
185
+ return;
186
+ }
187
+ }
188
+
189
+ // Clear previous errors
190
+ setConnectionError(null);
191
+ setConnectionSuccess(false);
192
+
193
+ // Test connection first
194
+ const connectionSuccess = await handleTestConnection();
195
+
196
+ if (!connectionSuccess) {
197
+ // Connection failed, don't proceed
198
+ return;
199
+ }
200
+
201
+ // If connection successful, fetch columns
202
+ const fetchedColumns = await handleFetchColumns();
203
+
204
+ if (fetchedColumns && fetchedColumns.length > 0) {
205
+ // Successfully fetched columns, move to next step
206
+ setCurrentStep('columns');
207
+ setConnectionError(null);
208
+ } else {
209
+ // Column fetching failed, show error but keep connection success
210
+ // Error is already set in handleFetchColumns
211
+ }
212
+ };
213
+
214
+ const handleColumnToggle = (column: string, category: keyof ColumnSelection) => {
215
+ setColumnSelection((prev) => {
216
+ const currentColumns = prev[category];
217
+ const isSelected = currentColumns.includes(column);
218
+
219
+ return {
220
+ ...prev,
221
+ [category]: isSelected
222
+ ? currentColumns.filter((c) => c !== column)
223
+ : [...currentColumns, column],
224
+ };
225
+ });
226
+ };
227
+
228
+ const handleSave = () => {
229
+ const config: DatabaseConfig = {
230
+ connection,
231
+ columnSelection,
232
+ };
233
+
234
+ if (onSave) {
235
+ onSave(config);
236
+ }
237
+
238
+ // Close modal and reset
239
+ setIsModalOpen(false);
240
+ setCurrentStep('connection');
241
+ setConnectionError(null);
242
+ };
243
+
244
+ const handleClose = () => {
245
+ setIsModalOpen(false);
246
+ setCurrentStep('connection');
247
+ setConnectionError(null);
248
+ setConnectionSuccess(false);
249
+ setAvailableColumns([]);
250
+ setColumnSelection({
251
+ embeddingColumns: [],
252
+ llmColumns: [],
253
+ chromaColumns: [],
254
+ });
255
+ };
256
+
257
+ return (
258
+ <>
259
+ {/* Sidebar Button/Item - This can be integrated into admin sidebar */}
260
+ <button
261
+ onClick={() => setIsModalOpen(true)}
262
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-left hover:bg-gray-100 rounded-lg transition-colors"
263
+ >
264
+ <svg
265
+ xmlns="http://www.w3.org/2000/svg"
266
+ className="h-5 w-5 text-gray-600"
267
+ fill="none"
268
+ viewBox="0 0 24 24"
269
+ stroke="currentColor"
270
+ >
271
+ <path
272
+ strokeLinecap="round"
273
+ strokeLinejoin="round"
274
+ strokeWidth={2}
275
+ d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"
276
+ />
277
+ </svg>
278
+ <span className="text-sm font-medium text-gray-700">Database Setup</span>
279
+ </button>
280
+
281
+ {/* Modal */}
282
+ {isModalOpen && (
283
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
284
+ <div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
285
+ {/* Modal Header */}
286
+ <div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
287
+ <h2 className="text-xl font-semibold text-gray-900">
288
+ {currentStep === 'connection' ? 'Database Connection' : 'Select Columns'}
289
+ </h2>
290
+ <button
291
+ onClick={handleClose}
292
+ className="text-gray-400 hover:text-gray-600 transition-colors"
293
+ >
294
+ <svg
295
+ xmlns="http://www.w3.org/2000/svg"
296
+ className="h-6 w-6"
297
+ fill="none"
298
+ viewBox="0 0 24 24"
299
+ stroke="currentColor"
300
+ >
301
+ <path
302
+ strokeLinecap="round"
303
+ strokeLinejoin="round"
304
+ strokeWidth={2}
305
+ d="M6 18L18 6M6 6l12 12"
306
+ />
307
+ </svg>
308
+ </button>
309
+ </div>
310
+
311
+ {/* Modal Content */}
312
+ <div className="flex-1 overflow-y-auto px-6 py-4">
313
+ {currentStep === 'connection' ? (
314
+ <div className="space-y-6">
315
+ {/* Database Type Selection */}
316
+ <div>
317
+ <label className="block text-sm font-medium text-gray-700 mb-3">
318
+ Database Type
319
+ </label>
320
+ <div className="grid grid-cols-2 gap-4">
321
+ <button
322
+ onClick={() => handleDbTypeChange('mongodb')}
323
+ className={clsx(
324
+ 'p-4 border-2 rounded-lg transition-all text-left',
325
+ dbType === 'mongodb'
326
+ ? 'border-blue-500 bg-blue-50'
327
+ : 'border-gray-200 hover:border-gray-300'
328
+ )}
329
+ >
330
+ <div className="flex items-center gap-3">
331
+ <div className={clsx(
332
+ 'w-5 h-5 rounded-full border-2 flex items-center justify-center',
333
+ dbType === 'mongodb' ? 'border-blue-500' : 'border-gray-300'
334
+ )}>
335
+ {dbType === 'mongodb' && (
336
+ <div className="w-3 h-3 rounded-full bg-blue-500" />
337
+ )}
338
+ </div>
339
+ <div>
340
+ <div className="font-semibold text-gray-900">MongoDB</div>
341
+ <div className="text-xs text-gray-500">NoSQL Database</div>
342
+ </div>
343
+ </div>
344
+ </button>
345
+ <button
346
+ onClick={() => handleDbTypeChange('postgres')}
347
+ className={clsx(
348
+ 'p-4 border-2 rounded-lg transition-all text-left',
349
+ dbType === 'postgres'
350
+ ? 'border-blue-500 bg-blue-50'
351
+ : 'border-gray-200 hover:border-gray-300'
352
+ )}
353
+ >
354
+ <div className="flex items-center gap-3">
355
+ <div className={clsx(
356
+ 'w-5 h-5 rounded-full border-2 flex items-center justify-center',
357
+ dbType === 'postgres' ? 'border-blue-500' : 'border-gray-300'
358
+ )}>
359
+ {dbType === 'postgres' && (
360
+ <div className="w-3 h-3 rounded-full bg-blue-500" />
361
+ )}
362
+ </div>
363
+ <div>
364
+ <div className="font-semibold text-gray-900">PostgreSQL</div>
365
+ <div className="text-xs text-gray-500">SQL Database</div>
366
+ </div>
367
+ </div>
368
+ </button>
369
+ </div>
370
+ </div>
371
+
372
+ {/* Connection Fields */}
373
+ {dbType === 'mongodb' ? (
374
+ <div className="space-y-4">
375
+ <div>
376
+ <label className="block text-sm font-medium text-gray-700 mb-2">
377
+ Connection String (Recommended)
378
+ </label>
379
+ <input
380
+ type="text"
381
+ value={connection.connectionString || ''}
382
+ onChange={(e) => handleConnectionChange('connectionString', e.target.value)}
383
+ placeholder="mongodb://username:password@host:port/database"
384
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
385
+ />
386
+ <p className="mt-1 text-xs text-gray-500">
387
+ Or fill individual fields below
388
+ </p>
389
+ </div>
390
+ <div className="border-t border-gray-200 pt-4">
391
+ <div className="grid grid-cols-2 gap-4">
392
+ <div>
393
+ <label className="block text-sm font-medium text-gray-700 mb-2">
394
+ Host *
395
+ </label>
396
+ <input
397
+ type="text"
398
+ value={connection.host}
399
+ onChange={(e) => handleConnectionChange('host', e.target.value)}
400
+ placeholder="localhost"
401
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
402
+ />
403
+ </div>
404
+ <div>
405
+ <label className="block text-sm font-medium text-gray-700 mb-2">
406
+ Port *
407
+ </label>
408
+ <input
409
+ type="number"
410
+ value={connection.port}
411
+ onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 27017)}
412
+ placeholder="27017"
413
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
414
+ />
415
+ </div>
416
+ </div>
417
+ <div className="mt-4">
418
+ <label className="block text-sm font-medium text-gray-700 mb-2">
419
+ Database Name *
420
+ </label>
421
+ <input
422
+ type="text"
423
+ value={connection.database}
424
+ onChange={(e) => handleConnectionChange('database', e.target.value)}
425
+ placeholder="my_database"
426
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
427
+ />
428
+ </div>
429
+ <div className="mt-4">
430
+ <label className="flex items-center gap-2">
431
+ <input
432
+ type="checkbox"
433
+ checked={connection.ssl || false}
434
+ onChange={(e) => handleConnectionChange('ssl', e.target.checked)}
435
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
436
+ />
437
+ <span className="text-sm text-gray-700">Enable SSL</span>
438
+ </label>
439
+ </div>
440
+ </div>
441
+ </div>
442
+ ) : (
443
+ <div className="space-y-4">
444
+ <div className="grid grid-cols-2 gap-4">
445
+ <div>
446
+ <label className="block text-sm font-medium text-gray-700 mb-2">
447
+ Host *
448
+ </label>
449
+ <input
450
+ type="text"
451
+ value={connection.host}
452
+ onChange={(e) => handleConnectionChange('host', e.target.value)}
453
+ placeholder="localhost"
454
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
455
+ />
456
+ </div>
457
+ <div>
458
+ <label className="block text-sm font-medium text-gray-700 mb-2">
459
+ Port *
460
+ </label>
461
+ <input
462
+ type="number"
463
+ value={connection.port}
464
+ onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 5432)}
465
+ placeholder="5432"
466
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
467
+ />
468
+ </div>
469
+ </div>
470
+ <div>
471
+ <label className="block text-sm font-medium text-gray-700 mb-2">
472
+ Database Name *
473
+ </label>
474
+ <input
475
+ type="text"
476
+ value={connection.database}
477
+ onChange={(e) => handleConnectionChange('database', e.target.value)}
478
+ placeholder="my_database"
479
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
480
+ />
481
+ </div>
482
+ <div className="grid grid-cols-2 gap-4">
483
+ <div>
484
+ <label className="block text-sm font-medium text-gray-700 mb-2">
485
+ Username *
486
+ </label>
487
+ <input
488
+ type="text"
489
+ value={connection.username || ''}
490
+ onChange={(e) => handleConnectionChange('username', e.target.value)}
491
+ placeholder="postgres"
492
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
493
+ />
494
+ </div>
495
+ <div>
496
+ <label className="block text-sm font-medium text-gray-700 mb-2">
497
+ Password *
498
+ </label>
499
+ <input
500
+ type="password"
501
+ value={connection.password || ''}
502
+ onChange={(e) => handleConnectionChange('password', e.target.value)}
503
+ placeholder="••••••••"
504
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
505
+ />
506
+ </div>
507
+ </div>
508
+ <div>
509
+ <label className="flex items-center gap-2">
510
+ <input
511
+ type="checkbox"
512
+ checked={connection.ssl || false}
513
+ onChange={(e) => handleConnectionChange('ssl', e.target.checked)}
514
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
515
+ />
516
+ <span className="text-sm text-gray-700">Enable SSL</span>
517
+ </label>
518
+ </div>
519
+ </div>
520
+ )}
521
+
522
+ {connectionError && (
523
+ <div className="bg-red-50 border border-red-200 rounded-lg p-4">
524
+ <p className="text-sm text-red-800">{connectionError}</p>
525
+ </div>
526
+ )}
527
+ {connectionSuccess && !connectionError && !isConnecting && (
528
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4">
529
+ <p className="text-sm text-green-800">✓ Connection successful!</p>
530
+ </div>
531
+ )}
532
+ </div>
533
+ ) : (
534
+ <div className="space-y-6">
535
+ {isLoadingColumns ? (
536
+ <div className="flex flex-col items-center justify-center py-8">
537
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mb-3"></div>
538
+ <p className="text-sm text-gray-600">Loading columns...</p>
539
+ </div>
540
+ ) : availableColumns.length === 0 ? (
541
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
542
+ <p className="text-sm text-yellow-800">No columns available. Please go back and check your connection.</p>
543
+ </div>
544
+ ) : (
545
+ <>
546
+ <div>
547
+ <p className="text-sm text-gray-600 mb-2">
548
+ Select which columns to use for <strong>Embeddings</strong>, <strong>LLM processing</strong>, and <strong>ChromaDB storage</strong>.
549
+ </p>
550
+ <p className="text-xs text-gray-500 mb-4">
551
+ Found {availableColumns.length} column{availableColumns.length !== 1 ? 's' : ''} in your database.
552
+ </p>
553
+ </div>
554
+
555
+ {/* Embedding Columns */}
556
+ <div>
557
+ <label className="block text-sm font-medium text-gray-700 mb-2">
558
+ Works with Embeddings
559
+ </label>
560
+ <p className="text-xs text-gray-500 mb-3">Select columns that will be used for embedding generation</p>
561
+ <div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded-lg p-3 bg-gray-50">
562
+ {availableColumns.length === 0 ? (
563
+ <p className="text-sm text-gray-500 col-span-2 text-center py-2">No columns available</p>
564
+ ) : (
565
+ availableColumns.map((column) => (
566
+ <label
567
+ key={`embedding-${column}`}
568
+ className="flex items-center gap-2 cursor-pointer hover:bg-white p-2 rounded transition-colors"
569
+ >
570
+ <input
571
+ type="checkbox"
572
+ checked={columnSelection.embeddingColumns.includes(column)}
573
+ onChange={() => handleColumnToggle(column, 'embeddingColumns')}
574
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
575
+ />
576
+ <span className="text-sm text-gray-700">{column}</span>
577
+ </label>
578
+ ))
579
+ )}
580
+ </div>
581
+ {columnSelection.embeddingColumns.length > 0 && (
582
+ <p className="text-xs text-green-600 mt-1">
583
+ {columnSelection.embeddingColumns.length} column{columnSelection.embeddingColumns.length !== 1 ? 's' : ''} selected
584
+ </p>
585
+ )}
586
+ </div>
587
+
588
+ {/* LLM Columns */}
589
+ <div>
590
+ <label className="block text-sm font-medium text-gray-700 mb-2">
591
+ Works with LLM
592
+ </label>
593
+ <p className="text-xs text-gray-500 mb-3">Select columns that will be processed by the LLM</p>
594
+ <div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded-lg p-3 bg-gray-50">
595
+ {availableColumns.length === 0 ? (
596
+ <p className="text-sm text-gray-500 col-span-2 text-center py-2">No columns available</p>
597
+ ) : (
598
+ availableColumns.map((column) => (
599
+ <label
600
+ key={`llm-${column}`}
601
+ className="flex items-center gap-2 cursor-pointer hover:bg-white p-2 rounded transition-colors"
602
+ >
603
+ <input
604
+ type="checkbox"
605
+ checked={columnSelection.llmColumns.includes(column)}
606
+ onChange={() => handleColumnToggle(column, 'llmColumns')}
607
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
608
+ />
609
+ <span className="text-sm text-gray-700">{column}</span>
610
+ </label>
611
+ ))
612
+ )}
613
+ </div>
614
+ {columnSelection.llmColumns.length > 0 && (
615
+ <p className="text-xs text-green-600 mt-1">
616
+ {columnSelection.llmColumns.length} column{columnSelection.llmColumns.length !== 1 ? 's' : ''} selected
617
+ </p>
618
+ )}
619
+ </div>
620
+
621
+ {/* ChromaDB Columns */}
622
+ <div>
623
+ <label className="block text-sm font-medium text-gray-700 mb-2">
624
+ Works with ChromaDB
625
+ </label>
626
+ <p className="text-xs text-gray-500 mb-3">Select columns that will be stored in ChromaDB</p>
627
+ <div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto border border-gray-200 rounded-lg p-3 bg-gray-50">
628
+ {availableColumns.length === 0 ? (
629
+ <p className="text-sm text-gray-500 col-span-2 text-center py-2">No columns available</p>
630
+ ) : (
631
+ availableColumns.map((column) => (
632
+ <label
633
+ key={`chroma-${column}`}
634
+ className="flex items-center gap-2 cursor-pointer hover:bg-white p-2 rounded transition-colors"
635
+ >
636
+ <input
637
+ type="checkbox"
638
+ checked={columnSelection.chromaColumns.includes(column)}
639
+ onChange={() => handleColumnToggle(column, 'chromaColumns')}
640
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
641
+ />
642
+ <span className="text-sm text-gray-700">{column}</span>
643
+ </label>
644
+ ))
645
+ )}
646
+ </div>
647
+ {columnSelection.chromaColumns.length > 0 && (
648
+ <p className="text-xs text-green-600 mt-1">
649
+ {columnSelection.chromaColumns.length} column{columnSelection.chromaColumns.length !== 1 ? 's' : ''} selected
650
+ </p>
651
+ )}
652
+ </div>
653
+ </>
654
+ )}
655
+ </div>
656
+ )}
657
+ </div>
658
+
659
+ {/* Modal Footer */}
660
+ <div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
661
+ <button
662
+ onClick={currentStep === 'columns' ? () => setCurrentStep('connection') : handleClose}
663
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
664
+ >
665
+ {currentStep === 'columns' ? 'Back' : 'Cancel'}
666
+ </button>
667
+ <div className="flex gap-3">
668
+ {currentStep === 'connection' ? (
669
+ <button
670
+ onClick={handleConnectAndNext}
671
+ disabled={isConnecting || isLoadingColumns}
672
+ className="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center gap-2"
673
+ >
674
+ {isConnecting && (
675
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
676
+ )}
677
+ {isLoadingColumns ? 'Loading Columns...' : isConnecting ? 'Connecting...' : 'Connect & Next'}
678
+ </button>
679
+ ) : (
680
+ <button
681
+ onClick={handleSave}
682
+ disabled={
683
+ columnSelection.embeddingColumns.length === 0 &&
684
+ columnSelection.llmColumns.length === 0 &&
685
+ columnSelection.chromaColumns.length === 0
686
+ }
687
+ className="px-6 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
688
+ >
689
+ Save Configuration
690
+ </button>
691
+ )}
692
+ </div>
693
+ </div>
694
+ </div>
695
+ </div>
696
+ )}
697
+ </>
698
+ );
699
+ };
700
+
701
+ export default AdminSetup;