"use client"; import { FileTextIcon, MoreHorizontalIcon, PencilIcon, SearchIcon, Trash2Icon, WrenchIcon, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { buildKnowledgeDocumentIndex, createKnowledgeDocument, deleteKnowledgeDocument, fetchKnowledgeDocuments, updateKnowledgeDocument, type CreateKnowledgeDocumentPayload, type KnowledgeDocument, type PageResult, } from "@/lib/api/admin"; import { getEnumLabel, getEnumOptions } from "@/lib/enums"; import { KnowledgeDocumentIndexStatus, KnowledgeDocumentIndexStatusLabels, StatusLabels } from "@/lib/generated/enums"; import { cn, formatDateTime } from "@/lib/utils"; import { DocumentEditDialog } from "./document-edit"; type DocumentListProps = { knowledgeBaseId: number | null; onActionStateChange?: (state: DocumentListActionState) => void; }; export type DocumentListActionState = { onRefresh: () => void; onChangeViewMode: (mode: "list" | "grid") => void; onCreate: () => void; viewMode: "list" | "grid"; loading: boolean; }; const statusOptions = [ { value: "all", label: "全部状态" }, ...getEnumOptions(StatusLabels), ] as const; const indexStatusOptions = [ { value: "all", label: "全部索引状态" }, ...getEnumOptions(KnowledgeDocumentIndexStatusLabels), ] as const; function getIndexStatusBadgeVariant(status: string) { switch (status) { case KnowledgeDocumentIndexStatus.Indexed: return "secondary" as const; case KnowledgeDocumentIndexStatus.Failed: return "destructive" as const; default: return "outline" as const; } } function renderIndexStatusBadge(item: KnowledgeDocument) { const badge = ( {item.indexStatusName} ) if ( item.indexStatus !== KnowledgeDocumentIndexStatus.Failed || !item.indexError ) { return badge } return ( {badge} {item.indexError} ) } function getDocumentPreview(content: string, contentType: string) { const preview = contentType === "markdown" ? content .replace(/[`*_>#-]/g, " ") .replace(/\[(.*?)\]\((.*?)\)/g, "$1") .replace(/\s+/g, " ") .trim() : content .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); return preview || "暂无内容"; } const VIEW_MODE_STORAGE_KEY = "knowledge-document-view-mode"; export function DocumentList({ knowledgeBaseId, onActionStateChange }: DocumentListProps) { const [keywordInput, setKeywordInput] = useState(""); const [statusFilterInput, setStatusFilterInput] = useState("all"); const [indexStatusFilterInput, setIndexStatusFilterInput] = useState("all"); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [indexStatusFilter, setIndexStatusFilter] = useState("all"); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [actionLoadingMap, setActionLoadingMap] = useState>({}); const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState( null, ); const [viewMode, setViewMode] = useState<"list" | "grid">(() => { if (typeof window === "undefined") return "grid"; const saved = localStorage.getItem(VIEW_MODE_STORAGE_KEY); return saved === "list" || saved === "grid" ? saved : "grid"; }); const [documents, setDocuments] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }); const loadData = useCallback(async () => { if (!knowledgeBaseId) { setDocuments({ results: [], page: { page: 1, limit: 20, total: 0 } }); setLoading(false); return; } setLoading(true); try { const data = await fetchKnowledgeDocuments({ title: keyword.trim() || undefined, status: statusFilter === "all" ? undefined : statusFilter, indexStatus: indexStatusFilter === "all" ? undefined : indexStatusFilter, knowledgeBaseId, limit: 1000, }); setDocuments(data); } catch (error) { toast.error(error instanceof Error ? error.message : "加载文档失败"); } finally { setLoading(false); } }, [indexStatusFilter, keyword, statusFilter, knowledgeBaseId]); useEffect(() => { void loadData(); }, [loadData]); useEffect(() => { localStorage.setItem(VIEW_MODE_STORAGE_KEY, viewMode); }, [viewMode]); function handleStatusFilterChange(value: string | null) { const newValue = value ?? "all"; setStatusFilterInput(newValue); setStatusFilter(newValue); } function handleIndexStatusFilterChange(value: string | null) { const newValue = value ?? "all"; setIndexStatusFilterInput(newValue); setIndexStatusFilter(newValue); } function applyFilters() { setKeyword(keywordInput); setStatusFilter(statusFilterInput); setIndexStatusFilter(indexStatusFilterInput); } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return; } event.preventDefault(); applyFilters(); } const openCreateDialog = useCallback(() => { setEditingItem(null); setDialogOpen(true); }, []); useEffect(() => { if (!onActionStateChange) { return; } onActionStateChange({ onRefresh: () => void loadData(), onChangeViewMode: setViewMode, onCreate: openCreateDialog, viewMode, loading, }); }, [onActionStateChange, loadData, openCreateDialog, viewMode, loading]); function openEditDialog(item: KnowledgeDocument) { setEditingItem(item); setDialogOpen(true); } function handleDialogOpenChange(open: boolean) { if (saving) { return; } if (!open) { setEditingItem(null); } setDialogOpen(open); } async function handleSubmit(payload: CreateKnowledgeDocumentPayload) { if (saving) { return; } setSaving(true); try { if (editingItem) { await updateKnowledgeDocument({ id: editingItem.id, ...payload, }); toast.success(`已更新文档:${editingItem.title}`); } else { await createKnowledgeDocument(payload); toast.success(`已创建文档:${payload.title}`); } setDialogOpen(false); setEditingItem(null); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存文档失败"); } finally { setSaving(false); } } async function handleDelete(item: KnowledgeDocument) { setActionLoadingMap((prev) => ({ ...prev, [item.id]: { ...prev[item.id], delete: true } })); try { await deleteKnowledgeDocument(item.id); toast.success(`已删除文档:${item.title}`); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : "删除文档失败"); } finally { setActionLoadingMap((prev) => ({ ...prev, [item.id]: { ...prev[item.id], delete: false } })); } } async function handleBuildIndex(item: KnowledgeDocument) { setActionLoadingMap((prev) => ({ ...prev, [item.id]: { ...prev[item.id], rebuildIndex: true } })); try { await buildKnowledgeDocumentIndex(item.id); toast.success(`已重建索引:${item.title}`); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : "重建索引失败"); } finally { setActionLoadingMap((prev) => ({ ...prev, [item.id]: { ...prev[item.id], rebuildIndex: false } })); } } if (!knowledgeBaseId) { return (

请选择一个知识库查看文档

); } return ( <>
setKeywordInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder="搜索文档标题" className="h-8 pl-8 text-xs" />
{documents.results.map((item) => ( viewMode === "grid" ? (
{/* */}
{item.title}
{renderIndexStatusBadge(item)}
{getDocumentPreview(item.content, item.contentType)}
{item.createUserName || "-"} {formatDateTime(item.createdAt)} {item.indexStatus === KnowledgeDocumentIndexStatus.Indexed ? `已索引 ${formatDateTime(item.indexedAt)}` : item.indexStatusName}
} aria-label={`更多操作 ${item.title}`} > openEditDialog(item)}> 编辑 void handleBuildIndex(item)}> {actionLoadingMap[item.id]?.rebuildIndex ? "执行中..." : "重建索引"} void handleDelete(item)} className="text-destructive focus:text-destructive" > {actionLoadingMap[item.id]?.delete ? "删除中..." : "删除"}
openEditDialog(item)}> 编辑 void handleBuildIndex(item)} disabled={actionLoadingMap[item.id]?.rebuildIndex}> {actionLoadingMap[item.id]?.rebuildIndex ? "执行中..." : "重建索引"} void handleDelete(item)} variant="destructive" disabled={actionLoadingMap[item.id]?.delete} > {actionLoadingMap[item.id]?.delete ? "删除中..." : "删除"}
) : (
{/* */}
{item.title}
{renderIndexStatusBadge(item)}
{getDocumentPreview(item.content, item.contentType)}
{item.indexStatus === KnowledgeDocumentIndexStatus.Indexed ? `索引时间:${formatDateTime(item.indexedAt)}` : item.indexError || item.indexStatusName}
{formatDateTime(item.createdAt)}
} aria-label={`更多操作 ${item.title}`} > openEditDialog(item)}> 编辑 void handleBuildIndex(item)}> {actionLoadingMap[item.id]?.rebuildIndex ? "执行中..." : "重建索引"} void handleDelete(item)} className="text-destructive focus:text-destructive" > {actionLoadingMap[item.id]?.delete ? "删除中..." : "删除"}
openEditDialog(item)}> 编辑 void handleBuildIndex(item)} disabled={actionLoadingMap[item.id]?.rebuildIndex}> {actionLoadingMap[item.id]?.rebuildIndex ? "执行中..." : "重建索引"} void handleDelete(item)} variant="destructive" disabled={actionLoadingMap[item.id]?.delete} > {actionLoadingMap[item.id]?.delete ? "删除中..." : "删除"}
) ))} {!loading && documents.results.length === 0 ? (
没有匹配的文档
) : null}
); }