ishaq101 commited on
Commit
d738108
Β·
1 Parent(s): 174544e

[KM-476] [DED][FE] Knowledge (Document and DB) Flow Page

Browse files

ticket: https://bukittechnology.atlassian.net/browse/KM-476
- Implement connectDatabase, getDatabaseClientTypes, getDatabaseClients,
deleteDatabaseClient, and ingestDatabaseClient in api.ts with correct
request/response shapes per API contract
- Add DbTypeField, DbTypeInfo, DatabaseClient, and IngestResponse types;
remove placeholder PostgresCredentials and ConnectDatabaseResponse
- Replace hard-coded DB_TYPES with dynamic fetch from GET /dbtypes endpoint
- Add "Connection Name" required field to credentials form
- Render form fields dynamically from API schema (supports all db_type variants:
postgres, mysql, supabase, sqlserver, bigquery, snowflake)
- Refresh connection list from GET /database-clients/{user_id} after connect
to confirm persisted data from backend
- Add "Databases" section in main view listing connected clients with
Ingest and Delete actions per connection

src/app/App.tsx CHANGED
@@ -1,6 +1,12 @@
1
  import { RouterProvider } from "react-router";
2
  import { router } from "./routes";
 
3
 
4
  export default function App() {
5
- return <RouterProvider router={router} />;
 
 
 
 
 
6
  }
 
1
  import { RouterProvider } from "react-router";
2
  import { router } from "./routes";
3
+ import { Toaster } from "./components/ui/sonner";
4
 
5
  export default function App() {
6
+ return (
7
+ <>
8
+ <RouterProvider router={router} />
9
+ <Toaster richColors position="top-right" />
10
+ </>
11
+ );
12
  }
src/app/components/KnowledgeManagement.tsx CHANGED
@@ -7,14 +7,25 @@ import {
7
  Loader2,
8
  Database,
9
  X,
 
 
10
  } from "lucide-react";
 
11
  import {
12
  getDocuments,
13
  uploadDocument,
14
  processDocument,
15
  deleteDocument,
 
 
 
 
 
16
  type ApiDocument,
17
  type DocumentStatus,
 
 
 
18
  } from "../../services/api";
19
 
20
  interface KnowledgeManagementProps {
@@ -22,6 +33,17 @@ interface KnowledgeManagementProps {
22
  onClose: () => void;
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
25
  const getUserId = (): string | null => {
26
  const stored = localStorage.getItem("chatbot_user");
27
  if (!stored) return null;
@@ -32,6 +54,7 @@ export default function KnowledgeManagement({
32
  open,
33
  onClose,
34
  }: KnowledgeManagementProps) {
 
35
  const [documents, setDocuments] = useState<ApiDocument[]>([]);
36
  const [loadingDocs, setLoadingDocs] = useState(false);
37
  const [docsError, setDocsError] = useState<string | null>(null);
@@ -40,13 +63,56 @@ export default function KnowledgeManagement({
40
  const [processing, setProcessing] = useState<string | null>(null);
41
  const [deleting, setDeleting] = useState<string | null>(null);
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  useEffect(() => {
44
  if (!open) return;
45
  const userId = getUserId();
46
  if (!userId) return;
47
  loadDocuments(userId);
 
48
  }, [open]);
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  const loadDocuments = async (userId: string) => {
51
  setLoadingDocs(true);
52
  setDocsError(null);
@@ -83,7 +149,6 @@ export default function KnowledgeManagement({
83
  created_at: new Date().toISOString(),
84
  };
85
  setDocuments((prev) => [newDoc, ...prev]);
86
-
87
  await processDocumentById(userId, uploadRes.data.id);
88
  } catch (err) {
89
  setUploadError(err instanceof Error ? err.message : "Upload failed");
@@ -148,6 +213,59 @@ export default function KnowledgeManagement({
148
  setDocuments([]);
149
  };
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  const formatFileSize = (bytes: number) => {
152
  if (bytes < 1024) return bytes + " B";
153
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
@@ -177,7 +295,6 @@ export default function KnowledgeManagement({
177
  );
178
  }
179
 
180
- // pending or failed
181
  return (
182
  <button
183
  onClick={() => {
@@ -195,142 +312,422 @@ export default function KnowledgeManagement({
195
 
196
  if (!open) return null;
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  return (
199
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
200
- <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col m-4">
 
201
  {/* Header */}
202
- <div className="flex items-center justify-between p-4 border-b border-slate-200 bg-gradient-to-r from-[#FF8F00] to-[#FF6F00]">
203
- <div className="flex items-center gap-2">
204
- <Database className="w-5 h-5 text-white" />
205
- <h2 className="text-lg text-white">Knowledge Management</h2>
 
 
 
 
 
 
 
 
 
 
 
 
206
  </div>
207
  <button
208
- onClick={onClose}
209
- className="text-white/80 hover:text-white transition"
210
  >
211
- <X className="w-5 h-5" />
212
  </button>
213
  </div>
214
 
215
  {/* Content */}
216
- <div className="flex-1 overflow-y-auto p-4">
217
- {/* Upload Section */}
218
- <div className="mb-4">
219
- <label
220
- htmlFor="file-upload"
221
- className="flex items-center justify-center gap-2 border-2 border-dashed border-slate-300 rounded-lg p-6 cursor-pointer hover:border-[#4FC3F7] hover:bg-slate-50 transition"
222
- >
223
- <Upload className="w-5 h-5 text-slate-600" />
224
- <div className="text-center">
225
- <p className="text-slate-900 font-medium text-sm">
226
- Upload Documents (PDF, DOCX, TXT)
227
- </p>
228
- <p className="text-xs text-slate-500 mt-0.5">
229
- Click to browse or drag and drop
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  </p>
231
- </div>
232
- <input
233
- id="file-upload"
234
- type="file"
235
- accept=".pdf,.docx,.txt"
236
- multiple
237
- onChange={handleFileUpload}
238
- className="hidden"
239
- disabled={uploading}
240
- />
241
- </label>
242
- {uploadError && (
243
- <p className="mt-2 text-xs text-red-600 bg-red-50 border border-red-200 px-3 py-2 rounded-lg">
244
- {uploadError}
245
- </p>
246
- )}
247
- </div>
248
 
249
- {/* Documents List */}
250
- <div className="space-y-2">
251
- <div className="flex items-center justify-between mb-3">
252
- <h3 className="text-sm text-slate-900 font-medium">
253
- Documents ({documents.length})
254
- </h3>
255
- {documents.length > 0 && (
256
- <button
257
- onClick={deleteAllDocuments}
258
- className="text-xs text-red-600 hover:text-red-700 flex items-center gap-1"
259
- >
260
- <Trash2 className="w-3.5 h-3.5" />
261
- Delete All
262
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  )}
264
- </div>
265
 
266
- {loadingDocs ? (
267
- <div className="flex justify-center py-8">
268
- <Loader2 className="w-6 h-6 animate-spin text-slate-400" />
269
- </div>
270
- ) : docsError ? (
271
- <p className="text-center text-sm text-red-600 py-4">
272
- {docsError}
273
- </p>
274
- ) : documents.length === 0 ? (
275
- <div className="text-center py-8">
276
- <FileText className="w-12 h-12 text-slate-300 mx-auto mb-3" />
277
- <p className="text-slate-500 text-sm">
278
- No documents uploaded yet
279
- </p>
280
- <p className="text-xs text-slate-400 mt-1">
281
- Upload files to build your knowledge base
282
- </p>
283
- </div>
284
- ) : (
285
- documents.map((doc) => (
286
- <div
287
- key={doc.id}
288
- className="bg-slate-50 rounded-lg p-3 border border-slate-200"
289
- >
290
- <div className="flex items-start gap-3">
291
- <FileText className="w-8 h-8 text-red-500 flex-shrink-0 mt-0.5" />
292
- <div className="flex-1 min-w-0">
293
- <div className="flex items-start justify-between gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  <div className="flex-1 min-w-0">
295
- <h4 className="text-slate-900 font-medium truncate text-sm">
296
  {doc.filename}
297
- </h4>
298
- <p className="text-xs text-slate-500 mt-0.5">
299
- {formatFileSize(doc.file_size)} β€’{" "}
300
- {formatDate(doc.created_at)}
301
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  </div>
303
- <button
304
- onClick={() => handleDeleteDocument(doc.id)}
305
- disabled={deleting === doc.id}
306
- className="text-slate-400 hover:text-red-600 transition flex-shrink-0 disabled:opacity-50"
307
- title="Delete document"
308
- >
309
- {deleting === doc.id ? (
310
- <Loader2 className="w-4 h-4 animate-spin" />
311
- ) : (
312
- <Trash2 className="w-4 h-4" />
313
- )}
314
- </button>
315
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
- <div className="mt-2 flex items-center gap-2">
318
- {renderStatus(doc)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </div>
320
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  </div>
322
- </div>
323
- ))
324
- )}
325
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  </div>
327
 
328
  {/* Footer */}
329
- <div className="border-t border-slate-200 p-3 bg-slate-50 rounded-b-xl">
330
- <p className="text-[10px] text-slate-500 text-center">
331
- Supported formats: PDF, DOCX, TXT
 
 
 
 
332
  </p>
333
  </div>
 
334
  </div>
335
  </div>
336
  );
 
7
  Loader2,
8
  Database,
9
  X,
10
+ ChevronLeft,
11
+ Link,
12
  } from "lucide-react";
13
+ import { toast } from "sonner";
14
  import {
15
  getDocuments,
16
  uploadDocument,
17
  processDocument,
18
  deleteDocument,
19
+ getDatabaseClientTypes,
20
+ connectDatabase,
21
+ getDatabaseClients,
22
+ deleteDatabaseClient,
23
+ ingestDatabaseClient,
24
  type ApiDocument,
25
  type DocumentStatus,
26
+ type DbType,
27
+ type DbTypeInfo,
28
+ type DatabaseClient,
29
  } from "../../services/api";
30
 
31
  interface KnowledgeManagementProps {
 
33
  onClose: () => void;
34
  }
35
 
36
+ type View = "main" | "db-select" | "db-credentials";
37
+
38
+ const LOGO_MAP: Record<string, string> = {
39
+ postgres: "/databases/postgres.png",
40
+ mysql: "/databases/mysql.png",
41
+ supabase: "/databases/supabase.png",
42
+ sqlserver: "/databases/sqlserver.png",
43
+ bigquery: "/databases/big_query.svg",
44
+ snowflake: "/databases/snowflake.png",
45
+ };
46
+
47
  const getUserId = (): string | null => {
48
  const stored = localStorage.getItem("chatbot_user");
49
  if (!stored) return null;
 
54
  open,
55
  onClose,
56
  }: KnowledgeManagementProps) {
57
+ // ── Document state ──────────────────────────────────────────────────────────
58
  const [documents, setDocuments] = useState<ApiDocument[]>([]);
59
  const [loadingDocs, setLoadingDocs] = useState(false);
60
  const [docsError, setDocsError] = useState<string | null>(null);
 
63
  const [processing, setProcessing] = useState<string | null>(null);
64
  const [deleting, setDeleting] = useState<string | null>(null);
65
 
66
+ // ── Navigation state ────────────────────────────────────────────────────────
67
+ const [view, setView] = useState<View>("main");
68
+ const [selectedDbType, setSelectedDbType] = useState<DbType | null>(null);
69
+
70
+ // ── DB type & client state ──────────────────────────────────────────────────
71
+ const [dbTypeInfos, setDbTypeInfos] = useState<DbTypeInfo[]>([]);
72
+ const [dbClients, setDbClients] = useState<DatabaseClient[]>([]);
73
+ const [loadingDbTypes, setLoadingDbTypes] = useState(false);
74
+ const [ingesting, setIngesting] = useState<string | null>(null);
75
+ const [deletingClient, setDeletingClient] = useState<string | null>(null);
76
+
77
+ // ── DB credentials form state ───────────────────────────────────────────────
78
+ const [connectionName, setConnectionName] = useState("");
79
+ const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
80
+ const [connecting, setConnecting] = useState(false);
81
+
82
  useEffect(() => {
83
  if (!open) return;
84
  const userId = getUserId();
85
  if (!userId) return;
86
  loadDocuments(userId);
87
+ loadDbData(userId);
88
  }, [open]);
89
 
90
+ const loadDbData = async (userId: string) => {
91
+ setLoadingDbTypes(true);
92
+ try {
93
+ const [types, clients] = await Promise.all([
94
+ getDatabaseClientTypes(),
95
+ getDatabaseClients(userId),
96
+ ]);
97
+ setDbTypeInfos(types);
98
+ setDbClients(clients);
99
+ } catch {
100
+ // non-blocking; silently fail
101
+ } finally {
102
+ setLoadingDbTypes(false);
103
+ }
104
+ };
105
+
106
+ const handleClose = () => {
107
+ setView("main");
108
+ setSelectedDbType(null);
109
+ setConnectionName("");
110
+ setDbForm({});
111
+ onClose();
112
+ };
113
+
114
+ // ── Document handlers ───────────────────────────────────────────────────────
115
+
116
  const loadDocuments = async (userId: string) => {
117
  setLoadingDocs(true);
118
  setDocsError(null);
 
149
  created_at: new Date().toISOString(),
150
  };
151
  setDocuments((prev) => [newDoc, ...prev]);
 
152
  await processDocumentById(userId, uploadRes.data.id);
153
  } catch (err) {
154
  setUploadError(err instanceof Error ? err.message : "Upload failed");
 
213
  setDocuments([]);
214
  };
215
 
216
+ // ── DB handlers ─────────────────────────────────────────────────────────────
217
+
218
+ const handleDbConnect = async () => {
219
+ const userId = getUserId();
220
+ if (!userId || !selectedDbType || !connectionName.trim()) return;
221
+ setConnecting(true);
222
+ try {
223
+ await connectDatabase(userId, selectedDbType, connectionName.trim(), dbForm);
224
+ const clients = await getDatabaseClients(userId);
225
+ setDbClients(clients);
226
+ toast.success("Database connected successfully");
227
+ setView("main");
228
+ setConnectionName("");
229
+ setDbForm({});
230
+ } catch (err) {
231
+ toast.error(
232
+ err instanceof Error ? err.message : "Failed to connect to database"
233
+ );
234
+ } finally {
235
+ setConnecting(false);
236
+ }
237
+ };
238
+
239
+ const handleIngest = async (clientId: string) => {
240
+ const userId = getUserId();
241
+ if (!userId) return;
242
+ setIngesting(clientId);
243
+ try {
244
+ const res = await ingestDatabaseClient(clientId, userId);
245
+ toast.success(`Ingested ${res.chunks_ingested} chunks successfully`);
246
+ } catch (err) {
247
+ toast.error(err instanceof Error ? err.message : "Ingestion failed");
248
+ } finally {
249
+ setIngesting(null);
250
+ }
251
+ };
252
+
253
+ const handleDeleteClient = async (clientId: string) => {
254
+ const userId = getUserId();
255
+ if (!userId) return;
256
+ setDeletingClient(clientId);
257
+ try {
258
+ await deleteDatabaseClient(clientId, userId);
259
+ setDbClients((prev) => prev.filter((c) => c.id !== clientId));
260
+ } catch (err) {
261
+ toast.error(err instanceof Error ? err.message : "Failed to delete connection");
262
+ } finally {
263
+ setDeletingClient(null);
264
+ }
265
+ };
266
+
267
+ // ── Helpers ─────────────────────────────────────────────────────────────────
268
+
269
  const formatFileSize = (bytes: number) => {
270
  if (bytes < 1024) return bytes + " B";
271
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + " KB";
 
295
  );
296
  }
297
 
 
298
  return (
299
  <button
300
  onClick={() => {
 
312
 
313
  if (!open) return null;
314
 
315
+ // ── Header title & back button logic ────────────────────────────────────────
316
+
317
+ const selectedDbInfo = dbTypeInfos.find((d) => d.db_type === selectedDbType);
318
+
319
+ const headerTitle =
320
+ view === "db-select"
321
+ ? "Connect Database"
322
+ : view === "db-credentials"
323
+ ? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}`
324
+ : "Knowledge Base";
325
+
326
+ const headerBack =
327
+ view === "db-select"
328
+ ? () => setView("main")
329
+ : view === "db-credentials"
330
+ ? () => setView("db-select")
331
+ : null;
332
+
333
+ // ── Render ───────────────────────────────────────────────────────────────────
334
+
335
  return (
336
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
337
+ <div className="bg-white rounded-2xl shadow-xl w-full max-w-md max-h-[85vh] flex flex-col">
338
+
339
  {/* Header */}
340
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
341
+ <div className="flex items-center gap-3">
342
+ {headerBack ? (
343
+ <button
344
+ onClick={headerBack}
345
+ className="p-1.5 -ml-1 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
346
+ aria-label="Back"
347
+ >
348
+ <ChevronLeft className="w-4 h-4" />
349
+ </button>
350
+ ) : (
351
+ <div className="w-7 h-7 rounded-lg bg-orange-100 flex items-center justify-center">
352
+ <Database className="w-4 h-4 text-[#FF8F00]" />
353
+ </div>
354
+ )}
355
+ <h2 className="text-sm font-semibold text-slate-900">{headerTitle}</h2>
356
  </div>
357
  <button
358
+ onClick={handleClose}
359
+ className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
360
  >
361
+ <X className="w-4 h-4" />
362
  </button>
363
  </div>
364
 
365
  {/* Content */}
366
+ <div className="flex-1 overflow-y-auto px-5 py-4">
367
+
368
+ {/* ── VIEW: main ── */}
369
+ {view === "main" && (
370
+ <div className="space-y-4">
371
+
372
+ {/* Upload zone */}
373
+ <label
374
+ htmlFor="file-upload"
375
+ className="group flex flex-col items-center gap-2.5 border-2 border-dashed border-slate-200 rounded-xl p-7 cursor-pointer hover:border-[#FF8F00] hover:bg-orange-50/30 transition-colors"
376
+ >
377
+ <div className="w-10 h-10 rounded-xl bg-slate-100 group-hover:bg-orange-100 flex items-center justify-center transition-colors">
378
+ {uploading ? (
379
+ <Loader2 className="w-5 h-5 text-[#FF8F00] animate-spin" />
380
+ ) : (
381
+ <Upload className="w-5 h-5 text-slate-400 group-hover:text-[#FF8F00] transition-colors" />
382
+ )}
383
+ </div>
384
+ <div className="text-center">
385
+ <p className="text-sm font-medium text-slate-700">
386
+ {uploading ? "Uploading…" : (
387
+ <>Drop files, or <span className="text-[#FF8F00]">browse</span></>
388
+ )}
389
+ </p>
390
+ <p className="text-xs text-slate-400 mt-0.5">PDF, CSV, XLSX</p>
391
+ </div>
392
+ <input
393
+ id="file-upload"
394
+ type="file"
395
+ accept=".pdf,.csv,.xlsx"
396
+ multiple
397
+ onChange={handleFileUpload}
398
+ className="hidden"
399
+ disabled={uploading}
400
+ />
401
+ </label>
402
+
403
+ {uploadError && (
404
+ <p className="text-xs text-red-500 bg-red-50 border border-red-100 px-3 py-2 rounded-lg">
405
+ {uploadError}
406
  </p>
407
+ )}
408
+
409
+ {/* Connect DB row */}
410
+ <button
411
+ onClick={() => setView("db-select")}
412
+ className="w-full flex items-center gap-3 px-4 py-3 rounded-xl border border-slate-200 hover:border-slate-300 hover:bg-slate-50 transition text-left group"
413
+ >
414
+ <div className="w-8 h-8 rounded-lg bg-slate-100 group-hover:bg-slate-200 flex items-center justify-center flex-shrink-0 transition-colors">
415
+ <Link className="w-4 h-4 text-slate-500" />
416
+ </div>
417
+ <div className="flex-1 min-w-0">
418
+ <p className="text-sm font-medium text-slate-700">Connect a Database</p>
419
+ <p className="text-xs text-slate-400">PostgreSQL and more</p>
420
+ </div>
421
+ <ChevronLeft className="w-4 h-4 text-slate-300 rotate-180 flex-shrink-0" />
422
+ </button>
 
423
 
424
+ {/* Database connections list */}
425
+ {dbClients.length > 0 && (
426
+ <div>
427
+ <div className="flex items-center justify-between mb-2">
428
+ <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
429
+ Databases Β· {dbClients.length}
430
+ </span>
431
+ </div>
432
+ <div className="space-y-1">
433
+ {dbClients.map((client) => (
434
+ <div
435
+ key={client.id}
436
+ className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
437
+ >
438
+ <div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
439
+ <img
440
+ src={LOGO_MAP[client.db_type] ?? ""}
441
+ alt={client.db_type}
442
+ className="w-5 h-5 object-contain"
443
+ />
444
+ </div>
445
+ <div className="flex-1 min-w-0">
446
+ <p className="text-sm font-medium text-slate-800 truncate">{client.name}</p>
447
+ <p className="text-xs text-slate-400 capitalize">{client.db_type} Β· {client.status}</p>
448
+ </div>
449
+ <div className="flex items-center gap-1.5 flex-shrink-0">
450
+ <button
451
+ onClick={() => handleIngest(client.id)}
452
+ disabled={ingesting === client.id || client.status === "inactive"}
453
+ title="Ingest schema to knowledge base"
454
+ className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-slate-100 hover:bg-orange-100 hover:text-[#FF8F00] text-slate-500 transition disabled:opacity-40 disabled:cursor-not-allowed"
455
+ >
456
+ {ingesting === client.id ? (
457
+ <Loader2 className="w-3 h-3 animate-spin" />
458
+ ) : (
459
+ <Database className="w-3 h-3" />
460
+ )}
461
+ {ingesting === client.id ? "Ingesting…" : "Ingest"}
462
+ </button>
463
+ <button
464
+ onClick={() => handleDeleteClient(client.id)}
465
+ disabled={deletingClient === client.id}
466
+ className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
467
+ title="Delete connection"
468
+ >
469
+ {deletingClient === client.id ? (
470
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
471
+ ) : (
472
+ <Trash2 className="w-3.5 h-3.5" />
473
+ )}
474
+ </button>
475
+ </div>
476
+ </div>
477
+ ))}
478
+ </div>
479
+ </div>
480
  )}
 
481
 
482
+ {/* Documents list */}
483
+ <div>
484
+ <div className="flex items-center justify-between mb-2">
485
+ <span className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
486
+ Documents Β· {documents.length}
487
+ </span>
488
+ {documents.length > 0 && (
489
+ <button
490
+ onClick={deleteAllDocuments}
491
+ className="text-xs text-slate-400 hover:text-red-500 flex items-center gap-1 transition"
492
+ >
493
+ <Trash2 className="w-3 h-3" />
494
+ Clear all
495
+ </button>
496
+ )}
497
+ </div>
498
+
499
+ {loadingDocs ? (
500
+ <div className="flex justify-center py-10">
501
+ <Loader2 className="w-5 h-5 animate-spin text-slate-300" />
502
+ </div>
503
+ ) : docsError ? (
504
+ <p className="text-center text-xs text-red-500 py-6">{docsError}</p>
505
+ ) : documents.length === 0 ? (
506
+ <div className="text-center py-10">
507
+ <div className="w-12 h-12 rounded-2xl bg-slate-100 flex items-center justify-center mx-auto mb-3">
508
+ <FileText className="w-5 h-5 text-slate-300" />
509
+ </div>
510
+ <p className="text-sm text-slate-400">No documents yet</p>
511
+ <p className="text-xs text-slate-300 mt-0.5">Upload files to get started</p>
512
+ </div>
513
+ ) : (
514
+ <div className="space-y-1">
515
+ {documents.map((doc) => (
516
+ <div
517
+ key={doc.id}
518
+ className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
519
+ >
520
+ <div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
521
+ <FileText className="w-4 h-4 text-red-400" />
522
+ </div>
523
  <div className="flex-1 min-w-0">
524
+ <p className="text-sm font-medium text-slate-800 truncate">
525
  {doc.filename}
 
 
 
 
526
  </p>
527
+ <p className="text-xs text-slate-400">
528
+ {formatFileSize(doc.file_size)} Β· {formatDate(doc.created_at)}
529
+ </p>
530
+ </div>
531
+ <div className="flex items-center gap-2 flex-shrink-0">
532
+ {renderStatus(doc)}
533
+ <button
534
+ onClick={() => handleDeleteDocument(doc.id)}
535
+ disabled={deleting === doc.id}
536
+ className="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition disabled:opacity-30"
537
+ title="Delete"
538
+ >
539
+ {deleting === doc.id ? (
540
+ <Loader2 className="w-3.5 h-3.5 animate-spin" />
541
+ ) : (
542
+ <Trash2 className="w-3.5 h-3.5" />
543
+ )}
544
+ </button>
545
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
546
  </div>
547
+ ))}
548
+ </div>
549
+ )}
550
+ </div>
551
+ </div>
552
+ )}
553
+
554
+ {/* ── VIEW: db-select ── */}
555
+ {view === "db-select" && (
556
+ <div className="space-y-4">
557
+ <p className="text-sm text-slate-400">
558
+ Choose a database to connect to your knowledge base.
559
+ </p>
560
+ {loadingDbTypes ? (
561
+ <div className="flex justify-center py-10">
562
+ <Loader2 className="w-5 h-5 animate-spin text-slate-300" />
563
+ </div>
564
+ ) : (
565
+ <div className="grid grid-cols-3 gap-2">
566
+ {dbTypeInfos.map((info) => {
567
+ const active = info.status === "active";
568
+ return (
569
+ <button
570
+ key={info.db_type}
571
+ disabled={!active}
572
+ onClick={() => {
573
+ if (!active) return;
574
+ setSelectedDbType(info.db_type);
575
+ const defaults = Object.fromEntries(
576
+ info.fields.map((f) => [
577
+ f.name,
578
+ f.default != null
579
+ ? f.default
580
+ : f.type === "integer" ? 0 : f.type === "boolean" ? false : "",
581
+ ])
582
+ );
583
+ setDbForm(defaults);
584
+ setView("db-credentials");
585
+ }}
586
+ className={[
587
+ "relative flex flex-col items-center gap-2 p-4 rounded-xl border transition text-xs font-medium",
588
+ active
589
+ ? "border-slate-200 hover:border-[#FF8F00] hover:bg-orange-50/50 text-slate-700 cursor-pointer"
590
+ : "border-slate-100 bg-slate-50/60 text-slate-400 cursor-not-allowed",
591
+ ].join(" ")}
592
+ >
593
+ <img
594
+ src={LOGO_MAP[info.logo] ?? ""}
595
+ alt={info.display_name}
596
+ className={`w-8 h-8 object-contain ${!active ? "opacity-30 grayscale" : ""}`}
597
+ />
598
+ <span>{info.display_name}</span>
599
+ {!active && (
600
+ <span className="absolute top-1.5 right-1.5 text-[9px] bg-slate-200 text-slate-400 px-1.5 py-0.5 rounded-full">
601
+ Soon
602
+ </span>
603
+ )}
604
+ </button>
605
+ );
606
+ })}
607
+ </div>
608
+ )}
609
+ </div>
610
+ )}
611
+
612
+ {/* ── VIEW: db-credentials ── */}
613
+ {view === "db-credentials" && (
614
+ <div className="space-y-4">
615
 
616
+ {/* Selected DB chip */}
617
+ {selectedDbInfo && (
618
+ <div className="flex items-center gap-2.5 px-3 py-2.5 rounded-xl bg-slate-50 border border-slate-100">
619
+ <img src={LOGO_MAP[selectedDbInfo.logo] ?? ""} alt={selectedDbInfo.display_name} className="w-5 h-5 object-contain" />
620
+ <span className="text-sm font-medium text-slate-700">{selectedDbInfo.display_name}</span>
621
+ </div>
622
+ )}
623
+
624
+ <div className="grid grid-cols-2 gap-3">
625
+ {/* Connection Name */}
626
+ <div className="col-span-2">
627
+ <label className="block text-xs font-medium text-slate-500 mb-1">
628
+ Connection Name <span className="text-red-400">*</span>
629
+ </label>
630
+ <input
631
+ type="text"
632
+ placeholder="Production DB"
633
+ value={connectionName}
634
+ onChange={(e) => setConnectionName(e.target.value)}
635
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
636
+ />
637
+ </div>
638
+
639
+ {/* Dynamic fields from API */}
640
+ {selectedDbInfo?.fields.map((field) => (
641
+ <div
642
+ key={field.name}
643
+ className={field.name === "host" || field.name === "service_account_json" ? "col-span-2" : ""}
644
+ >
645
+ <label className="block text-xs font-medium text-slate-500 mb-1 capitalize">
646
+ {field.name.replace(/_/g, " ")}
647
+ {field.required && <span className="text-red-400 ml-0.5">*</span>}
648
+ </label>
649
+
650
+ {field.type === "select" ? (
651
+ <select
652
+ value={String(dbForm[field.name] ?? field.default ?? "")}
653
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
654
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] transition"
655
+ >
656
+ {field.options?.map((opt) => (
657
+ <option key={opt} value={opt}>{opt}</option>
658
+ ))}
659
+ </select>
660
+ ) : field.type === "boolean" ? (
661
+ <div className="flex items-center gap-2 pt-1">
662
+ <input
663
+ type="checkbox"
664
+ id={`field-${field.name}`}
665
+ checked={Boolean(dbForm[field.name] ?? field.default ?? false)}
666
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.checked }))}
667
+ className="w-4 h-4 accent-[#FF8F00]"
668
+ />
669
+ <label htmlFor={`field-${field.name}`} className="text-xs text-slate-500">
670
+ {field.description}
671
+ </label>
672
  </div>
673
+ ) : field.name === "service_account_json" ? (
674
+ <textarea
675
+ rows={4}
676
+ placeholder={field.description}
677
+ value={String(dbForm[field.name] ?? "")}
678
+ onChange={(e) => setDbForm((f) => ({ ...f, [field.name]: e.target.value }))}
679
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-xs bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition font-mono"
680
+ />
681
+ ) : (
682
+ <input
683
+ type={field.sensitive ? "password" : field.type === "integer" ? "number" : "text"}
684
+ placeholder={field.default != null ? String(field.default) : field.description}
685
+ value={String(dbForm[field.name] ?? "")}
686
+ onChange={(e) =>
687
+ setDbForm((f) => ({
688
+ ...f,
689
+ [field.name]: field.type === "integer"
690
+ ? (parseInt(e.target.value, 10) || 0)
691
+ : e.target.value,
692
+ }))
693
+ }
694
+ className="w-full border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white focus:outline-none focus:ring-2 focus:ring-[#FF8F00]/25 focus:border-[#FF8F00] placeholder:text-slate-300 transition"
695
+ />
696
+ )}
697
  </div>
698
+ ))}
699
+ </div>
700
+
701
+ <button
702
+ onClick={handleDbConnect}
703
+ disabled={
704
+ connecting ||
705
+ !connectionName.trim() ||
706
+ (selectedDbInfo?.fields.filter((f) => f.required).some((f) => !dbForm[f.name]) ?? false)
707
+ }
708
+ className="w-full flex items-center justify-center gap-2 bg-[#FF8F00] hover:bg-[#FF6F00] active:bg-[#E65100] text-white py-2.5 rounded-xl text-sm font-medium transition disabled:opacity-40 disabled:cursor-not-allowed"
709
+ >
710
+ {connecting ? (
711
+ <><Loader2 className="w-4 h-4 animate-spin" /> Connecting…</>
712
+ ) : (
713
+ "Connect"
714
+ )}
715
+ </button>
716
+ </div>
717
+ )}
718
  </div>
719
 
720
  {/* Footer */}
721
+ <div className="px-5 py-3 border-t border-slate-100">
722
+ <p className="text-[10px] text-slate-300 text-center">
723
+ {view === "main"
724
+ ? "Supported formats: PDF, CSV, XLSX"
725
+ : view === "db-select"
726
+ ? "More integrations coming soon"
727
+ : "Credentials are encrypted at rest"}
728
  </p>
729
  </div>
730
+
731
  </div>
732
  </div>
733
  );
src/app/components/Login.tsx CHANGED
@@ -43,28 +43,149 @@ export default function Login() {
43
  };
44
 
45
  return (
46
- <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#BAE6FD] via-[#A7F3D0] to-[#FDE68A]">
47
- <div className="w-full max-w-md px-4">
48
- <div className="bg-white rounded-xl shadow-2xl p-6 border border-slate-200">
49
- <div className="flex items-center justify-center mb-6">
50
- <div className="bg-gradient-to-br from-[#059669] to-[#047857] p-2.5 rounded-lg">
51
- <LogIn className="w-6 h-6 text-white" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
  </div>
54
 
55
- <h1 className="text-xl text-center mb-1 text-slate-900">
56
- Welcome Back
57
- </h1>
58
- <p className="text-center text-slate-500 text-sm mb-6">
59
- Sign in to continue to your chatbot
60
- </p>
61
-
62
  <form onSubmit={handleLogin} className="space-y-4">
63
  <div>
64
- <label
65
- htmlFor="email"
66
- className="block text-xs mb-1.5 text-slate-700"
67
- >
68
  Email Address
69
  </label>
70
  <input
@@ -72,17 +193,14 @@ export default function Login() {
72
  type="email"
73
  value={email}
74
  onChange={(e) => setEmail(e.target.value)}
75
- className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
76
  placeholder="you@example.com"
77
  disabled={isLoading}
78
  />
79
  </div>
80
 
81
  <div>
82
- <label
83
- htmlFor="password"
84
- className="block text-xs mb-1.5 text-slate-700"
85
- >
86
  Password
87
  </label>
88
  <input
@@ -90,14 +208,14 @@ export default function Login() {
90
  type="password"
91
  value={password}
92
  onChange={(e) => setPassword(e.target.value)}
93
- className="w-full px-3 py-2 text-sm rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-[#4FC3F7] focus:border-transparent transition"
94
  placeholder="Enter your password"
95
  disabled={isLoading}
96
  />
97
  </div>
98
 
99
  {error && (
100
- <div className="bg-red-50 border border-red-200 text-red-700 px-3 py-2 rounded-lg text-xs">
101
  {error}
102
  </div>
103
  )}
@@ -105,14 +223,14 @@ export default function Login() {
105
  <button
106
  type="submit"
107
  disabled={isLoading}
108
- className="w-full flex items-center justify-center gap-2 bg-gradient-to-r from-[#059669] to-[#047857] text-white py-2.5 text-sm rounded-lg hover:from-[#047857] hover:to-[#065F46] transition font-medium disabled:opacity-60 disabled:cursor-not-allowed"
109
  >
110
  {isLoading ? (
111
  <Loader2 className="w-4 h-4 animate-spin" />
112
  ) : (
113
  <LogIn className="w-4 h-4" />
114
  )}
115
- {isLoading ? "Signing in..." : "Sign In"}
116
  </button>
117
  </form>
118
  </div>
 
43
  };
44
 
45
  return (
46
+ <div className="relative min-h-screen flex items-center justify-center overflow-hidden bg-[#1e2d45]">
47
+
48
+ {/* ── Dot grid texture ── */}
49
+ <div
50
+ className="absolute inset-0 z-0 opacity-20"
51
+ style={{
52
+ backgroundImage: "radial-gradient(circle, #334155 1px, transparent 1px)",
53
+ backgroundSize: "28px 28px",
54
+ }}
55
+ />
56
+
57
+ {/* ── Gradient orbs β€” glowing on dark bg ── */}
58
+ {/* Cyan β€” top left */}
59
+ <div
60
+ className="absolute -top-32 -left-32 w-[520px] h-[520px] rounded-full blur-3xl opacity-[0.18] animate-pulse"
61
+ style={{ background: "#0ea5e9", animationDuration: "5s" }}
62
+ />
63
+ {/* Orange β€” bottom right */}
64
+ <div
65
+ className="absolute -bottom-40 -right-40 w-[560px] h-[560px] rounded-full blur-3xl opacity-[0.16] animate-pulse"
66
+ style={{ background: "#f97316", animationDuration: "7s" }}
67
+ />
68
+ {/* Green β€” top right */}
69
+ <div
70
+ className="absolute -top-24 right-1/4 w-[320px] h-[320px] rounded-full blur-3xl opacity-[0.12] animate-pulse"
71
+ style={{ background: "#10b981", animationDuration: "9s" }}
72
+ />
73
+ {/* Purple accent β€” center left */}
74
+ <div
75
+ className="absolute top-1/2 -left-40 w-[360px] h-[360px] rounded-full blur-3xl opacity-[0.10] animate-pulse"
76
+ style={{ background: "#8b5cf6", animationDuration: "11s" }}
77
+ />
78
+
79
+ {/* ── Neural network β€” bottom left ── */}
80
+ <svg
81
+ className="absolute bottom-8 left-8 opacity-[0.35] animate-float-slow"
82
+ width="200" height="200" viewBox="0 0 200 200"
83
+ fill="none" xmlns="http://www.w3.org/2000/svg"
84
+ >
85
+ <line x1="20" y1="150" x2="65" y2="90" stroke="#0ea5e9" strokeWidth="1"/>
86
+ <line x1="20" y1="150" x2="55" y2="170" stroke="#0ea5e9" strokeWidth="1"/>
87
+ <line x1="65" y1="90" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
88
+ <line x1="65" y1="90" x2="130" y2="55" stroke="#10b981" strokeWidth="1"/>
89
+ <line x1="110" y1="125" x2="170" y2="105" stroke="#0ea5e9" strokeWidth="1"/>
90
+ <line x1="130" y1="55" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
91
+ <line x1="130" y1="55" x2="175" y2="30" stroke="#10b981" strokeWidth="1"/>
92
+ <line x1="55" y1="170" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1"/>
93
+ <line x1="175" y1="30" x2="170" y2="105" stroke="#10b981" strokeWidth="1"/>
94
+ <circle cx="20" cy="150" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
95
+ <circle cx="55" cy="170" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
96
+ <circle cx="65" cy="90" r="5" fill="#0ea5e9" fillOpacity="0.9"/>
97
+ <circle cx="110" cy="125" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
98
+ <circle cx="130" cy="55" r="5" fill="#10b981" fillOpacity="0.9"/>
99
+ <circle cx="170" cy="105" r="3.5" fill="#10b981" fillOpacity="0.7"/>
100
+ <circle cx="175" cy="30" r="2.5" fill="#10b981" fillOpacity="0.5"/>
101
+ </svg>
102
+
103
+ {/* ── Neural network β€” top right ── */}
104
+ <svg
105
+ className="absolute top-8 right-8 opacity-[0.30] animate-float"
106
+ style={{ "--float-rotate": "0deg" } as React.CSSProperties}
107
+ width="180" height="180" viewBox="0 0 180 180"
108
+ fill="none" xmlns="http://www.w3.org/2000/svg"
109
+ >
110
+ <line x1="160" y1="30" x2="115" y2="80" stroke="#f97316" strokeWidth="1"/>
111
+ <line x1="115" y1="80" x2="70" y2="55" stroke="#f97316" strokeWidth="1"/>
112
+ <line x1="115" y1="80" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
113
+ <line x1="70" y1="55" x2="25" y2="90" stroke="#f97316" strokeWidth="1"/>
114
+ <line x1="25" y1="90" x2="80" y2="155" stroke="#0ea5e9" strokeWidth="1"/>
115
+ <line x1="80" y1="155" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1"/>
116
+ <line x1="70" y1="55" x2="30" y2="25" stroke="#f97316" strokeWidth="1"/>
117
+ <circle cx="160" cy="30" r="3.5" fill="#f97316" fillOpacity="0.7"/>
118
+ <circle cx="115" cy="80" r="5" fill="#f97316" fillOpacity="0.9"/>
119
+ <circle cx="70" cy="55" r="3.5" fill="#f97316" fillOpacity="0.7"/>
120
+ <circle cx="130" cy="135" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
121
+ <circle cx="25" cy="90" r="3.5" fill="#0ea5e9" fillOpacity="0.7"/>
122
+ <circle cx="80" cy="155" r="2.5" fill="#0ea5e9" fillOpacity="0.5"/>
123
+ <circle cx="30" cy="25" r="2.5" fill="#f97316" fillOpacity="0.5"/>
124
+ </svg>
125
+
126
+ {/* ── Large ring β€” top right ── */}
127
+ <div className="absolute -top-24 -right-24 w-80 h-80 rounded-full border border-slate-700 animate-float-slow opacity-50" />
128
+ <div className="absolute -top-10 -right-10 w-48 h-48 rounded-full border border-slate-800 opacity-60" />
129
+
130
+ {/* ── Rotated square β€” bottom left ── */}
131
+ <div
132
+ className="absolute bottom-16 left-16 w-24 h-24 border border-slate-200 opacity-50 animate-float"
133
+ style={{ transform: "rotate(20deg)", "--float-rotate": "20deg" } as React.CSSProperties}
134
+ />
135
+ <div
136
+ className="absolute bottom-28 left-28 w-12 h-12 border border-slate-200 opacity-40"
137
+ style={{ transform: "rotate(45deg)" }}
138
+ />
139
+
140
+ {/* ── Hexagon β€” bottom right ── */}
141
+ <svg
142
+ className="absolute bottom-12 right-16 opacity-[0.35] animate-float"
143
+ style={{ "--float-rotate": "0deg", animationDelay: "2s" } as React.CSSProperties}
144
+ width="80" height="80" viewBox="0 0 160 160"
145
+ fill="none" xmlns="http://www.w3.org/2000/svg"
146
+ >
147
+ <path
148
+ d="M 140 80 L 110 132 L 50 132 L 20 80 L 50 28 L 110 28 Z"
149
+ stroke="#f97316" strokeWidth="1.5" fill="none"
150
+ />
151
+ <path
152
+ d="M 122 80 L 101 114 L 59 114 L 38 80 L 59 46 L 101 46 Z"
153
+ stroke="#f97316" strokeWidth="1" strokeOpacity="0.5" fill="none"
154
+ />
155
+ </svg>
156
+
157
+ {/* ── Rotated square β€” bottom left ── */}
158
+ <div
159
+ className="absolute bottom-16 left-16 w-24 h-24 border border-slate-700 opacity-50 animate-float"
160
+ style={{ transform: "rotate(20deg)", "--float-rotate": "20deg" } as React.CSSProperties}
161
+ />
162
+ <div
163
+ className="absolute bottom-28 left-28 w-12 h-12 border border-slate-700 opacity-30"
164
+ style={{ transform: "rotate(45deg)" }}
165
+ />
166
+
167
+ {/* ── Small floating dots ── */}
168
+ <div className="absolute top-1/4 left-12 w-2 h-2 rounded-full bg-sky-400 opacity-60 animate-float" style={{ animationDelay: "1s" }} />
169
+ <div className="absolute top-1/3 right-20 w-1.5 h-1.5 rounded-full bg-orange-400 opacity-60 animate-float-slow" style={{ animationDelay: "3s" }} />
170
+ <div className="absolute bottom-1/3 left-1/4 w-1.5 h-1.5 rounded-full bg-emerald-400 opacity-60 animate-float" style={{ animationDelay: "0.5s" }} />
171
+ <div className="absolute top-2/3 right-1/3 w-1 h-1 rounded-full bg-violet-400 opacity-50 animate-float-slow" style={{ animationDelay: "4s" }} />
172
+
173
+ {/* ── Login card ── */}
174
+ <div className="relative z-10 w-full max-w-md px-4">
175
+ <div className="bg-white rounded-2xl shadow-2xl shadow-black/40 p-7 border border-white/10">
176
+
177
+ {/* Brand logo */}
178
+ <div className="flex flex-col items-center gap-3 mb-6">
179
+ <img src="/logo.png" alt="DataEyond" className="w-12 h-12 object-contain" />
180
+ <div className="text-center">
181
+ <h1 className="text-xl font-semibold text-slate-900">Welcome Back</h1>
182
+ <p className="text-slate-400 text-sm mt-0.5">Sign in to continue to your chatbot</p>
183
  </div>
184
  </div>
185
 
 
 
 
 
 
 
 
186
  <form onSubmit={handleLogin} className="space-y-4">
187
  <div>
188
+ <label htmlFor="email" className="block text-xs font-medium mb-1.5 text-slate-600">
 
 
 
189
  Email Address
190
  </label>
191
  <input
 
193
  type="email"
194
  value={email}
195
  onChange={(e) => setEmail(e.target.value)}
196
+ className="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
197
  placeholder="you@example.com"
198
  disabled={isLoading}
199
  />
200
  </div>
201
 
202
  <div>
203
+ <label htmlFor="password" className="block text-xs font-medium mb-1.5 text-slate-600">
 
 
 
204
  Password
205
  </label>
206
  <input
 
208
  type="password"
209
  value={password}
210
  onChange={(e) => setPassword(e.target.value)}
211
+ className="w-full px-3 py-2 text-sm rounded-xl border border-slate-200 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-400/30 focus:border-sky-400 placeholder:text-slate-300 transition"
212
  placeholder="Enter your password"
213
  disabled={isLoading}
214
  />
215
  </div>
216
 
217
  {error && (
218
+ <div className="bg-red-50 border border-red-100 text-red-600 px-3 py-2 rounded-xl text-xs">
219
  {error}
220
  </div>
221
  )}
 
223
  <button
224
  type="submit"
225
  disabled={isLoading}
226
+ className="w-full flex items-center justify-center gap-2 bg-[#059669] hover:bg-[#047857] active:bg-[#065F46] text-white py-2.5 text-sm rounded-xl transition font-medium disabled:opacity-50 disabled:cursor-not-allowed mt-1"
227
  >
228
  {isLoading ? (
229
  <Loader2 className="w-4 h-4 animate-spin" />
230
  ) : (
231
  <LogIn className="w-4 h-4" />
232
  )}
233
+ {isLoading ? "Signing in…" : "Sign In"}
234
  </button>
235
  </form>
236
  </div>
src/app/components/Main.tsx CHANGED
@@ -604,9 +604,79 @@ export default function Main() {
604
  </div>
605
 
606
  {/* Main Content */}
607
- <div className="flex-1 flex flex-col min-w-0">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  {/* Header */}
609
- <div className="bg-white border-b border-slate-200 p-3 flex items-center gap-3">
610
  <button
611
  onClick={() => setSidebarOpen(!sidebarOpen)}
612
  className="text-slate-600 hover:text-slate-900 transition"
@@ -630,7 +700,7 @@ export default function Main() {
630
  </div>
631
 
632
  {/* Messages */}
633
- <div className="flex-1 overflow-y-auto p-4 space-y-4">
634
  {currentChat?.messages.length === 0 && (
635
  <div className="flex items-center justify-center h-full">
636
  <div className="text-center">
@@ -734,7 +804,7 @@ export default function Main() {
734
  </div>
735
 
736
  {/* Input Area */}
737
- <div className="bg-white border-t border-slate-200 p-3 shadow-[0_-2px_10px_rgba(0,0,0,0.06)]">
738
  <div className="max-w-4xl mx-auto">
739
  <div className="flex gap-2 items-end">
740
  <textarea
 
604
  </div>
605
 
606
  {/* Main Content */}
607
+ <div className="flex-1 flex flex-col min-w-0 relative">
608
+
609
+ {/* ── Subtle background β€” harmonizes with Login page ── */}
610
+ <div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
611
+ {/* Dot grid */}
612
+ <div
613
+ className="absolute inset-0 opacity-[0.13]"
614
+ style={{
615
+ backgroundImage: "radial-gradient(circle, #94a3b8 1px, transparent 1px)",
616
+ backgroundSize: "28px 28px",
617
+ }}
618
+ />
619
+ {/* Cyan orb β€” top right */}
620
+ <div
621
+ className="absolute -top-28 -right-28 w-80 h-80 rounded-full blur-3xl opacity-[0.08]"
622
+ style={{ background: "#0ea5e9" }}
623
+ />
624
+ {/* Orange orb β€” bottom left */}
625
+ <div
626
+ className="absolute -bottom-28 -left-28 w-96 h-96 rounded-full blur-3xl opacity-[0.07]"
627
+ style={{ background: "#f97316" }}
628
+ />
629
+ {/* Green orb β€” center left */}
630
+ <div
631
+ className="absolute top-1/2 -left-32 w-64 h-64 rounded-full blur-3xl opacity-[0.05]"
632
+ style={{ background: "#10b981" }}
633
+ />
634
+ {/* Neural network β€” bottom right */}
635
+ <svg
636
+ className="absolute bottom-20 right-4 opacity-[0.07]"
637
+ width="160" height="160" viewBox="0 0 200 200"
638
+ fill="none" xmlns="http://www.w3.org/2000/svg"
639
+ >
640
+ <line x1="20" y1="150" x2="65" y2="90" stroke="#0ea5e9" strokeWidth="1.2"/>
641
+ <line x1="20" y1="150" x2="55" y2="170" stroke="#0ea5e9" strokeWidth="1.2"/>
642
+ <line x1="65" y1="90" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1.2"/>
643
+ <line x1="65" y1="90" x2="130" y2="55" stroke="#10b981" strokeWidth="1.2"/>
644
+ <line x1="110" y1="125" x2="170" y2="105" stroke="#0ea5e9" strokeWidth="1.2"/>
645
+ <line x1="130" y1="55" x2="170" y2="105" stroke="#10b981" strokeWidth="1.2"/>
646
+ <line x1="130" y1="55" x2="175" y2="30" stroke="#10b981" strokeWidth="1.2"/>
647
+ <line x1="55" y1="170" x2="110" y2="125" stroke="#0ea5e9" strokeWidth="1.2"/>
648
+ <line x1="175" y1="30" x2="170" y2="105" stroke="#10b981" strokeWidth="1.2"/>
649
+ <circle cx="20" cy="150" r="4" fill="#0ea5e9"/>
650
+ <circle cx="55" cy="170" r="3" fill="#0ea5e9"/>
651
+ <circle cx="65" cy="90" r="5.5" fill="#0ea5e9"/>
652
+ <circle cx="110" cy="125" r="4" fill="#0ea5e9"/>
653
+ <circle cx="130" cy="55" r="5.5" fill="#10b981"/>
654
+ <circle cx="170" cy="105" r="4" fill="#10b981"/>
655
+ <circle cx="175" cy="30" r="3" fill="#10b981"/>
656
+ </svg>
657
+ {/* Neural network β€” top left (small) */}
658
+ <svg
659
+ className="absolute top-16 left-4 opacity-[0.06]"
660
+ width="110" height="110" viewBox="0 0 180 180"
661
+ fill="none" xmlns="http://www.w3.org/2000/svg"
662
+ >
663
+ <line x1="160" y1="30" x2="115" y2="80" stroke="#f97316" strokeWidth="1.2"/>
664
+ <line x1="115" y1="80" x2="70" y2="55" stroke="#f97316" strokeWidth="1.2"/>
665
+ <line x1="115" y1="80" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1.2"/>
666
+ <line x1="70" y1="55" x2="25" y2="90" stroke="#f97316" strokeWidth="1.2"/>
667
+ <line x1="25" y1="90" x2="80" y2="155" stroke="#0ea5e9" strokeWidth="1.2"/>
668
+ <line x1="80" y1="155" x2="130" y2="135" stroke="#0ea5e9" strokeWidth="1.2"/>
669
+ <circle cx="160" cy="30" r="4" fill="#f97316"/>
670
+ <circle cx="115" cy="80" r="5.5" fill="#f97316"/>
671
+ <circle cx="70" cy="55" r="4" fill="#f97316"/>
672
+ <circle cx="130" cy="135" r="3" fill="#0ea5e9"/>
673
+ <circle cx="25" cy="90" r="4" fill="#0ea5e9"/>
674
+ <circle cx="80" cy="155" r="3" fill="#0ea5e9"/>
675
+ </svg>
676
+ </div>
677
+
678
  {/* Header */}
679
+ <div className="relative z-10 bg-white border-b border-slate-200 p-3 flex items-center gap-3">
680
  <button
681
  onClick={() => setSidebarOpen(!sidebarOpen)}
682
  className="text-slate-600 hover:text-slate-900 transition"
 
700
  </div>
701
 
702
  {/* Messages */}
703
+ <div className="relative z-10 flex-1 overflow-y-auto p-4 space-y-4">
704
  {currentChat?.messages.length === 0 && (
705
  <div className="flex items-center justify-center h-full">
706
  <div className="text-center">
 
804
  </div>
805
 
806
  {/* Input Area */}
807
+ <div className="relative z-10 bg-white border-t border-slate-200 p-3 shadow-[0_-2px_10px_rgba(0,0,0,0.06)]">
808
  <div className="max-w-4xl mx-auto">
809
  <div className="flex gap-2 items-end">
810
  <textarea
src/services/api.ts CHANGED
@@ -151,6 +151,74 @@ export const deleteDocument = (userId: string, documentId: string) =>
151
  { method: "DELETE" }
152
  );
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  // ─── Chat ─────────────────────────────────────────────────────────────────────
155
 
156
  export const streamChat = (
 
151
  { method: "DELETE" }
152
  );
153
 
154
+ // ─── Database Clients ─────────────────────────────────────────────────────────
155
+
156
+ export type DbType = "postgres" | "mysql" | "sqlserver" | "supabase" | "bigquery" | "snowflake";
157
+
158
+ export interface DbTypeField {
159
+ name: string;
160
+ type: "string" | "integer" | "select" | "boolean";
161
+ required: boolean;
162
+ default: string | number | boolean | null;
163
+ description: string;
164
+ options?: string[];
165
+ sensitive?: boolean;
166
+ }
167
+
168
+ export interface DbTypeInfo {
169
+ db_type: DbType;
170
+ display_name: string;
171
+ logo: string;
172
+ status: "active" | "inactive";
173
+ message: string | null;
174
+ fields: DbTypeField[];
175
+ }
176
+
177
+ export interface DatabaseClient {
178
+ id: string;
179
+ user_id: string;
180
+ name: string;
181
+ db_type: DbType;
182
+ status: "active" | "inactive";
183
+ created_at: string;
184
+ updated_at: string | null;
185
+ }
186
+
187
+ export interface IngestResponse {
188
+ status: string;
189
+ client_id: string;
190
+ chunks_ingested: number;
191
+ }
192
+
193
+ export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> =>
194
+ request<DbTypeInfo[]>("/api/v1/database-clients/dbtypes");
195
+
196
+ export const connectDatabase = (
197
+ userId: string,
198
+ dbType: DbType,
199
+ name: string,
200
+ credentials: Record<string, string | number | boolean>
201
+ ): Promise<DatabaseClient> =>
202
+ request<DatabaseClient>(`/api/v1/database-clients?user_id=${userId}`, {
203
+ method: "POST",
204
+ body: JSON.stringify({ name, db_type: dbType, credentials }),
205
+ });
206
+
207
+ export const getDatabaseClients = (userId: string): Promise<DatabaseClient[]> =>
208
+ request<DatabaseClient[]>(`/api/v1/database-clients/${userId}`);
209
+
210
+ export const deleteDatabaseClient = (clientId: string, userId: string) =>
211
+ request<{ status: string; message: string }>(
212
+ `/api/v1/database-clients/${clientId}?user_id=${userId}`,
213
+ { method: "DELETE" }
214
+ );
215
+
216
+ export const ingestDatabaseClient = (clientId: string, userId: string): Promise<IngestResponse> =>
217
+ request<IngestResponse>(
218
+ `/api/v1/database-clients/${clientId}/ingest?user_id=${userId}`,
219
+ { method: "POST" }
220
+ );
221
+
222
  // ─── Chat ─────────────────────────────────────────────────────────────────────
223
 
224
  export const streamChat = (
src/styles/theme.css CHANGED
@@ -179,3 +179,21 @@
179
  line-height: 1.5;
180
  }
181
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  line-height: 1.5;
180
  }
181
  }
182
+
183
+ @keyframes float {
184
+ 0%, 100% { transform: translateY(0px) rotate(var(--float-rotate, 0deg)); }
185
+ 50% { transform: translateY(-14px) rotate(var(--float-rotate, 0deg)); }
186
+ }
187
+
188
+ @keyframes float-slow {
189
+ 0%, 100% { transform: translateY(0px); }
190
+ 50% { transform: translateY(-8px); }
191
+ }
192
+
193
+ .animate-float {
194
+ animation: float 7s ease-in-out infinite;
195
+ }
196
+
197
+ .animate-float-slow {
198
+ animation: float-slow 10s ease-in-out infinite;
199
+ }