nextjs-chatbot-ui 1.1.2 → 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.
@@ -2,19 +2,24 @@
2
2
 
3
3
  import React, { useState } from 'react';
4
4
  import { DatabaseType, DatabaseConnection, ColumnSelection, DatabaseConfig, AdminSetupProps } from '../types/admin';
5
+ import { normalizeConnection, transformConnectionForBackend } from '../utils/connectionHelpers';
5
6
  import clsx from 'clsx';
6
7
 
7
8
  const AdminSetup: React.FC<AdminSetupProps> = ({
8
9
  onSave,
9
10
  onTestConnection,
11
+ onFetchCollections,
10
12
  onFetchColumns,
11
13
  }) => {
12
14
  const [isModalOpen, setIsModalOpen] = useState(false);
13
- const [currentStep, setCurrentStep] = useState<'connection' | 'columns'>('connection');
15
+ const [currentStep, setCurrentStep] = useState<'connection' | 'collection' | 'columns'>('connection');
14
16
  const [isConnecting, setIsConnecting] = useState(false);
15
17
  const [connectionError, setConnectionError] = useState<string | null>(null);
16
18
  const [connectionSuccess, setConnectionSuccess] = useState(false);
19
+ const [availableCollections, setAvailableCollections] = useState<string[]>([]);
20
+ const [selectedCollection, setSelectedCollection] = useState<string>('');
17
21
  const [availableColumns, setAvailableColumns] = useState<string[]>([]);
22
+ const [isLoadingCollections, setIsLoadingCollections] = useState(false);
18
23
  const [isLoadingColumns, setIsLoadingColumns] = useState(false);
19
24
 
20
25
  const [dbType, setDbType] = useState<DatabaseType>('mongodb');
@@ -59,35 +64,31 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
59
64
  setConnectionSuccess(false);
60
65
 
61
66
  try {
67
+ const normalizedConnection = normalizeConnection(connection, dbType);
68
+ const backendRequest = transformConnectionForBackend(connection, dbType);
69
+
62
70
  let isValid: boolean;
63
71
 
64
72
  if (onTestConnection) {
65
- // Use provided handler
66
- isValid = await onTestConnection(connection);
73
+ isValid = await onTestConnection(normalizedConnection);
67
74
  } else {
68
- // Default: Try to call backend API
69
75
  try {
70
76
  const response = await fetch('/api/database/test', {
71
77
  method: 'POST',
72
- headers: {
73
- 'Content-Type': 'application/json',
74
- },
75
- body: JSON.stringify(connection),
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify(backendRequest),
76
80
  });
77
81
 
78
82
  if (!response.ok) {
79
83
  const errorData = await response.json().catch(() => ({}));
80
- throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
84
+ throw new Error(errorData.message || `HTTP ${response.status}`);
81
85
  }
82
86
 
83
87
  const data = await response.json();
84
88
  isValid = data.success === true || response.ok;
85
89
  } catch (fetchError: any) {
86
- // If API endpoint doesn't exist, show helpful error
87
90
  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
+ throw new Error('Backend API endpoint not found. Please implement POST /api/database/test endpoint.');
91
92
  }
92
93
  throw fetchError;
93
94
  }
@@ -98,13 +99,12 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
98
99
  setConnectionSuccess(true);
99
100
  return true;
100
101
  } else {
101
- setConnectionError('Connection failed. Please check your credentials and try again.');
102
+ setConnectionError('Connection failed. Please check your credentials.');
102
103
  setConnectionSuccess(false);
103
104
  return false;
104
105
  }
105
106
  } catch (error: any) {
106
- const errorMessage = error.message || 'Connection failed. Please try again.';
107
- setConnectionError(errorMessage);
107
+ setConnectionError(error.message || 'Connection failed. Please try again.');
108
108
  setConnectionSuccess(false);
109
109
  console.error('Database connection error:', error);
110
110
  return false;
@@ -113,40 +113,91 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
113
113
  }
114
114
  };
115
115
 
116
- const handleFetchColumns = async (): Promise<string[]> => {
116
+ const handleFetchCollections = async (): Promise<string[]> => {
117
+ setIsLoadingCollections(true);
118
+ setConnectionError(null);
119
+
120
+ try {
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
+ }
140
+
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[]> => {
117
169
  setIsLoadingColumns(true);
118
170
  setConnectionError(null);
119
171
 
120
172
  try {
173
+ const normalizedConnection = normalizeConnection(connection, dbType);
174
+ const backendRequest = transformConnectionForBackend(connection, dbType);
175
+
121
176
  let columns: string[];
122
177
 
123
178
  if (onFetchColumns) {
124
- // Use provided handler
125
- columns = await onFetchColumns(connection);
179
+ columns = await onFetchColumns(normalizedConnection, collectionName || selectedCollection);
126
180
  } else {
127
- // Default: Try to call backend API
128
181
  try {
129
182
  const response = await fetch('/api/database/columns', {
130
183
  method: 'POST',
131
- headers: {
132
- 'Content-Type': 'application/json',
133
- },
134
- body: JSON.stringify(connection),
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({
186
+ ...backendRequest,
187
+ collection: collectionName || selectedCollection,
188
+ }),
135
189
  });
136
190
 
137
191
  if (!response.ok) {
138
192
  const errorData = await response.json().catch(() => ({}));
139
- throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
193
+ throw new Error(errorData.message || `HTTP ${response.status}`);
140
194
  }
141
195
 
142
196
  const data = await response.json();
143
197
  columns = data.columns || data.columnNames || [];
144
198
  } catch (fetchError: any) {
145
- // If API endpoint doesn't exist, show helpful error
146
199
  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
- );
200
+ throw new Error('Backend API endpoint not found. Please implement POST /api/database/columns endpoint.');
150
201
  }
151
202
  throw fetchError;
152
203
  }
@@ -157,57 +208,49 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
157
208
  setIsLoadingColumns(false);
158
209
  return columns;
159
210
  } else {
160
- setConnectionError('No columns found in the database. Please check your database connection and table selection.');
211
+ setConnectionError('No columns found in the selected collection.');
161
212
  setIsLoadingColumns(false);
162
213
  return [];
163
214
  }
164
215
  } catch (error: any) {
165
- const errorMessage = error.message || 'Failed to fetch columns. Please try again.';
166
- setConnectionError(errorMessage);
216
+ setConnectionError(error.message || 'Failed to fetch columns.');
167
217
  setIsLoadingColumns(false);
168
218
  console.error('Fetch columns error:', error);
169
219
  return [];
170
220
  }
171
221
  };
172
222
 
173
- const handleConnectAndNext = async () => {
174
- // Validate connection fields
223
+ const handleConnect = async () => {
175
224
  if (dbType === 'mongodb') {
176
225
  if (!connection.connectionString && (!connection.host || !connection.database)) {
177
226
  setConnectionError('Please provide connection string or host and database');
178
- setConnectionSuccess(false);
179
227
  return;
180
228
  }
181
229
  } else {
182
230
  if (!connection.host || !connection.database || !connection.username || !connection.password) {
183
231
  setConnectionError('Please fill in all required fields');
184
- setConnectionSuccess(false);
185
232
  return;
186
233
  }
187
234
  }
188
235
 
189
- // Clear previous errors
190
236
  setConnectionError(null);
191
237
  setConnectionSuccess(false);
192
238
 
193
- // Test connection first
194
239
  const connectionSuccess = await handleTestConnection();
195
240
 
196
- if (!connectionSuccess) {
197
- // Connection failed, don't proceed
198
- return;
241
+ if (connectionSuccess) {
242
+ const collections = await handleFetchCollections();
243
+ if (collections && collections.length > 0) {
244
+ setCurrentStep('collection');
245
+ }
199
246
  }
247
+ };
200
248
 
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
249
+ const handleCollectionSelect = async (collection: string) => {
250
+ setSelectedCollection(collection);
251
+ const columns = await handleFetchColumns(collection);
252
+ if (columns && columns.length > 0) {
206
253
  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
254
  }
212
255
  };
213
256
 
@@ -235,7 +278,6 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
235
278
  onSave(config);
236
279
  }
237
280
 
238
- // Close modal and reset
239
281
  setIsModalOpen(false);
240
282
  setCurrentStep('connection');
241
283
  setConnectionError(null);
@@ -246,6 +288,8 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
246
288
  setCurrentStep('connection');
247
289
  setConnectionError(null);
248
290
  setConnectionSuccess(false);
291
+ setAvailableCollections([]);
292
+ setSelectedCollection('');
249
293
  setAvailableColumns([]);
250
294
  setColumnSelection({
251
295
  embeddingColumns: [],
@@ -256,115 +300,65 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
256
300
 
257
301
  return (
258
302
  <>
259
- {/* Sidebar Button/Item - This can be integrated into admin sidebar */}
260
303
  <button
261
304
  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"
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"
263
306
  >
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
- />
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" />
277
309
  </svg>
278
- <span className="text-sm font-medium text-gray-700">Database Setup</span>
310
+ <span className="font-medium">Database Setup</span>
279
311
  </button>
280
312
 
281
- {/* Modal */}
282
313
  {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'}
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'}
289
322
  </h2>
290
323
  <button
291
324
  onClick={handleClose}
292
- 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"
293
326
  >
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
- />
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" />
307
329
  </svg>
308
330
  </button>
309
331
  </div>
310
332
 
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 */}
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 */}
316
338
  <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">
339
+ <label className="block text-sm font-medium text-gray-700 mb-2">Database Type</label>
340
+ <div className="flex gap-3">
321
341
  <button
322
342
  onClick={() => handleDbTypeChange('mongodb')}
323
343
  className={clsx(
324
- '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',
325
345
  dbType === 'mongodb'
326
- ? 'border-blue-500 bg-blue-50'
327
- : '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'
328
348
  )}
329
349
  >
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>
350
+ MongoDB
344
351
  </button>
345
352
  <button
346
353
  onClick={() => handleDbTypeChange('postgres')}
347
354
  className={clsx(
348
- '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',
349
356
  dbType === 'postgres'
350
- ? 'border-blue-500 bg-blue-50'
351
- : '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'
352
359
  )}
353
360
  >
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>
361
+ PostgreSQL
368
362
  </button>
369
363
  </div>
370
364
  </div>
@@ -373,317 +367,233 @@ const AdminSetup: React.FC<AdminSetupProps> = ({
373
367
  {dbType === 'mongodb' ? (
374
368
  <div className="space-y-4">
375
369
  <div>
376
- <label className="block text-sm font-medium text-gray-700 mb-2">
377
- Connection String (Recommended)
378
- </label>
370
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Connection String</label>
379
371
  <input
380
372
  type="text"
381
373
  value={connection.connectionString || ''}
382
374
  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"
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"
385
377
  />
386
- <p className="mt-1 text-xs text-gray-500">
387
- Or fill individual fields below
388
- </p>
389
378
  </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>
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>
421
383
  <input
422
384
  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"
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"
427
389
  />
428
390
  </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>
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
+ />
439
400
  </div>
440
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>
441
412
  </div>
442
413
  ) : (
443
414
  <div className="space-y-4">
444
- <div className="grid grid-cols-2 gap-4">
415
+ <div className="grid grid-cols-2 gap-3">
445
416
  <div>
446
- <label className="block text-sm font-medium text-gray-700 mb-2">
447
- Host *
448
- </label>
417
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Host</label>
449
418
  <input
450
419
  type="text"
451
420
  value={connection.host}
452
421
  onChange={(e) => handleConnectionChange('host', e.target.value)}
453
422
  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"
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"
455
424
  />
456
425
  </div>
457
426
  <div>
458
- <label className="block text-sm font-medium text-gray-700 mb-2">
459
- Port *
460
- </label>
427
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Port</label>
461
428
  <input
462
429
  type="number"
463
430
  value={connection.port}
464
431
  onChange={(e) => handleConnectionChange('port', parseInt(e.target.value) || 5432)}
465
432
  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"
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"
467
434
  />
468
435
  </div>
469
436
  </div>
470
437
  <div>
471
- <label className="block text-sm font-medium text-gray-700 mb-2">
472
- Database Name *
473
- </label>
438
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Database</label>
474
439
  <input
475
440
  type="text"
476
441
  value={connection.database}
477
442
  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"
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"
480
445
  />
481
446
  </div>
482
- <div className="grid grid-cols-2 gap-4">
447
+ <div className="grid grid-cols-2 gap-3">
483
448
  <div>
484
- <label className="block text-sm font-medium text-gray-700 mb-2">
485
- Username *
486
- </label>
449
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Username</label>
487
450
  <input
488
451
  type="text"
489
452
  value={connection.username || ''}
490
453
  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"
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"
493
456
  />
494
457
  </div>
495
458
  <div>
496
- <label className="block text-sm font-medium text-gray-700 mb-2">
497
- Password *
498
- </label>
459
+ <label className="block text-sm font-medium text-gray-700 mb-1.5">Password</label>
499
460
  <input
500
461
  type="password"
501
462
  value={connection.password || ''}
502
463
  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"
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"
505
466
  />
506
467
  </div>
507
468
  </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
469
  </div>
520
470
  )}
521
471
 
522
472
  {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>
473
+ <div className="bg-red-50 border border-red-200 rounded-lg p-3">
474
+ <p className="text-sm text-red-700">{connectionError}</p>
525
475
  </div>
526
476
  )}
527
477
  {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>
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>
530
480
  </div>
531
481
  )}
532
482
  </div>
533
- ) : (
534
- <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
+
535
523
  {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>
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>
539
526
  </div>
540
527
  ) : 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>
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>
543
530
  </div>
544
531
  ) : (
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
- )}
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
+ ))}
619
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
+ )}
620
557
 
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
- </>
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>
654
562
  )}
655
563
  </div>
656
564
  )}
657
565
  </div>
658
566
 
659
- {/* Modal Footer */}
660
- <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">
661
569
  <button
662
- 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
+ }}
663
578
  className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
664
579
  >
665
- {currentStep === 'columns' ? 'Back' : 'Cancel'}
580
+ {currentStep === 'connection' ? 'Cancel' : 'Back'}
666
581
  </button>
667
582
  <div className="flex gap-3">
668
- {currentStep === 'connection' ? (
583
+ {currentStep === 'connection' && (
669
584
  <button
670
- onClick={handleConnectAndNext}
671
- disabled={isConnecting || isLoadingColumns}
585
+ onClick={handleConnect}
586
+ disabled={isConnecting || isLoadingCollections}
672
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"
673
588
  >
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'}
589
+ {isConnecting && <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>}
590
+ {isConnecting ? 'Connecting...' : 'Connect'}
678
591
  </button>
679
- ) : (
592
+ )}
593
+ {currentStep === 'columns' && (
680
594
  <button
681
595
  onClick={handleSave}
682
- disabled={
683
- columnSelection.embeddingColumns.length === 0 &&
684
- columnSelection.llmColumns.length === 0 &&
685
- columnSelection.chromaColumns.length === 0
686
- }
596
+ disabled={columnSelection.embeddingColumns.length === 0}
687
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"
688
598
  >
689
599
  Save Configuration
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nextjs-chatbot-ui",
3
3
 
4
- "version": "1.1.2",
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",
@@ -29,6 +29,7 @@
29
29
  "files": [
30
30
  "components",
31
31
  "types",
32
+ "utils",
32
33
  "index.tsx",
33
34
  "README.md"
34
35
  ],
package/types/admin.ts CHANGED
@@ -1,29 +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
- }
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'>;
36
+ }
@@ -0,0 +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
+ }