"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { useTranslation } from "react-i18next"; import { Loader2 } from "lucide-react"; import { useKnowledgeBases } from "@/hooks/useKnowledgeBases"; import { updateRagProviderMode } from "@/lib/knowledge-api"; import KnowledgeBaseDetail from "./KnowledgeBaseDetail"; import KnowledgeHome from "./KnowledgeHome"; import EngineDetail from "./EngineDetail"; import CreateKbModal from "./CreateKbModal"; import PageIndexSettingsModal from "./PageIndexSettingsModal"; export default function KnowledgePage() { const { t } = useTranslation(); const router = useRouter(); const searchParams = useSearchParams(); const initialKb = searchParams.get("kb"); const initialEngine = searchParams.get("engine"); const { kbs: allKbs, providers, uploadPolicy, loading, error, setError, tasksByKb, historyByKb, clearHistory, refresh, createKb, uploadFiles, setDefault, reindex, retry, deleteKb, connectObsidian, connectLinkedFolder, connectLightRagServer, } = useKnowledgeBases(); // Connected subagents are stored as ``type: subagent`` KBs so the chat // composer can select them, but they are agents, not knowledge bases — keep // them out of the Knowledge Center entirely. const kbs = useMemo( () => allKbs.filter((kb) => kb.metadata?.type !== "subagent"), [allKbs], ); const [explicitSelection, setExplicitSelection] = useState( initialKb, ); const [selectedEngineId, setSelectedEngineId] = useState( initialEngine, ); const [createOpen, setCreateOpen] = useState(false); const [createPreset, setCreatePreset] = useState<{ mode: "new" | "link"; source?: string; } | null>(null); const [pipelineOpen, setPipelineOpen] = useState(false); const openCreate = useCallback(() => { setCreatePreset(null); setCreateOpen(true); }, []); // Obsidian lives in the engines grid for discoverability but routes through // the unified create flow, pre-set to "link existing → Obsidian". const openObsidian = useCallback(() => { setCreatePreset({ mode: "link", source: "obsidian" }); setCreateOpen(true); }, []); // Lands on the Overview console unless deep-linked to a KB or an engine. const [view, setView] = useState<"home" | "kb" | "engine">( initialEngine ? "engine" : initialKb ? "kb" : "home", ); const openKb = useCallback((name: string) => { setExplicitSelection(name); setView("kb"); }, []); const openEngine = useCallback((id: string) => { setSelectedEngineId(id); setView("engine"); }, []); // Derive the effective selection: respect the user's pick if it still // exists, otherwise fall back to the default KB (or the first one). No // useEffect chains — keeps state out of effects. const selectedKbName = useMemo(() => { if (explicitSelection && kbs.some((kb) => kb.name === explicitSelection)) { return explicitSelection; } if (!kbs.length) return null; return kbs.find((kb) => kb.is_default)?.name ?? kbs[0].name; }, [explicitSelection, kbs]); const selectedKb = useMemo( () => kbs.find((kb) => kb.name === selectedKbName) ?? null, [kbs, selectedKbName], ); // The effective engine selection: respect the pick if it still exists. const selectedProvider = useMemo( () => providers.find((p) => p.id === selectedEngineId) ?? null, [providers, selectedEngineId], ); // Keep ?kb / ?engine in sync with the effective selection so deep links work. // The Overview view carries neither, so reloading the console stays on it. const urlKb = view === "kb" ? (selectedKbName ?? null) : null; const urlEngine = view === "engine" ? (selectedProvider?.id ?? null) : null; useEffect(() => { if (typeof window === "undefined") return; if ( searchParams.get("kb") === urlKb && searchParams.get("engine") === urlEngine ) { return; } const params = new URLSearchParams(Array.from(searchParams.entries())); if (urlKb) params.set("kb", urlKb); else params.delete("kb"); if (urlEngine) params.set("engine", urlEngine); else params.delete("engine"); const search = params.toString(); router.replace(search ? `?${search}` : "?", { scroll: false }); }, [router, searchParams, urlKb, urlEngine]); const handleCreate = useCallback( async (params: { name: string; provider: string; files: File[] }) => { try { await createKb(params); openKb(params.name); } catch (err) { setError(err instanceof Error ? err.message : String(err)); throw err; } }, [createKb, openKb, setError], ); const handleSetDefault = useCallback( async (name: string) => { try { await setDefault(name); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [setDefault, setError], ); const handleDelete = useCallback( async (name: string) => { if (!window.confirm(t('Delete knowledge base "{{name}}"?', { name }))) { return; } try { await deleteKb(name); if (explicitSelection === name) { setExplicitSelection(null); setView("home"); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [deleteKb, explicitSelection, setError, t], ); const handleUpload = useCallback( async (kbName: string, files: File[]) => { try { await uploadFiles(kbName, files); } catch (err) { setError(err instanceof Error ? err.message : String(err)); throw err; } }, [setError, uploadFiles], ); const handleReindex = useCallback( async (kbName: string) => { try { await reindex(kbName); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [reindex, setError], ); const handleRetry = useCallback( async (kbName: string) => { try { await retry(kbName); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [retry, setError], ); const handleSelectMode = useCallback( async (id: string, mode: string) => { try { await updateRagProviderMode(id, mode); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, [refresh, setError], ); return (
{error && (
{error}
)} {loading ? (
) : (
{view === "home" ? ( ) : view === "engine" && selectedProvider ? ( setView("home")} onOpenKb={openKb} onSelectMode={handleSelectMode} onChanged={() => void refresh({ force: true })} onError={(message) => setError(message)} /> ) : view === "engine" ? ( // Selected engine vanished (e.g. provider list changed); bounce home. ) : ( setView("home")} /> )}
)} setCreateOpen(false)} providers={providers} uploadPolicy={uploadPolicy} onCreate={handleCreate} onConnectLinkedFolder={connectLinkedFolder} onConnectObsidian={connectObsidian} onConnectLightRagServer={connectLightRagServer} initialMode={createPreset?.mode} initialSource={createPreset?.source} onConfigureProvider={() => { setCreateOpen(false); setPipelineOpen(true); }} /> setPipelineOpen(false)} onSaved={() => void refresh({ force: true })} />
); }