import * as React from "react"; import { useTranslation } from "react-i18next"; import { NavLink, useNavigate, useParams } from "react-router-dom"; import { Archive, BarChart3, BookOpen, Bot, ClipboardCheck, Database, FilePlus, FileSearch, FileText, Filter, Gauge, Layers3, ListChecks, RefreshCw, RotateCcw, Search, Settings2, ShieldCheck, SlidersHorizontal, Sparkles, UploadCloud, type LucideIcon, } from "lucide-react"; import { createKnowledgeBase, createKnowledgeDocument, getKnowledgeSettings, listKnowledgeIndexJobs, listKnowledgeBases, listKnowledgeChunks, listKnowledgeDocuments, listModels, parseKnowledgeDocument, reindexKnowledgeBase, searchKnowledge, updateKnowledgeSettings, updateKnowledgeBaseStatus, } from "@/api"; import { translateApiError } from "@/api/errors"; import { ApiErrorState } from "@/components/shared/ApiErrorState"; import { EmptyState } from "@/components/shared/EmptyState"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { MetricCard } from "@/components/shared/MetricCard"; import { PageHeader } from "@/components/shared/PageHeader"; import { SearchInput } from "@/components/shared/SearchInput"; import { StatusBadge } from "@/components/shared/StatusBadge"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog } from "@/components/ui/dialog"; import { Input, Textarea } from "@/components/ui/input"; import { Select } from "@/components/ui/select"; import { Tabs } from "@/components/ui/tabs"; import { toast } from "@/components/ui/toaster"; import { demoText } from "@/lib/demo-text"; import { formatDateTime } from "@/lib/utils"; import type { JSONObject, KnowledgeBase, KnowledgeChunk, KnowledgeDocument, KnowledgeDocumentIngestResponse, KnowledgeDocumentParseResponse, KnowledgeIndexJob, ModelDefinition, SearchResult } from "@/types"; import type { KnowledgeSettingsPayload } from "@/api/knowledge"; const documentStatusValues = ["all", "queued", "indexing", "indexed", "draft", "failed", "archived"] as const; const sourceTypeValues = ["all", "text", "markdown", "json", "html", "pdf"] as const; type KnowledgeJob = { id: string; type: string; target: string; status: KnowledgeIndexJob["status"]; progress: number; createdTime: string; workerKey?: string | null; errorMessage?: string | null; }; type EvalCase = { id: string; query: string; expected: string; status: "draft" | "passed" | "failed"; recall: number; precision: number; }; type RetrievalConfig = { retrievalMode: string; embeddingModelId: string; rerankModelId: string; chunkSize: string; chunkOverlap: string; topK: string; minScore: string; maxCandidates: string; keywordWeight: string; vectorWeight: string; rerankWeight: string; queryRewrite: boolean; requireCitations: boolean; }; const defaultRetrievalConfig: RetrievalConfig = { retrievalMode: "hybrid", embeddingModelId: "auto", rerankModelId: "auto", chunkSize: "800", chunkOverlap: "120", topK: "5", minScore: "0.20", maxCandidates: "50", keywordWeight: "0.30", vectorWeight: "0.55", rerankWeight: "0.15", queryRewrite: true, requireCitations: true, }; const knowledgeSections = [ { value: "overview", label: "Knowledge Bases", path: "/knowledge", icon: Database }, { value: "documents", label: "Documents", path: "/knowledge/documents", icon: FileText }, { value: "playground", label: "Test Search", path: "/knowledge/playground", icon: Search }, { value: "evaluation", label: "Quality", path: "/knowledge/evaluation", icon: ClipboardCheck }, { value: "jobs", label: "Index Jobs", path: "/knowledge/jobs", icon: ListChecks }, { value: "analytics", label: "Analytics", path: "/knowledge/analytics", icon: BarChart3 }, { value: "settings", label: "Settings", path: "/knowledge/settings", icon: Settings2 }, ]; const capabilityGroups = [ { titleKey: "knowledge.ingestion", icon: UploadCloud, items: [ { labelKey: "knowledge.textMarkdownIngest", state: "live" }, { labelKey: "knowledge.parsePreviewIng", state: "live" }, { labelKey: "knowledge.pdfDocxHtmlParser", state: "prototype" }, { labelKey: "knowledge.urlSitemapGithub", state: "prototype" }, ], }, { titleKey: "knowledge.indexingTab", icon: Layers3, items: [ { labelKey: "knowledge.chunkSizeOverlap", state: "live" }, { labelKey: "knowledge.embeddingModelTracking", state: "mock" }, { labelKey: "knowledge.reindexDocumentBase", state: "prototype" }, { labelKey: "knowledge.indexJobQueue", state: "prototype" }, ], }, { titleKey: "knowledge.retrievalTab", icon: Search, items: [ { labelKey: "knowledge.hybridSearchPlayground", state: "live" }, { labelKey: "knowledge.topKMetadataFilters", state: "live" }, { labelKey: "knowledge.scoreBreakdownCitations", state: "live" }, { labelKey: "knowledge.rerankQueryRewrite", state: "mock" }, ], }, { titleKey: "knowledge.evaluation", icon: ClipboardCheck, items: [ { labelKey: "knowledge.goldenQueries", state: "prototype" }, { labelKey: "knowledge.recallPrecisionMetrics", state: "prototype" }, { labelKey: "knowledge.humanRelevanceFeedback", state: "prototype" }, { labelKey: "knowledge.regressionComparison", state: "prototype" }, ], }, { titleKey: "knowledge.governance", icon: ShieldCheck, items: [ { labelKey: "knowledge.archiveRestoreBase", state: "live" }, { labelKey: "knowledge.documentAclPii", state: "prototype" }, { labelKey: "knowledge.auditLog", state: "prototype" }, ], }, { titleKey: "knowledge.integration", icon: Bot, items: [ { labelKey: "knowledge.agentBinding", state: "prototype" }, { labelKey: "knowledge.workflowRetrievalNode", state: "prototype" }, { labelKey: "knowledge.knowledgeSearchTool", state: "prototype" }, { labelKey: "knowledge.runTraceCitations", state: "prototype" }, ], }, ]; export function KnowledgePage() { const { t } = useTranslation(); const navigate = useNavigate(); const documentStatusOptions = documentStatusValues.map((value) => ({ value, label: t(`knowledge.statusLabels.${value}`), })); const sourceTypeOptions = sourceTypeValues.map((value) => ({ value, label: t(`knowledge.sourceLabels.${value}`), })); const { section: sectionParam } = useParams(); const section = knowledgeSections.some((item) => item.value === sectionParam) ? sectionParam ?? "overview" : "overview"; const [bases, setBases] = React.useState([]); const [documents, setDocuments] = React.useState([]); const [chunksByDocument, setChunksByDocument] = React.useState>({}); const [models, setModels] = React.useState([]); const [results, setResults] = React.useState([]); const [selectedBaseId, setSelectedBaseId] = React.useState(); const [scopeMode] = React.useState<"current" | "all">("current"); const [selectedDocumentId, setSelectedDocumentId] = React.useState(); const [baseSearch, setBaseSearch] = React.useState(""); const [documentSearch, setDocumentSearch] = React.useState(""); const [documentStatus, setDocumentStatus] = React.useState("all"); const [sourceType, setSourceType] = React.useState("all"); const [query, setQuery] = React.useState(""); const [topK, setTopK] = React.useState("5"); const [activeTab, setActiveTab] = React.useState("overview"); const [loading, setLoading] = React.useState(true); const [documentsLoading, setDocumentsLoading] = React.useState(false); const [searching, setSearching] = React.useState(false); const [statusBusy, setStatusBusy] = React.useState(false); const [error, setError] = React.useState(); const [createOpen, setCreateOpen] = React.useState(false); const [documentOpen, setDocumentOpen] = React.useState(false); const [lastIngest, setLastIngest] = React.useState(); const [jobs, setJobs] = React.useState([]); const [evalCases, setEvalCases] = React.useState([]); const [retrievalConfig, setRetrievalConfig] = React.useState(defaultRetrievalConfig); const selectedBase = bases.find((base) => base.id === selectedBaseId); const selectedBases = scopeMode === "all" ? bases : selectedBase ? [selectedBase] : []; const activeBaseIds = scopeMode === "all" ? bases.map((base) => base.id) : selectedBaseId ? [selectedBaseId] : []; const activeBaseKey = activeBaseIds.join("|"); const baseNameById = React.useMemo(() => new Map(bases.map((base) => [base.id, demoText(base.name, t)])), [bases, t]); const selectedDocument = documents.find((document) => document.id === selectedDocumentId); const sourceTypes = React.useMemo(() => Array.from(new Set(documents.map((document) => document.sourceType))).sort(), [documents]); const indexedCount = documents.filter((document) => document.status === "indexed").length; const filteredBases = bases.filter((base) => `${base.name} ${base.description ?? ""}`.toLowerCase().includes(baseSearch.toLowerCase())); const filteredDocuments = documents.filter((document) => { const matchesText = `${document.title} ${demoText(document.title, t)} ${document.sourceUri ?? ""} ${document.sourceType}`.toLowerCase().includes(documentSearch.toLowerCase()); const matchesStatus = documentStatus === "all" || document.status === documentStatus; const matchesSource = sourceType === "all" || document.sourceType === sourceType; return matchesText && matchesStatus && matchesSource; }); const selectedDocumentResults = selectedDocument ? results.filter((result) => result.document.id === selectedDocument.id || result.chunk.documentId === selectedDocument.id) : []; const selectedDocumentChunks = React.useMemo(() => { const chunks = [ ...(selectedDocumentId ? chunksByDocument[selectedDocumentId] ?? [] : []), ...(lastIngest && lastIngest.document.id === selectedDocumentId ? lastIngest.chunks : []), ...results.filter((result) => result.chunk.documentId === selectedDocumentId).map((result) => result.chunk), ]; return Array.from(new Map(chunks.map((chunk) => [chunk.id, chunk])).values()); }, [chunksByDocument, lastIngest, results, selectedDocumentId]); const loadBases = React.useCallback(async () => { setLoading(true); setError(undefined); try { const data = await listKnowledgeBases(); setBases(data); setSelectedBaseId((current) => current ?? data[0]?.id); } catch (err) { setError(translateApiError(err)); } finally { setLoading(false); } }, [t]); const loadDocuments = React.useCallback(async (knowledgeBaseIds?: string[]) => { if (!knowledgeBaseIds?.length) { setDocuments([]); setChunksByDocument({}); setResults([]); setSelectedDocumentId(undefined); return; } setDocumentsLoading(true); try { const [documentGroups, chunkGroups] = await Promise.all([ Promise.all(knowledgeBaseIds.map((knowledgeBaseId) => listKnowledgeDocuments(knowledgeBaseId))), Promise.all(knowledgeBaseIds.map((knowledgeBaseId) => listKnowledgeChunks(knowledgeBaseId).catch(() => [] as KnowledgeChunk[]))), ]); const data = documentGroups.flat(); setDocuments(data); setChunksByDocument(groupChunksByDocument(chunkGroups.flat())); setSelectedDocumentId((current) => (current && data.some((document) => document.id === current) ? current : data[0]?.id)); setResults([]); } catch { setDocuments([]); setChunksByDocument({}); setSelectedDocumentId(undefined); toast.error(t("knowledge.failedToLoadDocuments")); } finally { setDocumentsLoading(false); } }, [t]); const loadJobs = React.useCallback(async (knowledgeBaseIds?: string[]) => { if (!knowledgeBaseIds?.length) { setJobs([]); return; } try { const jobGroups = await Promise.all(knowledgeBaseIds.map((knowledgeBaseId) => listKnowledgeIndexJobs(knowledgeBaseId))); setJobs(jobGroups.flat().map(toKnowledgeJob)); } catch { setJobs([]); } }, []); React.useEffect(() => { void loadBases(); }, [loadBases]); React.useEffect(() => { let mounted = true; async function loadConfiguredModels() { try { const data = await listModels(); if (mounted) setModels(data); } catch { if (mounted) setModels([]); } } void loadConfiguredModels(); return () => { mounted = false; }; }, []); React.useEffect(() => { void loadDocuments(activeBaseIds); }, [loadDocuments, activeBaseKey]); React.useEffect(() => { void loadJobs(activeBaseIds); }, [loadJobs, activeBaseKey]); React.useEffect(() => { let mounted = true; async function loadRetrievalSettings() { if (!selectedBaseId) { if (mounted) setRetrievalConfig(defaultRetrievalConfig); return; } try { const data = await getKnowledgeSettings(selectedBaseId); if (mounted) setRetrievalConfig(settingsPayloadToForm(data)); } catch { if (mounted) setRetrievalConfig(defaultRetrievalConfig); } } void loadRetrievalSettings(); return () => { mounted = false; }; }, [selectedBaseId]); async function runSearch(event?: React.FormEvent) { event?.preventDefault(); if (!activeBaseIds.length || !query.trim()) return; setSearching(true); try { const filters: JSONObject = {}; if (documentStatus !== "all") filters.status = documentStatus; if (sourceType !== "all") filters.sourceType = sourceType; const data = (await Promise.all(activeBaseIds.map((baseId) => searchKnowledge(baseId, query.trim(), Number(topK) || 5, filters)))).flat(); data.sort((a, b) => b.score - a.score); setResults(data); setActiveTab("search"); if (section !== "playground") navigate("/knowledge/playground"); toast.info(data.length ? t("knowledge.searchResults", { count: data.length }) : t("knowledge.noMatchingChunks")); } catch (err) { toast.error(translateApiError(err)); } finally { setSearching(false); } } async function reloadSelectedBase() { await Promise.all([loadBases(), loadDocuments(activeBaseIds), loadJobs(activeBaseIds)]); } function selectBase(baseId: string) { setSelectedBaseId(baseId); setSelectedDocumentId(undefined); } async function toggleBaseStatus() { if (!selectedBase) return; setStatusBusy(true); try { const nextStatus = selectedBase.status === "active" ? "archived" : "active"; const updated = await updateKnowledgeBaseStatus({ knowledgeBaseId: selectedBase.id, status: nextStatus }); setBases((items) => items.map((base) => (base.id === updated.id ? updated : base))); toast.success(nextStatus === "active" ? t("knowledge.knowledgeBaseRestored") : t("knowledge.knowledgeBaseArchived")); } finally { setStatusBusy(false); } } async function createJobsForActiveBases(type: string) { const targets = selectedBases.length ? selectedBases : selectedBase ? [selectedBase] : []; for (const base of targets) { try { const result = await reindexKnowledgeBase({ knowledgeBaseId: base.id, chunkSize: Number(retrievalConfig.chunkSize) || undefined, chunkOverlap: Number(retrievalConfig.chunkOverlap) || undefined, }); setJobs((items) => [...result.jobs.map(toKnowledgeJob), ...items.filter((item) => !result.jobs.some((job) => job.jobId === item.id))]); toast.success(t("knowledge.createdJob", { type: formatKnowledgeJobType(type, t) })); await Promise.all([loadDocuments(activeBaseIds), loadJobs(activeBaseIds)]); } catch { toast.error(t("knowledge.documentIngestFailed")); } } } function addEvalCase(queryText: string, expected: string) { setEvalCases((items) => [ { id: `eval_${Date.now().toString(36)}`, query: queryText, expected, status: "draft", recall: 0, precision: 0 }, ...items, ]); } async function runEvalCase(evalId: string) { const item = evalCases.find((evalCase) => evalCase.id === evalId); if (!item || !activeBaseIds.length) return; const data = (await Promise.all(activeBaseIds.map((baseId) => searchKnowledge(baseId, item.query, Number(topK) || 5)))).flat(); const expected = item.expected.toLowerCase(); const matched = data.some((result) => `${result.document.title} ${result.chunk.contentText}`.toLowerCase().includes(expected)); const scoreAverage = average(data.map((result) => result.score)); setEvalCases((items) => items.map((evalCase) => evalCase.id === evalId ? { ...evalCase, status: matched ? "passed" : "failed", recall: matched ? 1 : 0, precision: Number(Math.min(scoreAverage || 0, 1).toFixed(2)), } : evalCase)); toast.success(t("knowledge.evaluationCaseFinished")); } function updateRetrievalConfig(nextConfig: RetrievalConfig) { setRetrievalConfig(nextConfig); if (!selectedBaseId) return; void updateKnowledgeSettings(formToSettingsPayload(nextConfig, selectedBaseId)).catch(() => { toast.error(t("common.failedToSave")); }); } const isOverview = section === "overview"; const pageTitle = isOverview ? t("knowledge.knowledgeBases") : demoText(selectedBase?.name, t) || t("knowledge.title"); const pageDescription = isOverview ? t("knowledge.description") : t("knowledge.manageInsideBase"); if (loading) return ; if (error) return void loadBases()} />; return (
{!isOverview ? ( ) : null} {isOverview ? : null}
)} /> {!isOverview ? ( <> setDocumentOpen(true)} onTestSearch={() => navigate("/knowledge/playground")} onReindex={() => createJobsForActiveBases("Re-index")} onSettings={() => navigate("/knowledge/settings")} /> ) : null} {section === "overview" ? (
setCreateOpen(true)} onSelect={selectBase} />
void toggleBaseStatus()} onOpenBase={() => navigate("/knowledge/documents")} />
) : null} {section === "documents" ? (
setDocumentOpen(true)} onSelect={(documentId) => { setSelectedDocumentId(documentId); setActiveTab("overview"); }} />
) : null} {section === "playground" ? (
{t("knowledge.retrievalPlayground")}

{t("knowledge.askAgainstBase", { name: selectedBase?.name ?? t("knowledge.currentBase") })}

void runSearch(event)}> setQuery(event.target.value)} placeholder={t("knowledge.askRetrievalQuestion")} /> setTopK(event.target.value)} options={[3, 5, 10, 20].map((value) => ({ value: String(value), label: `Top ${value}` }))} />
{results.length ?
{results.map((result) => )}
: }
) : null} {section === "evaluation" ? : null} {section === "jobs" ? createJobsForActiveBases("Re-index")} /> : null} {section === "analytics" ? : null} {section === "settings" ? (
) : null} void loadBases()} /> { setLastIngest(ingest); setChunksByDocument((current) => ({ ...current, [ingest.document.id]: ingest.chunks })); if (ingest.job) { setJobs((items) => [toKnowledgeJob(ingest.job!), ...items.filter((item) => item.id !== ingest.job?.jobId)]); } setSelectedDocumentId(ingest.document.id); setActiveTab("overview"); void Promise.all([loadDocuments(activeBaseIds), loadJobs(activeBaseIds)]); }} /> ); } function KnowledgeSectionNav() { const { t } = useTranslation(); const workspaceSections = knowledgeSections.filter((item) => item.value !== "overview"); return (
); } function KnowledgeScopeBar({ bases, selectedBaseId, documentCount, indexedCount, jobCount, onSelectBase, onAddDocument, onTestSearch, onReindex, onSettings, }: { bases: KnowledgeBase[]; selectedBaseId?: string; documentCount: number; indexedCount: number; jobCount: number; onSelectBase: (baseId: string) => void; onAddDocument: () => void; onTestSearch: () => void; onReindex: () => void; onSettings: () => void; }) { const { t } = useTranslation(); const selectedBase = bases.find((base) => base.id === selectedBaseId); const hasBase = Boolean(selectedBase); return (

{t("knowledge.currentBase")}

onStatus(event.target.value)} options={documentStatusValues.map((value) => ({ value, label: t(`knowledge.statusLabels.${value}`) }))} />
{documentsLoading ? ( ) : filteredDocuments.length ? (
{filteredDocuments.map((document) => ( ))}
) : ( )}
); } function DocumentInspector({ activeTab, onTab, selectedBase, selectedDocument, selectedDocumentResults, selectedDocumentChunks, lastIngest, }: { activeTab: string; onTab: (value: string) => void; selectedBase?: KnowledgeBase; selectedDocument?: KnowledgeDocument; selectedDocumentResults: SearchResult[]; selectedDocumentChunks: KnowledgeChunk[]; lastIngest?: KnowledgeDocumentIngestResponse; }) { const { t } = useTranslation(); return ( {selectedDocument ? demoText(selectedDocument.title, t) : t("knowledge.inspector")}
{lastIngest?.document.id === selectedDocument.id ? (
{t("knowledge.lastIngestCreated", { count: lastIngest.chunks.length })}
) : null} ) : ( ), }, { value: "search", label: t("knowledge.search"), content: selectedDocumentResults.length ? (
{selectedDocumentResults.map((result) => )}
) : ( ), }, { value: "metadata", label: t("knowledge.propertiesTab"), content: , }, { value: "chunks", label: t("knowledge.chunks"), content: selectedDocumentChunks.length ? (
{selectedDocumentChunks.map((chunk) => (
{t("knowledge.chunk")} #{chunk.chunkIndex} {chunk.tokenCount} {t("knowledge.tokens")}

{chunk.contentText}

))}
) : ( ), }, ]} />
); } function EvaluationPage({ evalCases, onAdd, onRun }: { evalCases: EvalCase[]; onAdd: (queryText: string, expected: string) => void; onRun: (evalId: string) => void }) { const { t } = useTranslation(); const [queryText, setQueryText] = React.useState(""); const [expected, setExpected] = React.useState(""); const avgRecall = average(evalCases.map((item) => item.recall)); const avgPrecision = average(evalCases.map((item) => item.precision)); function submit(event: React.FormEvent) { event.preventDefault(); if (!queryText.trim()) return; onAdd(queryText.trim(), expected.trim() || t("knowledge.expectedCitation")); setQueryText(""); setExpected(""); } return (
{t("knowledge.goldenQuery")}

{t("knowledge.buildEvaluationSet")}