"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, useMemo, 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 { OptionCombobox } from "@/components/option-combobox"; import { createKnowledgeBase, deleteKnowledgeBase, fetchKnowledgeBases, rebuildKnowledgeBaseIndex, updateKnowledgeBase, updateKnowledgeBaseSort, type CreateKnowledgeBasePayload, type KnowledgeBase, } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; import { KnowledgeBaseType, Status } 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; }; type TFunction = (key: string, values?: Record) => string; function getStatusOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allStatus") }, { value: String(Status.Ok), label: t("knowledge.statusOk") }, { value: String(Status.Disabled), label: t("knowledge.statusDisabled") }, { value: String(Status.Deleted), label: t("knowledge.statusDeleted") }, ]; } type SortableKnowledgeBaseCardProps = { item: KnowledgeBase; isSelected: boolean; isContextMenuOpen: boolean; disabled: boolean; onContextMenuOpenChange: (open: boolean) => void; onSelect: () => void; onEdit: () => void; onDelete: () => void; onRebuildIndex: () => void; deleteLoadingId: number | null; rebuildIndexLoadingId: number | null; t: TFunction; }; function SortableKnowledgeBaseCard({ item, isSelected, isContextMenuOpen, disabled, onContextMenuOpenChange, onSelect, onEdit, onDelete, onRebuildIndex, deleteLoadingId, rebuildIndexLoadingId, t, }: 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={t("knowledge.moreActions", { name: item.name })} > { e.stopPropagation(); onEdit(); }} > {t("knowledge.edit")} { e.stopPropagation(); onRebuildIndex(); }} > {rebuildIndexLoadingId === item.id ? t("knowledge.rebuilding") : t("knowledge.rebuildIndex")} { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > {deleteLoadingId === item.id ? t("knowledge.deleting") : t("knowledge.delete")} { e.stopPropagation(); onEdit(); }} > {t("knowledge.edit")} { e.stopPropagation(); onRebuildIndex(); }} > {rebuildIndexLoadingId === item.id ? t("knowledge.rebuilding") : t("knowledge.rebuildIndex")} { e.stopPropagation(); onDelete(); }} variant="destructive" > {deleteLoadingId === item.id ? t("knowledge.deleting") : t("knowledge.delete")} ); } export function KnowledgeBaseList({ selectedKnowledgeBaseId, onSelectKnowledgeBase, }: KnowledgeBaseListProps) { const t = useI18n(); 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 [contextMenuKnowledgeBaseId, setContextMenuKnowledgeBaseId] = useState(null); const statusOptions = useMemo(() => getStatusOptions(t), [t]); 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 : t("knowledge.loadBasesFailed")); } finally { setLoading(false); } }, [keyword, statusFilter, t]); 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(t("knowledge.baseUpdated", { name: editingItem?.name || payload.name })); } else { await createKnowledgeBase(payload); toast.success(t("knowledge.baseCreated", { name: payload.name })); } setDialogOpen(false); setEditingItemId(null); await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : t("knowledge.baseSaveFailed")); } finally { setSaving(false); } } async function handleDelete(item: KnowledgeBase) { setDeleteLoadingId(item.id); try { await deleteKnowledgeBase(item.id); toast.success(t("knowledge.baseDeleted", { name: item.name })); if (selectedKnowledgeBaseId === item.id) { onSelectKnowledgeBase(null); } await loadData(); } catch (error) { toast.error(error instanceof Error ? error.message : t("knowledge.baseDeleteFailed")); } finally { setDeleteLoadingId(null); } } async function handleRebuildIndex(item: KnowledgeBase) { setRebuildIndexLoadingId(item.id); try { await rebuildKnowledgeBaseIndex(item.id); toast.success(t("knowledge.rebuildStarted", { name: item.name })); } catch (error) { toast.error( error instanceof Error ? error.message : t("knowledge.rebuildFailed"), ); } 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(t("knowledge.sortUpdated")); await loadData(); } catch (error) { setKnowledgeBases(previousResults); toast.error( error instanceof Error ? error.message : t("knowledge.sortUpdateFailed"), ); } finally { setSorting(false); } } return ( <>

{t("knowledge.title")}

setKeywordInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder={t("knowledge.searchBase")} className="h-8 pl-8 text-xs" />
void handleDragEnd(event)} > item.id)} strategy={verticalListSortingStrategy} > {knowledgeBases.map((item) => ( setContextMenuKnowledgeBaseId(open ? item.id : null) } onSelect={() => onSelectKnowledgeBase(item)} onEdit={() => openEditDialog(item)} onDelete={() => void handleDelete(item)} onRebuildIndex={() => void handleRebuildIndex(item)} deleteLoadingId={deleteLoadingId} rebuildIndexLoadingId={rebuildIndexLoadingId} t={t} /> ))} {!loading && knowledgeBases.length === 0 ? (
{t("knowledge.emptyBases")}
) : null}
); }