"use client"; import { useState, useEffect } from "react"; import Link from "next/link"; import { useTenant } from "@/contexts/TenantContext"; type Document = { id: number; text: string; created_at: string | null; }; type DocumentListResponse = { documents: Document[]; total: number; limit: number; offset: number; }; const API_BASE = process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000"; export default function KnowledgeBasePage() { const { tenantId, isLoading: tenantLoading, role } = useTenant(); const [documents, setDocuments] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [searchFilter, setSearchFilter] = useState(""); const [filterType, setFilterType] = useState<"all" | "pdf" | "text" | "faq" | "link">("all"); const [isDeleting, setIsDeleting] = useState(null); const [isDeletingAll, setIsDeletingAll] = useState(false); async function loadDocuments() { // Guard against empty tenant ID if (!tenantId || !tenantId.trim()) { setError("Please enter a tenant ID"); setLoading(false); return; } setLoading(true); setError(null); try { const response = await fetch(`${API_BASE}/rag/list?limit=1000&offset=0`, { headers: { "x-tenant-id": tenantId, "x-user-role": role, }, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMsg = errorData.detail || errorData.message || `Failed to load documents (${response.status})`; if (response.status === 400) { throw new Error(errorMsg.includes("tenant") ? "Missing tenant ID. Please enter a tenant ID in the navbar." : errorMsg); } else if (response.status === 503) { throw new Error("Cannot connect to RAG MCP server. Please ensure the RAG server is running."); } else { throw new Error(errorMsg); } } const data: DocumentListResponse = await response.json(); setDocuments(data.documents || []); setTotal(data.total || 0); } catch (err) { console.error(err); setError( err instanceof Error ? err.message : "Failed to load knowledge base. Please check if the backend services are running.", ); } finally { setLoading(false); } } useEffect(() => { // Wait for tenant context to finish loading, then load documents if tenant ID is available if (!tenantLoading && tenantId && tenantId.trim()) { loadDocuments(); } }, [tenantId, tenantLoading]); // Filter documents based on search and type const filteredDocuments = documents.filter((doc) => { const matchesSearch = !searchFilter || doc.text.toLowerCase().includes(searchFilter.toLowerCase()); // Simple heuristics for document type detection const textLower = doc.text.toLowerCase(); let docType: "pdf" | "text" | "faq" | "link" = "text"; if (textLower.includes("http://") || textLower.includes("https://") || textLower.includes("www.")) { docType = "link"; } else if ( textLower.includes("q:") || textLower.includes("question:") || textLower.includes("faq") || textLower.includes("frequently asked") ) { docType = "faq"; } else if (textLower.includes(".pdf") || textLower.includes("pdf document")) { docType = "pdf"; } const matchesType = filterType === "all" || docType === filterType; return matchesSearch && matchesType; }); const getDocumentType = (text: string): "pdf" | "text" | "faq" | "link" => { const textLower = text.toLowerCase(); if (textLower.includes("http://") || textLower.includes("https://") || textLower.includes("www.")) { return "link"; } else if ( textLower.includes("q:") || textLower.includes("question:") || textLower.includes("faq") || textLower.includes("frequently asked") ) { return "faq"; } else if (textLower.includes(".pdf") || textLower.includes("pdf document")) { return "pdf"; } return "text"; }; const getTypeColor = (type: string) => { switch (type) { case "pdf": return "bg-red-500/20 text-red-300 border-red-500/30"; case "faq": return "bg-purple-500/20 text-purple-300 border-purple-500/30"; case "link": return "bg-blue-500/20 text-blue-300 border-blue-500/30"; default: return "bg-slate-500/20 text-slate-300 border-slate-500/30"; } }; async function handleDeleteDocument(documentId: number) { if (!tenantId.trim() || isDeleting !== null) return; setIsDeleting(documentId); try { const response = await fetch(`${API_BASE}/rag/delete/${documentId}`, { method: "DELETE", headers: { "x-tenant-id": tenantId, "x-user-role": role, }, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMsg = errorData.detail || `Failed to delete document: ${response.status}`; throw new Error(errorMsg); } // Remove from local state and update total setDocuments(docs => docs.filter(doc => doc.id !== documentId)); setTotal(prev => Math.max(0, prev - 1)); } catch (err) { console.error(err); setError( err instanceof Error ? err.message : "Failed to delete document", ); } finally { setIsDeleting(null); } } async function handleDeleteAll() { if (!tenantId.trim() || isDeletingAll) return; if (!confirm("Are you sure you want to delete ALL documents for this tenant? This action cannot be undone.")) { return; } setIsDeletingAll(true); try { const response = await fetch(`${API_BASE}/rag/delete-all`, { method: "DELETE", headers: { "x-tenant-id": tenantId, "x-user-role": role, }, }); if (!response.ok) { throw new Error(`Failed to delete all documents: ${response.status}`); } const data = await response.json(); setDocuments([]); setTotal(0); } catch (err) { console.error(err); setError( err instanceof Error ? err.message : "Failed to delete all documents", ); } finally { setIsDeletingAll(false); } } return (
{/* Header */}

Knowledge Base Library

All ingested documents, PDFs, FAQs, links, and text content

{documents.length > 0 && ( )}
{/* Stats & Filters */}

Total Documents

{total}

Filtered

{filteredDocuments.length}

setSearchFilter(e.target.value)} className="rounded-full border border-white/10 bg-white/5 px-4 py-2 text-sm text-white outline-none focus:border-cyan-300" />
{(["all", "text", "pdf", "faq", "link"] as const).map((type) => ( ))}
{/* Error Message */} {error && (

⚠️ Error loading knowledge base

{error}

)} {/* Documents Grid */} {loading ? (

Loading documents...

) : filteredDocuments.length === 0 ? (

No documents found

{documents.length === 0 ? "Start by ingesting some content in the Knowledge Base panel." : "Try adjusting your search or filter criteria."}

) : (
{filteredDocuments.map((doc) => { const docType = getDocumentType(doc.text); const preview = doc.text.slice(0, 200) + (doc.text.length > 200 ? "..." : ""); return (
{docType} {doc.created_at && ( {new Date(doc.created_at).toLocaleDateString()} )}

{preview}

ID: {doc.id} {doc.text.length} chars
); })}
)} {/* Footer */}

Knowledge base powered by pgvector + MiniLM embeddings •{" "} Back to Console

); }