"use client"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/features/agent/hooks/useAuth"; import { ResponsiveLayout } from "@/components/layout"; import type { I18nKey } from "@/lib/i18n/dict"; import { useI18n } from "@/lib/i18n/provider"; import { fetchKnowledgeBases, createKnowledgeBase, updateKnowledgeBase, updateKnowledgeBaseRAGEnabled, deleteKnowledgeBase, type KnowledgeBase, type CreateKnowledgeBaseRequest, type UpdateKnowledgeBaseRequest, } from "@/features/agent/services/knowledgeBaseApi"; import { fetchDocuments, createDocument, updateDocument, deleteDocument, publishDocument, unpublishDocument, type Document, type CreateDocumentRequest, type UpdateDocumentRequest, type DocumentListResult, } from "@/features/agent/services/documentApi"; import { importDocuments, importFromUrls, type ImportResult, } from "@/features/agent/services/importApi"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Card } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Switch } from "@/components/ui/switch"; import { Plus, Edit, Trash2, Search, FileText, Upload, Link as LinkIcon, BookOpen, CheckCircle2, XCircle, Loader2, ChevronLeft, ChevronRight, Scissors, } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "@/hooks/useToast"; import DocumentDetailPage from "./[docId]/page"; export default function KnowledgePage(props: any = {}) { const { embedded = false } = props; const router = useRouter(); const { agent } = useAuth(); const { t, lang } = useI18n(); const [chunkingDocId, setChunkingDocId] = useState(null); const tr = (key: I18nKey, vars?: Record) => { let s = t(key); if (!vars) return s; for (const k of Object.keys(vars)) { s = s.replaceAll(`{{${k}}}`, vars[k] ?? ""); } return s; }; // 知识库状态 const [knowledgeBases, setKnowledgeBases] = useState([]); const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState(null); const [loadingKBs, setLoadingKBs] = useState(true); // 文档状态 const [documents, setDocuments] = useState([]); const [documentResult, setDocumentResult] = useState(null); const [loadingDocs, setLoadingDocs] = useState(false); const [searchKeyword, setSearchKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [currentPage, setCurrentPage] = useState(1); const pageSize = 20; // 对话框状态 const [createKBDialogOpen, setCreateKBDialogOpen] = useState(false); const [editKBDialogOpen, setEditKBDialogOpen] = useState(false); const [deleteKBDialogOpen, setDeleteKBDialogOpen] = useState(false); const [createDocDialogOpen, setCreateDocDialogOpen] = useState(false); const [editDocDialogOpen, setEditDocDialogOpen] = useState(false); const [deleteDocDialogOpen, setDeleteDocDialogOpen] = useState(false); const [importDialogOpen, setImportDialogOpen] = useState(false); const [importTab, setImportTab] = useState<"file" | "url">("file"); const [selectedDocument, setSelectedDocument] = useState(null); // 表单状态 const [submitting, setSubmitting] = useState(false); const [createKBForm, setCreateKBForm] = useState({ name: "", description: "", }); const [editKBForm, setEditKBForm] = useState({}); const [createDocForm, setCreateDocForm] = useState({ knowledge_base_id: 0, title: "", content: "", summary: "", type: "document", status: "draft", }); const [editDocForm, setEditDocForm] = useState({}); const [importUrls, setImportUrls] = useState(""); const [importFiles, setImportFiles] = useState([]); // 加载知识库列表(不依赖 selectedKnowledgeBase,避免选中后反复触发 effect 导致疯狂刷新) const loadKnowledgeBases = useCallback(async () => { setLoadingKBs(true); try { const data = await fetchKnowledgeBases(); setKnowledgeBases(data); } catch (error) { console.error("加载知识库列表失败:", error); toast.error((error as Error).message || t("agent.knowledge.toast.loadKbFailed")); } finally { setLoadingKBs(false); } }, [t]); // 加载文档列表(silent:后台轮询向量化状态时不全屏“加载中”、不弹 Toast,避免刷屏) const loadDocuments = useCallback( async (opts?: { silent?: boolean }) => { if (!selectedKnowledgeBase) { setDocuments([]); setDocumentResult(null); return; } const silent = opts?.silent === true; if (!silent) { setLoadingDocs(true); } try { const status = statusFilter === "all" ? undefined : statusFilter; const result = await fetchDocuments( selectedKnowledgeBase.id, currentPage, pageSize, searchKeyword || undefined, status ); setDocumentResult(result); setDocuments(result.documents ?? []); } catch (error) { console.error("加载文档列表失败:", error); if (!silent) { toast.error((error as Error).message || t("agent.knowledge.toast.loadDocFailed")); setDocuments([]); setDocumentResult(null); } } finally { if (!silent) { setLoadingDocs(false); } } }, [selectedKnowledgeBase, currentPage, searchKeyword, statusFilter, t] ); // 初始加载 useEffect(() => { loadKnowledgeBases(); }, [loadKnowledgeBases]); // 当选择知识库或搜索条件变化时,重新加载文档 useEffect(() => { setCurrentPage(1); // 切换知识库或搜索时重置页码 void loadDocuments(); }, [loadDocuments]); // 向量化进行中时自动刷新列表(pending → processing → completed/failed),无需手动整页刷新 useEffect(() => { if (!selectedKnowledgeBase) return; const hasEmbeddingInFlight = documents.some( (d) => d.embedding_status === "pending" || d.embedding_status === "processing" ); if (!hasEmbeddingInFlight) return; const intervalMs = 2500; const id = window.setInterval(() => { void loadDocuments({ silent: true }); }, intervalMs); return () => window.clearInterval(id); }, [selectedKnowledgeBase, documents, loadDocuments]); // 选择知识库 const handleSelectKnowledgeBase = (kb: KnowledgeBase) => { setSelectedKnowledgeBase(kb); setSearchKeyword(""); setStatusFilter("all"); setCurrentPage(1); }; // 创建知识库 const handleCreateKB = async () => { if (!createKBForm.name.trim()) { toast.error(t("agent.knowledge.toast.kbNameRequired")); return; } setSubmitting(true); try { await createKnowledgeBase(createKBForm); setCreateKBDialogOpen(false); setCreateKBForm({ name: "", description: "" }); await loadKnowledgeBases(); toast.success(t("agent.knowledge.toast.createSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.createKbFailed")); } finally { setSubmitting(false); } }; // 打开编辑知识库对话框 const handleOpenEditKB = (kb: KnowledgeBase) => { setEditKBForm({ name: kb.name, description: kb.description, }); setSelectedKnowledgeBase(kb); setEditKBDialogOpen(true); }; // 更新知识库 const handleUpdateKB = async () => { if (!selectedKnowledgeBase) return; setSubmitting(true); try { await updateKnowledgeBase(selectedKnowledgeBase.id, editKBForm); setEditKBDialogOpen(false); await loadKnowledgeBases(); toast.success(t("agent.knowledge.toast.updateSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.updateKbFailed")); } finally { setSubmitting(false); } }; // 打开删除知识库对话框 const handleOpenDeleteKB = (kb: KnowledgeBase) => { setSelectedKnowledgeBase(kb); setDeleteKBDialogOpen(true); }; // 删除知识库 const handleDeleteKB = async () => { if (!selectedKnowledgeBase) return; setSubmitting(true); try { await deleteKnowledgeBase(selectedKnowledgeBase.id); setDeleteKBDialogOpen(false); setSelectedKnowledgeBase(null); await loadKnowledgeBases(); toast.success(t("agent.knowledge.toast.deleteSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.deleteKbFailed")); } finally { setSubmitting(false); } }; // 打开创建文档对话框 const handleOpenCreateDoc = () => { if (!selectedKnowledgeBase) { toast.error(t("agent.knowledge.toast.selectKbFirst")); return; } setCreateDocForm({ knowledge_base_id: selectedKnowledgeBase.id, title: "", content: "", summary: "", type: "document", status: "draft", }); setCreateDocDialogOpen(true); }; // 创建文档 const handleCreateDoc = async () => { if (!createDocForm.title.trim() || !createDocForm.content.trim()) { toast.error(t("agent.knowledge.toast.docTitleContentRequired")); return; } setSubmitting(true); try { await createDocument(createDocForm); setCreateDocDialogOpen(false); setCreateDocForm({ knowledge_base_id: selectedKnowledgeBase?.id || 0, title: "", content: "", summary: "", type: "document", status: "draft", }); await loadDocuments(); toast.success(t("agent.knowledge.toast.createSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.createDocFailed")); } finally { setSubmitting(false); } }; // 打开编辑文档对话框 const handleOpenEditDoc = (doc: Document) => { setSelectedDocument(doc); setEditDocForm({ title: doc.title, content: doc.content, summary: doc.summary, type: doc.type, status: doc.status, }); setEditDocDialogOpen(true); }; // 更新文档 const handleUpdateDoc = async (docId: number) => { setSubmitting(true); try { await updateDocument(docId, editDocForm); setEditDocDialogOpen(false); await loadDocuments(); toast.success(t("agent.knowledge.toast.updateSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.updateDocFailed")); } finally { setSubmitting(false); } }; // 打开删除文档对话框 const handleOpenDeleteDoc = (doc: Document) => { setSelectedDocument(doc); setDeleteDocDialogOpen(true); }; // 删除文档 const handleDeleteDoc = async (docId: number) => { setSubmitting(true); try { await deleteDocument(docId); setDeleteDocDialogOpen(false); await loadDocuments(); toast.success(t("agent.knowledge.toast.deleteSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.deleteDocFailed")); } finally { setSubmitting(false); } }; // 发布文档 const handlePublishDoc = async (docId: number) => { try { await publishDocument(docId); await loadDocuments(); toast.success(t("agent.knowledge.toast.publishSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.publishFailed")); } }; // 取消发布文档 const handleUnpublishDoc = async (docId: number) => { try { await unpublishDocument(docId); await loadDocuments(); toast.success(t("agent.knowledge.toast.unpublishSuccess")); } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.unpublishFailed")); } }; // 导入文件 const handleImportFiles = async () => { if (!selectedKnowledgeBase) { toast.error(t("agent.knowledge.toast.selectKbFirst")); return; } if (importFiles.length === 0) { toast.error(t("agent.knowledge.toast.selectFiles")); return; } setSubmitting(true); try { const result: ImportResult = await importDocuments(selectedKnowledgeBase.id, importFiles); const errMsg = result.errors?.length ? result.errors[0] : ""; if (result.failed_count > 0 && result.success_count === 0) { toast.error( errMsg || tr("agent.knowledge.toast.importFailed.files", { count: String(result.failed_count), }) ); } else if (result.failed_count > 0) { toast.success( tr("agent.knowledge.toast.importDone.partial", { success: String(result.success_count), failed: String(result.failed_count), err: errMsg ? `(${errMsg})` : "", }) ); } else { toast.success( tr("agent.knowledge.toast.importDone.files", { success: String(result.success_count), }) ); } setImportDialogOpen(false); setImportFiles([]); try { await loadDocuments(); await loadKnowledgeBases(); } catch { toast.error(t("agent.knowledge.toast.importRefreshFailed")); } } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.importDocFailed")); } finally { setSubmitting(false); } }; // 导入 URL const handleImportUrls = async () => { if (!selectedKnowledgeBase) { toast.error(t("agent.knowledge.toast.selectKbFirst")); return; } const urls = importUrls .split("\n") .map((url) => url.trim()) .filter((url) => url.length > 0); if (urls.length === 0) { toast.error(t("agent.knowledge.toast.urlRequired")); return; } setSubmitting(true); try { const result: ImportResult = await importFromUrls({ knowledge_base_id: selectedKnowledgeBase.id, urls, }); const errMsg = result.errors?.length ? result.errors[0] : ""; if (result.failed_count > 0 && result.success_count === 0) { toast.error( errMsg || tr("agent.knowledge.toast.importFailed.urls", { count: String(result.failed_count), }) ); } else if (result.failed_count > 0) { toast.success( tr("agent.knowledge.toast.importDone.partial", { success: String(result.success_count), failed: String(result.failed_count), err: errMsg ? `(${errMsg})` : "", }) ); } else { toast.success( tr("agent.knowledge.toast.importDone.urls", { success: String(result.success_count), }) ); } setImportDialogOpen(false); setImportUrls(""); try { await loadDocuments(); await loadKnowledgeBases(); } catch { toast.error(t("agent.knowledge.toast.importRefreshFailed")); } } catch (error) { toast.error((error as Error).message || t("agent.knowledge.toast.importUrlFailed")); } finally { setSubmitting(false); } }; // 格式化时间 const formatTime = (dateStr: string) => { const date = new Date(dateStr); return date.toLocaleString(lang === "en" ? "en-US" : "zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", }); }; // 获取状态标签 const getStatusBadge = (status: string) => { switch (status) { case "published": return ( {t("agent.knowledge.status.published")} ); case "draft": return ( {t("agent.knowledge.status.draft")} ); default: return ( {status} ); } }; // 获取向量化状态标签 const getEmbeddingStatusBadge = (status: string) => { switch (status) { case "completed": return ( {t("agent.knowledge.embedding.completed")} ); case "processing": return ( {t("agent.knowledge.embedding.processing")} ); case "failed": return ( {t("agent.knowledge.embedding.failed")} ); case "pending": default: return ( {t("agent.knowledge.embedding.pending")} ); } }; // 构建头部内容 const headerContent = (

{t("agent.knowledge.title")}

{!embedded && ( )}
); // 构建主内容区 const mainContent = (
{/* 左侧:知识库列表(小屏置顶且限高,避免挤掉文档区) */}
{loadingKBs ? (
{t("common.loading")}
) : knowledgeBases.length === 0 ? (
{t("agent.knowledge.kb.empty")}
) : (
{knowledgeBases.map((kb) => ( handleSelectKnowledgeBase(kb)} >

{kb.name}

{kb.description && (

{kb.description}

)}
{tr("agent.knowledge.kb.docCount", { count: String(kb.document_count), })}
))}
)}
{/* 右侧:文档列表 */}
{selectedKnowledgeBase ? ( <> {/* 文档列表头部 */}

{selectedKnowledgeBase.name}

{ try { const updated = await updateKnowledgeBaseRAGEnabled(selectedKnowledgeBase.id, checked); setSelectedKnowledgeBase((prev) => (prev?.id === updated.id ? { ...prev, rag_enabled: updated.rag_enabled } : prev)); setKnowledgeBases((prev) => prev.map((kb) => (kb.id === updated.id ? { ...kb, rag_enabled: updated.rag_enabled } : kb))); } catch (e) { toast.error((e as Error).message || t("agent.knowledge.toast.updateFailed")); } }} />
{/* 搜索和筛选 */}
setSearchKeyword(e.target.value)} className="pl-10" />
{/* 文档列表 */}
{loadingDocs ? (
{t("common.loading")}
) : (documents?.length ?? 0) === 0 ? (
{searchKeyword || statusFilter !== "all" ? t("agent.knowledge.doc.empty.filtered") : t("agent.knowledge.doc.empty")}
) : (
{(documents ?? []).map((doc) => (

{doc.title}

{getStatusBadge(doc.status)} {getEmbeddingStatusBadge(doc.embedding_status)}
{doc.summary && (

{doc.summary}

)}
{t("agent.knowledge.doc.type")}: {doc.type} {t("agent.knowledge.doc.createdAt")}: {formatTime(doc.created_at)}
{doc.status === "published" ? ( ) : ( )}
))}
)} {/* 分页 */} {documentResult && documentResult.total_page > 1 && (
{tr("agent.knowledge.pagination", { page: String(currentPage), totalPage: String(documentResult.total_page), total: String(documentResult.total), })}
)}
) : (
{t("agent.knowledge.kb.selectOne")}
)}
); const dialogs = ( <> {t("agent.knowledge.dialog.kbCreateTitle")} {t("agent.knowledge.dialog.kbCreateDesc")}
setCreateKBForm({ ...createKBForm, name: e.target.value })} placeholder={t("agent.knowledge.ph.kbName")} />