ishaq101 commited on
Commit
c479f3f
Β·
1 Parent(s): 89e1114

[NOTICKET] Update icon file

Browse files
src/app/components/KnowledgeManagement.tsx CHANGED
@@ -3,12 +3,15 @@ import {
3
  Upload,
4
  Trash2,
5
  FileText,
 
 
6
  Check,
7
  Loader2,
8
  Database,
9
  X,
10
  ChevronLeft,
11
  Link,
 
12
  } from "lucide-react";
13
  import { toast } from "sonner";
14
  import {
@@ -22,12 +25,14 @@ import {
22
  getDatabaseClients,
23
  deleteDatabaseClient,
24
  ingestDatabaseClient,
 
25
  type ApiDocument,
26
  type DocumentStatus,
27
  type DocTypeInfo,
28
  type DbType,
29
  type DbTypeInfo,
30
  type DatabaseClient,
 
31
  } from "../../services/api";
32
 
33
  interface KnowledgeManagementProps {
@@ -35,7 +40,7 @@ interface KnowledgeManagementProps {
35
  onClose: () => void;
36
  }
37
 
38
- type View = "main" | "db-select" | "db-credentials";
39
 
40
  const LOGO_MAP: Record<string, string> = {
41
  postgres: "https://cdn.simpleicons.org/postgresql/336791",
@@ -52,6 +57,17 @@ const getUserId = (): string | null => {
52
  return (JSON.parse(stored).user_id as string) ?? null;
53
  };
54
 
 
 
 
 
 
 
 
 
 
 
 
55
  export default function KnowledgeManagement({
56
  open,
57
  onClose,
@@ -79,6 +95,10 @@ export default function KnowledgeManagement({
79
  const [deletingClient, setDeletingClient] = useState<string | null>(null);
80
  const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
81
 
 
 
 
 
82
  // ── DB credentials form state ───────────────────────────────────────────────
83
  const [connectionName, setConnectionName] = useState("");
84
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
@@ -355,6 +375,21 @@ export default function KnowledgeManagement({
355
  );
356
  };
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  if (!open) return null;
359
 
360
  // ── Header title & back button logic ────────────────────────────────────────
@@ -366,6 +401,8 @@ export default function KnowledgeManagement({
366
  ? "Connect Database"
367
  : view === "db-credentials"
368
  ? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}`
 
 
369
  : "Knowledge Base";
370
 
371
  const headerBack =
@@ -373,6 +410,8 @@ export default function KnowledgeManagement({
373
  ? () => setView("main")
374
  : view === "db-credentials"
375
  ? () => setView("db-select")
 
 
376
  : null;
377
 
378
  // ── Render ───────────────────────────────────────────────────────────────────
@@ -399,12 +438,23 @@ export default function KnowledgeManagement({
399
  )}
400
  <h2 className="text-sm font-semibold text-slate-900">{headerTitle}</h2>
401
  </div>
402
- <button
403
- onClick={handleClose}
404
- className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
405
- >
406
- <X className="w-4 h-4" />
407
- </button>
 
 
 
 
 
 
 
 
 
 
 
408
  </div>
409
 
410
  {/* Content */}
@@ -570,8 +620,8 @@ export default function KnowledgeManagement({
570
  key={doc.id}
571
  className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
572
  >
573
- <div className="w-8 h-8 rounded-lg bg-red-50 flex items-center justify-center flex-shrink-0">
574
- <FileText className="w-4 h-4 text-red-400" />
575
  </div>
576
  <div className="flex-1 min-w-0">
577
  <p className="text-sm font-medium text-slate-800 truncate" title={doc.filename}>
@@ -768,6 +818,36 @@ export default function KnowledgeManagement({
768
  </button>
769
  </div>
770
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  </div>
772
 
773
  {/* Footer */}
@@ -777,6 +857,8 @@ export default function KnowledgeManagement({
777
  ? `Supported formats: ${supportedFormatsText}`
778
  : view === "db-select"
779
  ? "More integrations coming soon"
 
 
780
  : "Credentials are encrypted at rest"}
781
  </p>
782
  </div>
 
3
  Upload,
4
  Trash2,
5
  FileText,
6
+ FileSpreadsheet,
7
+ File,
8
  Check,
9
  Loader2,
10
  Database,
11
  X,
12
  ChevronLeft,
13
  Link,
14
+ Table,
15
  } from "lucide-react";
16
  import { toast } from "sonner";
17
  import {
 
25
  getDatabaseClients,
26
  deleteDatabaseClient,
27
  ingestDatabaseClient,
28
+ getDataCatalog,
29
  type ApiDocument,
30
  type DocumentStatus,
31
  type DocTypeInfo,
32
  type DbType,
33
  type DbTypeInfo,
34
  type DatabaseClient,
35
+ type DataCatalogSource,
36
  } from "../../services/api";
37
 
38
  interface KnowledgeManagementProps {
 
40
  onClose: () => void;
41
  }
42
 
43
+ type View = "main" | "db-select" | "db-credentials" | "catalog";
44
 
45
  const LOGO_MAP: Record<string, string> = {
46
  postgres: "https://cdn.simpleicons.org/postgresql/336791",
 
57
  return (JSON.parse(stored).user_id as string) ?? null;
58
  };
59
 
60
+ const FILE_ICON_MAP: Record<string, { icon: React.ElementType; bg: string; color: string }> = {
61
+ pdf: { icon: FileText, bg: "bg-red-50", color: "text-red-400" },
62
+ docx: { icon: FileText, bg: "bg-blue-50", color: "text-blue-400" },
63
+ txt: { icon: File, bg: "bg-slate-100", color: "text-slate-400" },
64
+ csv: { icon: FileSpreadsheet, bg: "bg-green-50", color: "text-green-500" },
65
+ xlsx: { icon: FileSpreadsheet, bg: "bg-emerald-50",color: "text-emerald-500"},
66
+ };
67
+
68
+ const getFileIcon = (fileType: string) =>
69
+ FILE_ICON_MAP[fileType.toLowerCase()] ?? { icon: File, bg: "bg-slate-100", color: "text-slate-400" };
70
+
71
  export default function KnowledgeManagement({
72
  open,
73
  onClose,
 
95
  const [deletingClient, setDeletingClient] = useState<string | null>(null);
96
  const pollingTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
97
 
98
+ // ── Data Catalog state ──────────────────────────────────────────────────────
99
+ const [catalogSources, setCatalogSources] = useState<DataCatalogSource[]>([]);
100
+ const [loadingCatalog, setLoadingCatalog] = useState(false);
101
+
102
  // ── DB credentials form state ───────────────────────────────────────────────
103
  const [connectionName, setConnectionName] = useState("");
104
  const [dbForm, setDbForm] = useState<Record<string, string | number | boolean>>({});
 
375
  );
376
  };
377
 
378
+ const handleViewCatalog = async () => {
379
+ const uid = getUserId();
380
+ setLoadingCatalog(true);
381
+ setView("catalog");
382
+ try {
383
+ if (!uid) throw new Error("no user");
384
+ const catalog = await getDataCatalog(uid);
385
+ setCatalogSources(catalog.sources.filter((s) => s.source_type !== "unstructured"));
386
+ } catch {
387
+ setCatalogSources([]);
388
+ } finally {
389
+ setLoadingCatalog(false);
390
+ }
391
+ };
392
+
393
  if (!open) return null;
394
 
395
  // ── Header title & back button logic ────────────────────────────────────────
 
401
  ? "Connect Database"
402
  : view === "db-credentials"
403
  ? `Connect to ${selectedDbInfo?.display_name ?? selectedDbType}`
404
+ : view === "catalog"
405
+ ? "Data Catalog"
406
  : "Knowledge Base";
407
 
408
  const headerBack =
 
410
  ? () => setView("main")
411
  : view === "db-credentials"
412
  ? () => setView("db-select")
413
+ : view === "catalog"
414
+ ? () => setView("main")
415
  : null;
416
 
417
  // ── Render ───────────────────────────────────────────────────────────────────
 
438
  )}
439
  <h2 className="text-sm font-semibold text-slate-900">{headerTitle}</h2>
440
  </div>
441
+ <div className="flex items-center gap-1">
442
+ {view === "main" && (
443
+ <button
444
+ onClick={handleViewCatalog}
445
+ title="View data catalog"
446
+ className="p-1.5 rounded-lg text-slate-400 hover:text-[#FF8F00] hover:bg-orange-50 transition"
447
+ >
448
+ <Table className="w-4 h-4" />
449
+ </button>
450
+ )}
451
+ <button
452
+ onClick={handleClose}
453
+ className="p-1.5 rounded-lg text-slate-400 hover:text-slate-700 hover:bg-slate-100 transition"
454
+ >
455
+ <X className="w-4 h-4" />
456
+ </button>
457
+ </div>
458
  </div>
459
 
460
  {/* Content */}
 
620
  key={doc.id}
621
  className="flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-slate-50 transition group"
622
  >
623
+ <div className={`w-8 h-8 rounded-lg ${getFileIcon(doc.file_type).bg} flex items-center justify-center flex-shrink-0`}>
624
+ {(() => { const { icon: Icon, color } = getFileIcon(doc.file_type); return <Icon className={`w-4 h-4 ${color}`} />; })()}
625
  </div>
626
  <div className="flex-1 min-w-0">
627
  <p className="text-sm font-medium text-slate-800 truncate" title={doc.filename}>
 
818
  </button>
819
  </div>
820
  )}
821
+
822
+ {/* ── VIEW: catalog ── */}
823
+ {view === "catalog" && (
824
+ <div className="space-y-2">
825
+ {loadingCatalog ? (
826
+ <div className="flex justify-center py-10">
827
+ <Loader2 className="w-5 h-5 animate-spin text-slate-300" />
828
+ </div>
829
+ ) : catalogSources.length === 0 ? (
830
+ <p className="text-center text-xs text-slate-400 py-8">No structured data indexed yet.</p>
831
+ ) : (
832
+ catalogSources.map((src) => (
833
+ <div key={src.source_id} className="flex items-center gap-3 px-3 py-2.5 rounded-xl border border-slate-100">
834
+ <div className="w-8 h-8 rounded-lg bg-slate-100 flex items-center justify-center flex-shrink-0">
835
+ {src.source_type === "schema"
836
+ ? <Database className="w-4 h-4 text-slate-500" />
837
+ : <Table className="w-4 h-4 text-slate-500" />}
838
+ </div>
839
+ <div className="flex-1 min-w-0">
840
+ <p className="text-sm font-medium text-slate-800 truncate">{src.name}</p>
841
+ <p className="text-xs text-slate-400">
842
+ {src.source_type === "schema" ? "Database" : "Tabular"}
843
+ {src.table_count != null ? ` Β· ${src.table_count} table${src.table_count !== 1 ? "s" : ""}` : ""}
844
+ </p>
845
+ </div>
846
+ </div>
847
+ ))
848
+ )}
849
+ </div>
850
+ )}
851
  </div>
852
 
853
  {/* Footer */}
 
857
  ? `Supported formats: ${supportedFormatsText}`
858
  : view === "db-select"
859
  ? "More integrations coming soon"
860
+ : view === "catalog"
861
+ ? "Structured data sources (database schemas & tabular files)"
862
  : "Credentials are encrypted at rest"}
863
  </p>
864
  </div>
src/app/components/Main.tsx CHANGED
@@ -12,6 +12,7 @@ import {
12
  Bot,
13
  Loader2,
14
  Database,
 
15
  } from "lucide-react";
16
  import ReactMarkdown from "react-markdown";
17
  import remarkGfm from "remark-gfm";
@@ -30,6 +31,7 @@ import {
30
  getRoom,
31
  createRoom,
32
  deleteRoom,
 
33
  streamChat,
34
  type ChatSource,
35
  } from "../../services/api";
@@ -399,6 +401,18 @@ export default function Main() {
399
  setCurrentChatId(null);
400
  };
401
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  const handleLogout = () => {
403
  localStorage.removeItem("chatbot_user");
404
  navigate("/login");
@@ -485,10 +499,11 @@ export default function Main() {
485
  const decoder = new TextDecoder();
486
  let buffer = "";
487
  let currentEvent = "";
 
488
 
489
  while (true) {
490
  const { done, value } = await reader.read();
491
- if (done) break;
492
 
493
  buffer += decoder.decode(value, { stream: true });
494
  const lines = buffer.split(/\r?\n/);
@@ -501,19 +516,23 @@ export default function Main() {
501
  const data = line.replace(/^data: ?/, "");
502
 
503
  if (currentEvent === "sources" && data) {
504
- const sources: ChatSource[] = JSON.parse(data);
505
- setChats((prev) =>
506
- prev.map((chat) =>
507
- chat.id === roomId
508
- ? {
509
- ...chat,
510
- messages: chat.messages.map((m) =>
511
- m.id === assistantMsgId ? { ...m, sources } : m
512
- ),
513
- }
514
- : chat
515
- )
516
- );
 
 
 
 
517
  } else if (currentEvent === "chunk" && data) {
518
  setChats((prev) =>
519
  prev.map((chat) =>
@@ -545,6 +564,7 @@ export default function Main() {
545
  )
546
  );
547
  } else if (currentEvent === "done") {
 
548
  break;
549
  }
550
  }
@@ -790,6 +810,16 @@ export default function Main() {
790
  Hasil Interview
791
  </button>
792
  )}
 
 
 
 
 
 
 
 
 
 
793
  </div>
794
  )}
795
 
 
12
  Bot,
13
  Loader2,
14
  Database,
15
+ Eraser,
16
  } from "lucide-react";
17
  import ReactMarkdown from "react-markdown";
18
  import remarkGfm from "remark-gfm";
 
31
  getRoom,
32
  createRoom,
33
  deleteRoom,
34
+ clearRoomMessages,
35
  streamChat,
36
  type ChatSource,
37
  } from "../../services/api";
 
401
  setCurrentChatId(null);
402
  };
403
 
404
+ const handleClearChat = async () => {
405
+ if (!currentChatId || !user || isStreaming) return;
406
+ try {
407
+ await clearRoomMessages(currentChatId, user.user_id);
408
+ setChats((prev) =>
409
+ prev.map((chat) =>
410
+ chat.id === currentChatId ? { ...chat, messages: [] } : chat
411
+ )
412
+ );
413
+ } catch { /* silent */ }
414
+ };
415
+
416
  const handleLogout = () => {
417
  localStorage.removeItem("chatbot_user");
418
  navigate("/login");
 
499
  const decoder = new TextDecoder();
500
  let buffer = "";
501
  let currentEvent = "";
502
+ let streamDone = false;
503
 
504
  while (true) {
505
  const { done, value } = await reader.read();
506
+ if (done || streamDone) break;
507
 
508
  buffer += decoder.decode(value, { stream: true });
509
  const lines = buffer.split(/\r?\n/);
 
516
  const data = line.replace(/^data: ?/, "");
517
 
518
  if (currentEvent === "sources" && data) {
519
+ try {
520
+ const sources: ChatSource[] = JSON.parse(data);
521
+ setChats((prev) =>
522
+ prev.map((chat) =>
523
+ chat.id === roomId
524
+ ? {
525
+ ...chat,
526
+ messages: chat.messages.map((m) =>
527
+ m.id === assistantMsgId ? { ...m, sources } : m
528
+ ),
529
+ }
530
+ : chat
531
+ )
532
+ );
533
+ } catch {
534
+ // ignore malformed sources
535
+ }
536
  } else if (currentEvent === "chunk" && data) {
537
  setChats((prev) =>
538
  prev.map((chat) =>
 
564
  )
565
  );
566
  } else if (currentEvent === "done") {
567
+ streamDone = true;
568
  break;
569
  }
570
  }
 
810
  Hasil Interview
811
  </button>
812
  )}
813
+ {currentPhase === "analytics" && (currentChat?.messages.length ?? 0) > 0 && (
814
+ <button
815
+ onClick={handleClearChat}
816
+ disabled={isStreaming}
817
+ title="Clear chat history"
818
+ className="ml-auto p-1.5 rounded-lg text-slate-400 hover:text-red-500 hover:bg-red-50 transition disabled:opacity-40"
819
+ >
820
+ <Eraser className="w-3.5 h-3.5" />
821
+ </button>
822
+ )}
823
  </div>
824
  )}
825
 
src/services/api.ts CHANGED
@@ -158,6 +158,13 @@ export const createRoom = (userId: string, title?: string) =>
158
  body: JSON.stringify({ user_id: userId, title }),
159
  });
160
 
 
 
 
 
 
 
 
161
  // ─── Documents ────────────────────────────────────────────────────────────────
162
 
163
  export const getDocuments = (userId: string): Promise<ApiDocument[]> =>
 
158
  body: JSON.stringify({ user_id: userId, title }),
159
  });
160
 
161
+ export const clearRoomMessages = (roomId: string, userId: string) =>
162
+ request<{ status: string; message: string }>(
163
+ ORCHESTRATION_BASE_URL,
164
+ `/api/v1/chat-rooms/${roomId}/messages?user_id=${userId}`,
165
+ { method: "DELETE" }
166
+ );
167
+
168
  // ─── Documents ────────────────────────────────────────────────────────────────
169
 
170
  export const getDocuments = (userId: string): Promise<ApiDocument[]> =>