"use client"; import { useState, useRef, useEffect } from "react"; import Link from "next/link"; import { useTenant } from "@/contexts/TenantContext"; type SearchResult = { text: string; similarity?: number; relevance?: number; }; type Document = { id: number; text: string; created_at?: string; }; type SourceType = "raw_text" | "url" | "pdf" | "docx" | "txt" | "markdown"; const API_BASE = process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000"; export function KnowledgeBasePanel() { const { tenantId, isLoading: tenantLoading } = useTenant(); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [ingestContent, setIngestContent] = useState(""); const [sourceType, setSourceType] = useState("raw_text"); const [filename, setFilename] = useState(""); const [url, setUrl] = useState(""); const [isIngesting, setIsIngesting] = useState(false); const [ingestStatus, setIngestStatus] = useState(null); const [searchError, setSearchError] = useState(null); const [documents, setDocuments] = useState([]); const [isLoadingDocs, setIsLoadingDocs] = useState(false); const [isDeleting, setIsDeleting] = useState(null); const [isDeletingAll, setIsDeletingAll] = useState(false); const fileInputRef = useRef(null); async function handleSearch() { if (!searchQuery.trim() || isSearching) return; setIsSearching(true); setSearchError(null); setSearchResults([]); try { const response = await fetch(`${API_BASE}/rag/search`, { method: "POST", headers: { "Content-Type": "application/json", "x-tenant-id": tenantId, }, body: JSON.stringify({ query: searchQuery }), }); if (!response.ok) { throw new Error(`Search failed: ${response.status}`); } const data = await response.json(); setSearchResults(data.results || []); } catch (err) { console.error(err); setSearchError( err instanceof Error ? err.message : "Failed to search knowledge base. Is the RAG MCP server running?", ); } finally { setIsSearching(false); } } async function handleFileUpload(event: React.ChangeEvent) { const file = event.target.files?.[0]; if (!file) return; // Detect file type from extension const ext = file.name.split('.').pop()?.toLowerCase(); let detectedType: SourceType = "raw_text"; if (ext === "pdf") detectedType = "pdf"; else if (ext === "docx" || ext === "doc") detectedType = "docx"; else if (ext === "txt" || ext === "text") detectedType = "txt"; else if (ext === "md" || ext === "markdown") detectedType = "markdown"; setSourceType(detectedType); setFilename(file.name); // For binary files (PDF, DOCX), upload directly to server if (detectedType === "pdf" || detectedType === "docx") { await handleFileIngest(file); return; } // For text files, read and show in textarea const reader = new FileReader(); reader.onload = async (e) => { const text = e.target?.result as string; setIngestContent(text); }; reader.readAsText(file); } async function handleFileIngest(file: File) { setIsIngesting(true); setIngestStatus(null); try { const formData = new FormData(); formData.append("file", file); const response = await fetch(`${API_BASE}/rag/ingest-file`, { method: "POST", headers: { "x-tenant-id": tenantId, }, body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( errorData.detail || `File ingestion failed: ${response.status}`, ); } const data = await response.json(); setIngestStatus( `✅ ${data.message || `Successfully ingested ${data.chunks_stored || 0} chunk(s)`}`, ); setFilename(""); if (fileInputRef.current) { fileInputRef.current.value = ""; } // Reload documents after successful ingestion loadDocuments(); } catch (err) { console.error(err); setIngestStatus( err instanceof Error ? `❌ Error: ${err.message}` : "Failed to ingest file. Is the RAG MCP server running?", ); } finally { setIsIngesting(false); } } async function handleIngest() { if (!ingestContent.trim() || isIngesting) return; setIsIngesting(true); setIngestStatus(null); try { // Prepare metadata const metadata: Record = {}; if (filename) metadata.filename = filename; if (url || sourceType === "url") { const ingestUrl = url || ingestContent.trim(); metadata.url = ingestUrl; } if (filename) { // Generate doc_id from filename metadata.doc_id = filename.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); } // Use the new enhanced endpoint const response = await fetch(`${API_BASE}/rag/ingest-document`, { method: "POST", headers: { "Content-Type": "application/json", "x-tenant-id": tenantId, }, body: JSON.stringify({ action: "ingest_document", tenant_id: tenantId, source_type: sourceType, content: sourceType === "url" ? (url || ingestContent.trim()) : ingestContent, metadata: Object.keys(metadata).length > 0 ? metadata : undefined, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error( errorData.detail || `Ingestion failed: ${response.status}`, ); } const data = await response.json(); setIngestStatus( `✅ ${data.message || `Successfully ingested ${data.chunks_stored || 0} chunk(s)`}`, ); setIngestContent(""); setFilename(""); setUrl(""); if (fileInputRef.current) { fileInputRef.current.value = ""; } // Reload documents after successful ingestion loadDocuments(); } catch (err) { console.error(err); setIngestStatus( err instanceof Error ? `❌ Error: ${err.message}` : "Failed to ingest content. Is the RAG MCP server running?", ); } finally { setIsIngesting(false); } } async function loadDocuments() { // Guard against empty tenant ID if (!tenantId || !tenantId.trim() || isLoadingDocs) return; setIsLoadingDocs(true); try { const response = await fetch(`${API_BASE}/rag/list?limit=10&offset=0`, { method: "GET", headers: { "x-tenant-id": tenantId, }, }); 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) { // Missing tenant ID - silently fail, user will see empty list console.warn("Cannot load documents: Missing tenant ID"); setDocuments([]); return; } else if (response.status === 503) { console.error("Cannot connect to RAG MCP server"); setDocuments([]); return; } else { throw new Error(errorMsg); } } const data = await response.json(); setDocuments(data.documents || []); } catch (err) { console.error(err); setDocuments([]); // Don't show error in status for document loading - it's not critical } finally { setIsLoadingDocs(false); } } 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, }, }); 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 setDocuments(docs => docs.filter(doc => doc.id !== documentId)); setIngestStatus("✅ Document deleted successfully"); } catch (err) { console.error(err); setIngestStatus( err instanceof Error ? `❌ 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, }, }); if (!response.ok) { throw new Error(`Failed to delete all documents: ${response.status}`); } const data = await response.json(); setDocuments([]); setIngestStatus(`✅ Deleted ${data.deleted_count || 0} document(s)`); } catch (err) { console.error(err); setIngestStatus( err instanceof Error ? `❌ Error: ${err.message}` : "Failed to delete all documents", ); } finally { setIsDeletingAll(false); } } // Load documents on mount and when tenant changes useEffect(() => { // Wait for tenant context to finish loading, then load documents if tenant ID is available if (!tenantLoading && tenantId && tenantId.trim()) { loadDocuments(); } }, [tenantId, tenantLoading]); return (

Knowledge Base

Search & ingest documents

View All Documents →
{/* Search Section */}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} className="flex-1 rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white outline-none focus:border-cyan-200/80" />
{searchError && (

{searchError}

)} {searchResults.length > 0 && (

Found {searchResults.length} result(s)

{searchResults.map((result, idx) => (

{result.text}

{(result.similarity !== undefined || result.relevance !== undefined) && ( {( result.similarity ?? result.relevance ?? 0 ).toFixed(2)} )}
))}
)}
{/* Ingest Section */}

Add to Knowledge Base

Upload files (PDF, DOCX, TXT, MD), paste text, or provide URLs. Content will be chunked, embedded, and stored.

{/* Source Type Selector */}
{(["raw_text", "url", "pdf", "docx", "txt", "markdown"] as SourceType[]).map((type) => ( ))}
{/* File Upload */}
{filename && ( {filename} )}
{/* URL Input (when source type is URL) */} {sourceType === "url" && ( setUrl(e.target.value)} className="mt-4 w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white outline-none focus:border-cyan-200/80" /> )} {/* Content Textarea */}