nextjs-chatbot-ui 1.2.0 → 1.3.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.
@@ -8,14 +8,18 @@ import clsx from 'clsx';
8
8
  const AdminSetup: React.FC<AdminSetupProps> = ({
9
9
  onSave,
10
10
  onTestConnection,
11
+ onFetchCollections,
11
12
  onFetchColumns,
12
13
  }) => {
13
14
  const [isModalOpen, setIsModalOpen] = useState(false);
14
- const [currentStep, setCurrentStep] = useState<'connection' | 'columns'>('connection');
15
+ const [currentStep, setCurrentStep] = useState<'connection' | 'collection' | 'columns'>('connection');
15
16
  const [isConnecting, setIsConnecting] = useState(false);
16
17
  const [connectionError, setConnectionError] = useState<string | null>(null);
17
18
  const [connectionSuccess, setConnectionSuccess] = useState(false);
19
+ const [availableCollections, setAvailableCollections] = useState<string[]>([]);
20
+ const [selectedCollection, setSelectedCollection] = useState<string>('');
18
21
  const [availableColumns, setAvailableColumns] = useState<string[]>([]);
22
+ const [isLoadingCollections, setIsLoadingCollections] = useState(false);
19
23
  const [isLoadingColumns, setIsLoadingColumns] = useState(false);
20
24
 
21
25
  const [dbType, setDbType] = useState<DatabaseType>('mongodb');
@@ -60,41 +64,31 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
60
64
  setConnectionSuccess(false);
61
65
 
62
66
  try {
63
- // Ensure default ports are set
64
67
  const normalizedConnection = normalizeConnection(connection, dbType);
65
-
66
- // Transform to backend format
67
68
  const backendRequest = transformConnectionForBackend(connection, dbType);
68
69
 
69
70
  let isValid: boolean;
70
71
 
71
72
  if (onTestConnection) {
72
- // Use provided handler - send normalized connection
73
73
  isValid = await onTestConnection(normalizedConnection);
74
74
  } else {
75
- // Default: Try to call backend API
76
75
  try {
77
76
  const response = await fetch('/api/database/test', {
78
77
  method: 'POST',
79
- headers: {
80
- 'Content-Type': 'application/json',
81
- },
82
- body: JSON.stringify(backendRequest), // Send transformed format
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify(backendRequest),
83
80
  });
84
81
 
85
82
  if (!response.ok) {
86
83
  const errorData = await response.json().catch(() => ({}));
87
- throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
84
+ throw new Error(errorData.message || `HTTP ${response.status}`);
88
85
  }
89
86
 
90
87
  const data = await response.json();
91
88
  isValid = data.success === true || response.ok;
92
89
  } catch (fetchError: any) {
93
- // If API endpoint doesn't exist, show helpful error
94
90
  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
- );
91
+ throw new Error('Backend API endpoint not found. Please implement POST /api/database/test endpoint.');
98
92
  }
99
93
  throw fetchError;
100
94
  }
@@ -105,13 +99,12 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
105
99
  setConnectionSuccess(true);
106
100
  return true;
107
101
  } else {
108
- setConnectionError('Connection failed. Please check your credentials and try again.');
102
+ setConnectionError('Connection failed. Please check your credentials.');
109
103
  setConnectionSuccess(false);
110
104
  return false;
111
105
  }
112
106
  } catch (error: any) {
113
- const errorMessage = error.message || 'Connection failed. Please try again.';
114
- setConnectionError(errorMessage);
107
+ setConnectionError(error.message || 'Connection failed. Please try again.');
115
108
  setConnectionSuccess(false);
116
109
  console.error('Database connection error:', error);
117
110
  return false;
@@ -120,46 +113,91 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
120
113
  }
121
114
  };
122
115
 
123
- const handleFetchColumns = async (): Promise<string[]> => {
124
- setIsLoadingColumns(true);
116
+ const handleFetchCollections = async (): Promise<string[]> => {
117
+ setIsLoadingCollections(true);
125
118
  setConnectionError(null);
126
119
 
127
120
  try {
128
- // Ensure default ports are set
129
121
  const normalizedConnection = normalizeConnection(connection, dbType);
122
+ const backendRequest = transformConnectionForBackend(connection, dbType);
123
+
124
+ let collections: string[];
125
+
126
+ if (onFetchCollections) {
127
+ collections = await onFetchCollections(normalizedConnection);
128
+ } else {
129
+ try {
130
+ const response = await fetch('/api/database/collections', {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify(backendRequest),
134
+ });
135
+
136
+ if (!response.ok) {
137
+ const errorData = await response.json().catch(() => ({}));
138
+ throw new Error(errorData.message || `HTTP ${response.status}`);
139
+ }
130
140
 
131
- // Transform to backend format
141
+ const data = await response.json();
142
+ collections = data.collections || data.tables || data.columnNames || [];
143
+ } catch (fetchError: any) {
144
+ if (fetchError.message?.includes('Failed to fetch') || fetchError.message?.includes('404')) {
145
+ throw new Error('Backend API endpoint not found. Please implement POST /api/database/collections endpoint.');
146
+ }
147
+ throw fetchError;
148
+ }
149
+ }
150
+
151
+ if (collections && collections.length > 0) {
152
+ setAvailableCollections(collections);
153
+ setIsLoadingCollections(false);
154
+ return collections;
155
+ } else {
156
+ setConnectionError('No collections found in the database.');
157
+ setIsLoadingCollections(false);
158
+ return [];
159
+ }
160
+ } catch (error: any) {
161
+ setConnectionError(error.message || 'Failed to fetch collections.');
162
+ setIsLoadingCollections(false);
163
+ console.error('Fetch collections error:', error);
164
+ return [];
165
+ }
166
+ };
167
+
168
+ const handleFetchColumns = async (collectionName?: string): Promise<string[]> => {
169
+ setIsLoadingColumns(true);
170
+ setConnectionError(null);
171
+
172
+ try {
173
+ const normalizedConnection = normalizeConnection(connection, dbType);
132
174
  const backendRequest = transformConnectionForBackend(connection, dbType);
133
175
 
134
176
  let columns: string[];
135
177
 
136
178
  if (onFetchColumns) {
137
- // Use provided handler
138
- columns = await onFetchColumns(normalizedConnection);
179
+ columns = await onFetchColumns(normalizedConnection, collectionName || selectedCollection);
139
180
  } else {
140
- // Default: Try to call backend API
141
181
  try {
142
182
  const response = await fetch('/api/database/columns', {
143
183
  method: 'POST',
144
- headers: {
145
- 'Content-Type': 'application/json',
146
- },
147
- body: JSON.stringify(backendRequest), // Send transformed format
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({
186
+ ...backendRequest,
187
+ collection: collectionName || selectedCollection,
188
+ }),
148
189
  });
149
190
 
150
191
  if (!response.ok) {
151
192
  const errorData = await response.json().catch(() => ({}));
152
- throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
193
+ throw new Error(errorData.message || `HTTP ${response.status}`);
153
194
  }
154
195
 
155
196
  const data = await response.json();
156
197
  columns = data.columns || data.columnNames || [];
157
198
  } catch (fetchError: any) {
158
- // If API endpoint doesn't exist, show helpful error
159
199
  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
- );
200
+ throw new Error('Backend API endpoint not found. Please implement POST /api/database/columns endpoint.');
163
201
  }
164
202
  throw fetchError;
165
203
  }
@@ -170,57 +208,49 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
170
208
  setIsLoadingColumns(false);
171
209
  return columns;
172
210
  } else {
173
- setConnectionError('No columns found in the database. Please check your database connection and table selection.');
211
+ setConnectionError('No columns found in the selected collection.');
174
212
  setIsLoadingColumns(false);
175
213
  return [];
176
214
  }
177
215
  } catch (error: any) {
178
- const errorMessage = error.message || 'Failed to fetch columns. Please try again.';
179
- setConnectionError(errorMessage);
216
+ setConnectionError(error.message || 'Failed to fetch columns.');
180
217
  setIsLoadingColumns(false);
181
218
  console.error('Fetch columns error:', error);
182
219
  return [];
183
220
  }
184
221
  };
185
222
 
186
- const handleConnectAndNext = async () => {
187
- // Validate connection fields
223
+ const handleConnect = async () => {
188
224
  if (dbType === 'mongodb') {
189
225
  if (!connection.connectionString && (!connection.host || !connection.database)) {
190
226
  setConnectionError('Please provide connection string or host and database');
191
- setConnectionSuccess(false);
192
227
  return;
193
228
  }
194
229
  } else {
195
230
  if (!connection.host || !connection.database || !connection.username || !connection.password) {
196
231
  setConnectionError('Please fill in all required fields');
197
- setConnectionSuccess(false);
198
232
  return;
199
233
  }
200
234
  }
201
235
 
202
- // Clear previous errors
203
236
  setConnectionError(null);
204
237
  setConnectionSuccess(false);
205
238
 
206
- // Test connection first
207
239
  const connectionSuccess = await handleTestConnection();
208
240
 
209
- if (!connectionSuccess) {
210
- // Connection failed, don't proceed
211
- return;
241
+ if (connectionSuccess) {
242
+ const collections = await handleFetchCollections();
243
+ if (collections && collections.length > 0) {
244
+ setCurrentStep('collection');
245
+ }
212
246
  }
247
+ };
213
248
 
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
249
+ const handleCollectionSelect = async (collection: string) => {
250
+ setSelectedCollection(collection);
251
+ const columns = await handleFetchColumns(collection);
252
+ if (columns && columns.length > 0) {
219
253
  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
254
  }
225
255
  };
226
256
 
@@ -248,7 +278,6 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
248
278
  onSave(config);
249
279
  }
250
280
 
251
- // Close modal and reset
252
281
  setIsModalOpen(false);
253
282
  setCurrentStep('connection');
254
283
  setConnectionError(null);
@@ -259,6 +288,8 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
259
288
  setCurrentStep('connection');
260
289
  setConnectionError(null);
261
290
  setConnectionSuccess(false);
291
+ setAvailableCollections([]);
292
+ setSelectedCollection('');
262
293
  setAvailableColumns([]);
263
294
  setColumnSelection({
264
295
  embeddingColumns: [],
@@ -269,115 +300,65 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
269
300
 
270
301
  return (
271
302
  <>
272
- {/* Sidebar Button/Item - This can be integrated into admin sidebar */}
273
303
  <button
274
304
  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"
305
+ className="w-full flex items-center gap-3 px-4 py-2.5 text-left hover:bg-gray-50 rounded-lg transition-colors text-sm text-gray-700"
276
306
  >
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
- />
307
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
308
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
290
309
  </svg>
291
- <span className="text-sm font-medium text-gray-700">Database Setup</span>
310
+ <span className="font-medium">Database Setup</span>
292
311
  </button>
293
312
 
294
- {/* Modal */}
295
313
  {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'}
314
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
315
+ <div className="bg-white rounded-xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-hidden flex flex-col">
316
+ {/* Header */}
317
+ <div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between bg-gray-50/50">
318
+ <h2 className="text-lg font-semibold text-gray-900">
319
+ {currentStep === 'connection' && 'Database Connection'}
320
+ {currentStep === 'collection' && 'Select Collection'}
321
+ {currentStep === 'columns' && 'Configure Columns'}
302
322
  </h2>
303
323
  <button
304
324
  onClick={handleClose}
305
- className="text-gray-400 hover:text-gray-600 transition-colors"
325
+ className="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100"
306
326
  >
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
- />
327
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
328
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
320
329
  </svg>
321
330
  </button>
322
331
  </div>
323
332
 
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 */}
333
+ {/* Content */}
334
+ <div className="flex-1 overflow-y-auto px-6 py-6">
335
+ {currentStep === 'connection' && (
336
+ <div className="space-y-5">
337
+ {/* Database Type */}
329
338
  <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">
339
+ <label className="block text-sm font-medium text-gray-700 mb-2">Database Type</label>
340
+ <div className="flex gap-3">
334
341
  <button
335
342
  onClick={() => handleDbTypeChange('mongodb')}
336
343
  className={clsx(
337
- 'p-4 border-2 rounded-lg transition-all text-left',
344
+ 'flex-1 px-4 py-3 rounded-lg border-2 transition-all text-sm font-medium',
338
345
  dbType === 'mongodb'
339
- ? 'border-blue-500 bg-blue-50'
340
- : 'border-gray-200 hover:border-gray-300'
346
+ ? 'border-blue-500 bg-blue-50 text-blue-700'
347
+ : 'border-gray-200 text-gray-700 hover:border-gray-300'
341
348
  )}
342
349
  >
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>
350
+ MongoDB
357
351
  </button>
358
352
  <button
359
353
  onClick={() => handleDbTypeChange('postgres')}
360
354
  className={clsx(
361
- 'p-4 border-2 rounded-lg transition-all text-left',
355
+ 'flex-1 px-4 py-3 rounded-lg border-2 transition-all text-sm font-medium',
362
356
  dbType === 'postgres'
363
- ? 'border-blue-500 bg-blue-50'
364
- : 'border-gray-200 hover:border-gray-300'
357
+ ? 'border-blue-500 bg-blue-50 text-blue-700'
358
+ : 'border-gray-200 text-gray-700 hover:border-gray-300'
365
359
  )}
366
360
  >
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>
361
+ PostgreSQL
381
362
  </button>
382
363
  </div>
383
364
  </div>
@@ -386,317 +367,233 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
386
367
  {dbType === 'mongodb' ? (
387
368
  <div className="space-y-4">
388
369
  <div>
389
- <label className="block text-sm font-medium text-gray-700 mb-2">
390
- Connection String (Recommended)
391
- </label>
370
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Connection String</label>
392
371
  <input
393
372
  type="text"
394
373
  value={connection.connectionString || ''}
395
374
  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"
375
+ placeholder="mongodb://host:port/database"
376
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
398
377
  />
399
- <p className="mt-1 text-xs text-gray-500">
400
- Or fill individual fields below
401
- </p>
402
378
  </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>
379
+ <div className="text-xs text-gray-500 text-center">or</div>
380
+ <div className="grid grid-cols-2 gap-3">
381
+ <div>
382
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Host</label>
434
383
  <input
435
384
  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"
385
+ value={connection.host}
386
+ onChange={(e) => handleConnectionChange('host', e.target.value)}
387
+ placeholder="localhost"
388
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
440
389
  />
441
390
  </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>
391
+ <div>
392
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Port</label>
393
+ <input
394
+ type="number"
395
+ value={connection.port}
396
+ onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 27017)}
397
+ placeholder="27017"
398
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
399
+ />
452
400
  </div>
453
401
  </div>
402
+ <div>
403
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Database</label>
404
+ <input
405
+ type="text"
406
+ value={connection.database}
407
+ onChange={(e) => handleConnectionChange('database', e.target.value)}
408
+ placeholder="database_name"
409
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
410
+ />
411
+ </div>
454
412
  </div>
455
413
  ) : (
456
414
  <div className="space-y-4">
457
- <div className="grid grid-cols-2 gap-4">
415
+ <div className="grid grid-cols-2 gap-3">
458
416
  <div>
459
- <label className="block text-sm font-medium text-gray-700 mb-2">
460
- Host *
461
- </label>
417
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Host</label>
462
418
  <input
463
419
  type="text"
464
420
  value={connection.host}
465
421
  onChange={(e) => handleConnectionChange('host', e.target.value)}
466
422
  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"
423
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
468
424
  />
469
425
  </div>
470
426
  <div>
471
- <label className="block text-sm font-medium text-gray-700 mb-2">
472
- Port *
473
- </label>
427
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Port</label>
474
428
  <input
475
429
  type="number"
476
430
  value={connection.port}
477
431
  onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 5432)}
478
432
  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"
433
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
480
434
  />
481
435
  </div>
482
436
  </div>
483
437
  <div>
484
- <label className="block text-sm font-medium text-gray-700 mb-2">
485
- Database Name *
486
- </label>
438
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Database</label>
487
439
  <input
488
440
  type="text"
489
441
  value={connection.database}
490
442
  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"
443
+ placeholder="database_name"
444
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
493
445
  />
494
446
  </div>
495
- <div className="grid grid-cols-2 gap-4">
447
+ <div className="grid grid-cols-2 gap-3">
496
448
  <div>
497
- <label className="block text-sm font-medium text-gray-700 mb-2">
498
- Username *
499
- </label>
449
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Username</label>
500
450
  <input
501
451
  type="text"
502
452
  value={connection.username || ''}
503
453
  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"
454
+ placeholder="username"
455
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
506
456
  />
507
457
  </div>
508
458
  <div>
509
- <label className="block text-sm font-medium text-gray-700 mb-2">
510
- Password *
511
- </label>
459
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Password</label>
512
460
  <input
513
461
  type="password"
514
462
  value={connection.password || ''}
515
463
  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"
464
+ placeholder="••••••"
465
+ className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
518
466
  />
519
467
  </div>
520
468
  </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
469
  </div>
533
470
  )}
534
471
 
535
472
  {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>
473
+ <div className="bg-red-50 border border-red-200 rounded-lg p-3">
474
+ <p className="text-sm text-red-700">{connectionError}</p>
538
475
  </div>
539
476
  )}
540
477
  {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>
478
+ <div className="bg-green-50 border border-green-200 rounded-lg p-3">
479
+ <p className="text-sm text-green-700">✓ Connection successful</p>
543
480
  </div>
544
481
  )}
545
482
  </div>
546
- ) : (
547
- <div className="space-y-6">
483
+ )}
484
+
485
+ {currentStep === 'collection' && (
486
+ <div className="space-y-4">
487
+ <div>
488
+ <p className="text-sm text-gray-600 mb-4">Select a collection to configure for embeddings:</p>
489
+ {isLoadingCollections ? (
490
+ <div className="flex items-center justify-center py-8">
491
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
492
+ </div>
493
+ ) : (
494
+ <select
495
+ value={selectedCollection}
496
+ onChange={(e) => handleCollectionSelect(e.target.value)}
497
+ className="w-full px-3 py-2.5 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white"
498
+ >
499
+ <option value="">Select a collection...</option>
500
+ {availableCollections.map((collection) => (
501
+ <option key={collection} value={collection}>
502
+ {collection}
503
+ </option>
504
+ ))}
505
+ </select>
506
+ )}
507
+ </div>
508
+ {connectionError && (
509
+ <div className="bg-red-50 border border-red-200 rounded-lg p-3">
510
+ <p className="text-sm text-red-700">{connectionError}</p>
511
+ </div>
512
+ )}
513
+ </div>
514
+ )}
515
+
516
+ {currentStep === 'columns' && (
517
+ <div className="space-y-5">
518
+ <div>
519
+ <p className="text-sm text-gray-600 mb-1">Collection: <span className="font-medium text-gray-900">{selectedCollection}</span></p>
520
+ <p className="text-xs text-gray-500">Select columns for embeddings</p>
521
+ </div>
522
+
548
523
  {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>
524
+ <div className="flex items-center justify-center py-8">
525
+ <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
552
526
  </div>
553
527
  ) : 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>
528
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
529
+ <p className="text-sm text-yellow-700">No columns found.</p>
556
530
  </div>
557
531
  ) : (
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
- )}
532
+ <div>
533
+ <label className="block text-sm font-medium text-gray-700 mb-3">Embedding Columns</label>
534
+ <div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-3 bg-gray-50/50">
535
+ {availableColumns.map((column) => (
536
+ <label
537
+ key={column}
538
+ className="flex items-center gap-2 cursor-pointer hover:bg-white p-2 rounded transition-colors"
539
+ >
540
+ <input
541
+ type="checkbox"
542
+ checked={columnSelection.embeddingColumns.includes(column)}
543
+ onChange={() => handleColumnToggle(column, 'embeddingColumns')}
544
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
545
+ />
546
+ <span className="text-sm text-gray-700">{column}</span>
547
+ </label>
548
+ ))}
632
549
  </div>
550
+ {columnSelection.embeddingColumns.length > 0 && (
551
+ <p className="text-xs text-green-600 mt-2">
552
+ {columnSelection.embeddingColumns.length} column{columnSelection.embeddingColumns.length !== 1 ? 's' : ''} selected
553
+ </p>
554
+ )}
555
+ </div>
556
+ )}
633
557
 
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
- </>
558
+ {connectionError && (
559
+ <div className="bg-red-50 border border-red-200 rounded-lg p-3">
560
+ <p className="text-sm text-red-700">{connectionError}</p>
561
+ </div>
667
562
  )}
668
563
  </div>
669
564
  )}
670
565
  </div>
671
566
 
672
- {/* Modal Footer */}
673
- <div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
567
+ {/* Footer */}
568
+ <div className="px-6 py-4 border-t border-gray-100 bg-gray-50/50 flex items-center justify-between">
674
569
  <button
675
- onClick={currentStep === 'columns' ? () => setCurrentStep('connection') : handleClose}
570
+ onClick={currentStep === 'connection' ? handleClose : () => {
571
+ if (currentStep === 'columns') {
572
+ setCurrentStep('collection');
573
+ setAvailableColumns([]);
574
+ } else {
575
+ setCurrentStep('connection');
576
+ }
577
+ }}
676
578
  className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
677
579
  >
678
- {currentStep === 'columns' ? 'Back' : 'Cancel'}
580
+ {currentStep === 'connection' ? 'Cancel' : 'Back'}
679
581
  </button>
680
582
  <div className="flex gap-3">
681
- {currentStep === 'connection' ? (
583
+ {currentStep === 'connection' && (
682
584
  <button
683
- onClick={handleConnectAndNext}
684
- disabled={isConnecting || isLoadingColumns}
585
+ onClick={handleConnect}
586
+ disabled={isConnecting || isLoadingCollections}
685
587
  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
588
  >
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'}
589
+ {isConnecting && <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>}
590
+ {isConnecting ? 'Connecting...' : 'Connect'}
691
591
  </button>
692
- ) : (
592
+ )}
593
+ {currentStep === 'columns' && (
693
594
  <button
694
595
  onClick={handleSave}
695
- disabled={
696
- columnSelection.embeddingColumns.length === 0 &&
697
- columnSelection.llmColumns.length === 0 &&
698
- columnSelection.chromaColumns.length === 0
699
- }
596
+ disabled={columnSelection.embeddingColumns.length === 0}
700
597
  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
598
  >
702
599
  Save Configuration
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nextjs-chatbot-ui",
3
3
 
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "description": "A configurable chatbot UI component for Next.js with Tailwind CSS",
6
6
  "main": "./index.tsx",
7
7
  "module": "./index.tsx",
package/types/admin.ts CHANGED
@@ -1,35 +1,36 @@
1
- export type DatabaseType = 'mongodb' | 'postgres';
2
-
3
- export interface DatabaseConnection {
4
- type: DatabaseType;
5
- host: string;
6
- port: number;
7
- database: string;
8
- username?: string;
9
- password?: string;
10
- connectionString?: string; // For MongoDB connection string
11
- ssl?: boolean;
12
- }
13
-
14
- export interface ColumnSelection {
15
- embeddingColumns: string[];
16
- llmColumns: string[];
17
- chromaColumns: string[];
18
- }
19
-
20
- export interface DatabaseConfig {
21
- connection: DatabaseConnection;
22
- columnSelection: ColumnSelection;
23
- }
24
-
25
- export interface AdminSetupProps {
26
- onSave?: (config: DatabaseConfig) => void;
27
- onTestConnection?: (connection: DatabaseConnection) => Promise<boolean>;
28
- onFetchColumns?: (connection: DatabaseConnection) => Promise<string[]>;
29
- }
30
-
31
- // Backend request format
32
- export interface BackendConnectionRequest {
33
- databaseType: 'mongodb' | 'postgresql';
34
- connection: Omit<DatabaseConnection, 'type'>;
1
+ export type DatabaseType = 'mongodb' | 'postgres';
2
+
3
+ export interface DatabaseConnection {
4
+ type: DatabaseType;
5
+ host: string;
6
+ port: number;
7
+ database: string;
8
+ username?: string;
9
+ password?: string;
10
+ connectionString?: string; // For MongoDB connection string
11
+ ssl?: boolean;
12
+ }
13
+
14
+ export interface ColumnSelection {
15
+ embeddingColumns: string[];
16
+ llmColumns: string[];
17
+ chromaColumns: string[];
18
+ }
19
+
20
+ export interface DatabaseConfig {
21
+ connection: DatabaseConnection;
22
+ columnSelection: ColumnSelection;
23
+ }
24
+
25
+ export interface AdminSetupProps {
26
+ onSave?: (config: DatabaseConfig) => void;
27
+ onTestConnection?: (connection: DatabaseConnection) => Promise<boolean>;
28
+ onFetchCollections?: (connection: DatabaseConnection) => Promise<string[]>; // For MongoDB collections or PostgreSQL tables
29
+ onFetchColumns?: (connection: DatabaseConnection, collection?: string) => Promise<string[]>; // collection/table name optional
30
+ }
31
+
32
+ // Backend request format
33
+ export interface BackendConnectionRequest {
34
+ databaseType: 'mongodb' | 'postgresql';
35
+ connection: Omit<DatabaseConnection, 'type'>;
35
36
  }
@@ -1,40 +1,40 @@
1
- import { DatabaseConnection, DatabaseType } from '../types/admin';
2
-
3
- export interface BackendConnectionRequest {
4
- databaseType: 'mongodb' | 'postgresql';
5
- connection: Omit<DatabaseConnection, 'type'>;
6
- }
7
-
8
- /**
9
- * Normalizes connection data with defaults
10
- */
11
- export function normalizeConnection(
12
- connection: DatabaseConnection,
13
- dbType: DatabaseType
14
- ): DatabaseConnection {
15
- return {
16
- ...connection,
17
- port: connection.port || (dbType === 'mongodb' ? 27017 : 5432),
18
- host: connection.host || 'localhost',
19
- database: connection.database || '',
20
- };
21
- }
22
-
23
- /**
24
- * Transforms DatabaseConnection to backend format
25
- */
26
- export function transformConnectionForBackend(
27
- connection: DatabaseConnection,
28
- dbType: DatabaseType
29
- ): BackendConnectionRequest {
30
- const normalized = normalizeConnection(connection, dbType);
31
-
32
- const backendConnection = { ...normalized };
33
- // Remove type from connection object
34
- delete (backendConnection as any).type;
35
-
36
- return {
37
- databaseType: dbType === 'postgres' ? 'postgresql' : 'mongodb',
38
- connection: backendConnection,
39
- };
40
- }
1
+ import { DatabaseConnection, DatabaseType } from '../types/admin';
2
+
3
+ export interface BackendConnectionRequest {
4
+ databaseType: 'mongodb' | 'postgresql';
5
+ connection: Omit<DatabaseConnection, 'type'>;
6
+ }
7
+
8
+ /**
9
+ * Normalizes connection data with defaults
10
+ */
11
+ export function normalizeConnection(
12
+ connection: DatabaseConnection,
13
+ dbType: DatabaseType
14
+ ): DatabaseConnection {
15
+ return {
16
+ ...connection,
17
+ port: connection.port || (dbType === 'mongodb' ? 27017 : 5432),
18
+ host: connection.host || 'localhost',
19
+ database: connection.database || '',
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Transforms DatabaseConnection to backend format
25
+ */
26
+ export function transformConnectionForBackend(
27
+ connection: DatabaseConnection,
28
+ dbType: DatabaseType
29
+ ): BackendConnectionRequest {
30
+ const normalized = normalizeConnection(connection, dbType);
31
+
32
+ const backendConnection = { ...normalized };
33
+ // Remove type from connection object
34
+ delete (backendConnection as any).type;
35
+
36
+ return {
37
+ databaseType: dbType === 'postgres' ? 'postgresql' : 'mongodb',
38
+ connection: backendConnection,
39
+ };
40
+ }