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