"use client"; import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { CircleHelpIcon, FileTextIcon, MoreHorizontalIcon, PencilIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, } from "lucide-react"; import type { CSSProperties } from "react"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; 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 { createKnowledgeBase, deleteKnowledgeBase, fetchKnowledgeBases, rebuildKnowledgeBaseIndex, updateKnowledgeBase, updateKnowledgeBaseSort, type CreateKnowledgeBasePayload, type KnowledgeBase, } from "@/lib/api/admin"; import { getEnumLabel, getEnumOptions } from "@/lib/enums"; import { KnowledgeBaseType, StatusLabels } from "@/lib/generated/enums"; import { cn } from "@/lib/utils"; import { EditDialog } from "./knowledge-base-edit"; type KnowledgeBaseListProps = { selectedKnowledgeBaseId: number | null; onSelectKnowledgeBase: (knowledgeBase: KnowledgeBase | null) => void; }; const statusOptions = [ { value: "all", label: "全部状态" }, ...getEnumOptions(StatusLabels), ] as const; type SortableKnowledgeBaseCardProps = { item: KnowledgeBase; isSelected: boolean; disabled: boolean; onSelect: () => void; onEdit: () => void; onDelete: () => void; onRebuildIndex: () => void; deleteLoadingId: number | null; rebuildIndexLoadingId: number | null; }; function SortableKnowledgeBaseCard({ item, isSelected, disabled, onSelect, onEdit, onDelete, onRebuildIndex, deleteLoadingId, rebuildIndexLoadingId, }: SortableKnowledgeBaseCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled, }); const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, }; return ( { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(); } }} {...attributes} {...listeners} > {item.knowledgeType === KnowledgeBaseType.FAQ ? ( ) : ( )} {item.name} {item.knowledgeType === "faq" ? item.faqCount : item.documentCount} } aria-label={`更多操作 ${item.name}`} > { e.stopPropagation(); onEdit(); }} > 编辑 { e.stopPropagation(); onRebuildIndex(); }} > {rebuildIndexLoadingId === item.id ? "重建中..." : "重建索引"} { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > {deleteLoadingId === item.id ? "删除中..." : "删除"} { e.stopPropagation(); onEdit(); }} > 编辑 { e.stopPropagation(); onRebuildIndex(); }} > {rebuildIndexLoadingId === item.id ? "重建中..." : "重建索引"} { e.stopPropagation(); onDelete(); }} variant="destructive" > {deleteLoadingId === item.id ? "删除中..." : "删除"} ); } export function KnowledgeBaseList({ selectedKnowledgeBaseId, onSelectKnowledgeBase, }: KnowledgeBaseListProps) { const [keywordInput, setKeywordInput] = useState(""); const [statusFilterInput, setStatusFilterInput] = useState("all"); const [keyword, setKeyword] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [sorting, setSorting] = useState(false); const [deleteLoadingId, setDeleteLoadingId] = useState(null); const [rebuildIndexLoadingId, setRebuildIndexLoadingId] = useState< number | null >(null); const [dialogOpen, setDialogOpen] = useState(false); const [editingItemId, setEditingItemId] = useState(null); const [knowledgeBases, setKnowledgeBases] = useState([]); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 8 }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); const loadData = useCallback(async () => { setLoading(true); try { const data = await fetchKnowledgeBases({ name: keyword.trim() || undefined, status: statusFilter === "all" ? undefined : statusFilter, limit: 1000, }); setKnowledgeBases(data.results); } catch (error) { toast.error(error instanceof Error ? error.message : "加载知识库失败"); } finally { setLoading(false); } }, [keyword, statusFilter]); useEffect(() => { void loadData(); }, [loadData]); useEffect(() => { if ( selectedKnowledgeBaseId === null && knowledgeBases.length > 0 && !loading ) { onSelectKnowledgeBase(knowledgeBases[0]); } }, [selectedKnowledgeBaseId, knowledgeBases, loading, onSelectKnowledgeBase]); function handleStatusFilterChange(value: string | null) { setStatusFilterInput(value ?? "all"); } function applyFilters() { setKeyword(keywordInput); setStatusFilter(statusFilterInput); } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return; } event.preventDefault(); applyFilters(); } function openCreateDialog() { setEditingItemId(null); setDialogOpen(true); } function openEditDialog(item: KnowledgeBase) { setEditingItemId(item.id); setDialogOpen(true); } function handleDialogOpenChange(open: boolean) { if (saving) { return; } if (!open) { setEditingItemId(null); } setDialogOpen(open); } async function handleSubmit(payload: CreateKnowledgeBasePayload) { if (saving) { return; } setSaving(true); try { if (editingItemId) { await updateKnowledgeBase({ id: editingItemId, ...payload, }); const editingItem = knowledgeBases.find( (item) => item.id === editingItemId, ); toast.success(`已更新知识库:${editingItem?.name || payload.name}`); } else { await createKnowledgeBase(payload); toast.success(`已创建知识库:${payload.name}`); } setDialogOpen(false); setEditingItemId(null); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : "保存知识库失败"); } finally { setSaving(false); } } async function handleDelete(item: KnowledgeBase) { setDeleteLoadingId(item.id); try { await deleteKnowledgeBase(item.id); toast.success(`已删除知识库:${item.name}`); if (selectedKnowledgeBaseId === item.id) { onSelectKnowledgeBase(null); } await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : "删除知识库失败"); } finally { setDeleteLoadingId(null); } } async function handleRebuildIndex(item: KnowledgeBase) { setRebuildIndexLoadingId(item.id); try { await rebuildKnowledgeBaseIndex(item.id); toast.success(`已开始重建知识库索引:${item.name}`); } catch (error) { toast.error( error instanceof Error ? error.message : "重建知识库索引失败", ); } finally { setRebuildIndexLoadingId(null); } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id || sorting) { return; } const previousResults = knowledgeBases; const oldIndex = previousResults.findIndex((item) => item.id === active.id); const newIndex = previousResults.findIndex((item) => item.id === over.id); if (oldIndex < 0 || newIndex < 0) { return; } const nextResults = arrayMove(previousResults, oldIndex, newIndex); setKnowledgeBases(nextResults); setSorting(true); try { await updateKnowledgeBaseSort(nextResults.map((item) => item.id)); toast.success("知识库排序已更新"); await loadData(); } catch (error) { setKnowledgeBases(previousResults); toast.error( error instanceof Error ? error.message : "更新知识库排序失败", ); } finally { setSorting(false); } } return ( <> 知识库 void loadData()} disabled={loading || sorting} > setKeywordInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder="搜索知识库" className="h-8 pl-8 text-xs" /> {statusFilterInput === "all" ? "全部状态" : getEnumLabel(StatusLabels, Number(statusFilterInput))} {statusOptions.map((item) => ( {item.label} ))} void handleDragEnd(event)} > item.id)} strategy={verticalListSortingStrategy} > {knowledgeBases.map((item) => ( onSelectKnowledgeBase(item)} onEdit={() => openEditDialog(item)} onDelete={() => void handleDelete(item)} onRebuildIndex={() => void handleRebuildIndex(item)} deleteLoadingId={deleteLoadingId} rebuildIndexLoadingId={rebuildIndexLoadingId} /> ))} {!loading && knowledgeBases.length === 0 ? ( 没有匹配的知识库 ) : null} > ); }