IntegraChat / frontend /components /knowledge-base-panel.tsx
nothingworry's picture
feat: add knowledge base management and analytics dashboard
aa63765
raw
history blame
7.88 kB
"use client";
import { useState } from "react";
import Link from "next/link";
type SearchResult = {
text: string;
similarity?: number;
relevance?: number;
};
const API_BASE =
process.env.NEXT_PUBLIC_API_URL?.replace(/\/$/, "") || "http://localhost:8000";
export function KnowledgeBasePanel() {
const [tenantId, setTenantId] = useState("tenant123");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [ingestContent, setIngestContent] = useState("");
const [isIngesting, setIsIngesting] = useState(false);
const [ingestStatus, setIngestStatus] = useState<string | null>(null);
const [searchError, setSearchError] = useState<string | null>(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 handleIngest() {
if (!ingestContent.trim() || isIngesting) return;
setIsIngesting(true);
setIngestStatus(null);
try {
const response = await fetch(`${API_BASE}/rag/ingest`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-tenant-id": tenantId,
},
body: JSON.stringify({ content: ingestContent }),
});
if (!response.ok) {
throw new Error(`Ingestion failed: ${response.status}`);
}
const data = await response.json();
setIngestStatus(
`✅ Successfully ingested ${data.chunks_stored || 0} chunk(s)`,
);
setIngestContent("");
} 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);
}
}
return (
<section
id="knowledge-base"
className="gradient-border relative rounded-[28px] p-1 text-white"
>
<div className="glass-panel relative rounded-[26px] p-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.5em] text-cyan-200/70">
Knowledge Base
</p>
<h2 className="mt-2 text-3xl font-semibold">
Search & ingest documents
</h2>
</div>
<Link
href="/knowledge-base"
className="rounded-full border border-cyan-500/50 bg-cyan-500/10 px-5 py-2.5 text-sm font-semibold text-cyan-300 transition hover:bg-cyan-500/20"
>
View All Documents →
</Link>
<div className="flex flex-col gap-2 text-sm text-slate-200">
<label className="text-xs uppercase tracking-widest text-slate-400">
Tenant ID
</label>
<input
value={tenantId}
onChange={(e) => setTenantId(e.target.value)}
className="rounded-full border border-white/15 bg-white/10 px-4 py-1.5 text-sm text-white outline-none focus:border-cyan-300"
/>
</div>
</div>
{/* Search Section */}
<div className="mt-6">
<div className="flex flex-col gap-3 md:flex-row">
<input
type="text"
placeholder="Search knowledge base (e.g., 'HR policy', 'refund procedure')..."
value={searchQuery}
onChange={(e) => 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"
/>
<button
onClick={handleSearch}
disabled={isSearching}
className="min-w-[140px] rounded-2xl bg-gradient-to-r from-sky-400 to-cyan-500 px-6 py-3 font-semibold text-slate-950 shadow-lg shadow-cyan-500/30 transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
>
{isSearching ? "Searching…" : "Search"}
</button>
</div>
{searchError && (
<p className="mt-3 rounded-2xl border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200">
{searchError}
</p>
)}
{searchResults.length > 0 && (
<div className="mt-4 space-y-3">
<p className="text-sm uppercase tracking-widest text-slate-400">
Found {searchResults.length} result(s)
</p>
{searchResults.map((result, idx) => (
<div
key={idx}
className="rounded-2xl border border-white/10 bg-slate-950/40 p-4"
>
<div className="flex items-start justify-between gap-3">
<p className="flex-1 text-sm text-slate-200">
{result.text}
</p>
{(result.similarity !== undefined ||
result.relevance !== undefined) && (
<span className="text-xs text-cyan-300">
{(
result.similarity ?? result.relevance ?? 0
).toFixed(2)}
</span>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* Ingest Section */}
<div className="mt-8 rounded-2xl border border-white/10 bg-slate-950/40 p-4">
<p className="text-sm uppercase tracking-[0.5em] text-slate-400">
Add to Knowledge Base
</p>
<p className="mt-2 text-sm text-slate-300">
Paste text content to ingest. It will be chunked, embedded, and
stored in your tenant's knowledge base.
</p>
<textarea
placeholder="Paste document content here (e.g., policy text, procedures, documentation)..."
value={ingestContent}
onChange={(e) => setIngestContent(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"
rows={6}
/>
<div className="mt-4 flex items-center gap-3">
<button
onClick={handleIngest}
disabled={isIngesting || !ingestContent.trim()}
className="rounded-2xl bg-gradient-to-r from-emerald-400 to-teal-500 px-6 py-2.5 font-semibold text-slate-950 shadow-lg shadow-emerald-500/30 transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
>
{isIngesting ? "Ingesting…" : "Ingest Content"}
</button>
{ingestStatus && (
<p className="text-sm text-slate-300">{ingestStatus}</p>
)}
</div>
</div>
</div>
</section>
);
}