"use client"; import { ChevronDownIcon, ChevronRightIcon, FolderIcon, FolderPlusIcon, LayersIcon, MoreHorizontalIcon, PencilIcon, PlusIcon, Trash2Icon, } from "lucide-react"; import type { ReactNode } from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { OptionCombobox } from "@/components/option-combobox"; import { ProjectDialog } from "@/components/project-dialog"; 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 { Field, FieldContent, FieldLabel } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Textarea } from "@/components/ui/textarea"; import { createKnowledgeDirectory, deleteKnowledgeDirectory, fetchKnowledgeDirectories, updateKnowledgeDirectory, type KnowledgeDirectory, } from "@/lib/api/admin"; import { useI18n } from "@/i18n/provider"; import { cn } from "@/lib/utils"; type KnowledgeDirectoryPanelProps = { knowledgeBaseId: number; selectedDirectoryId: number | null; onSelectDirectory: (directoryId: number | null) => void; onChanged?: () => void; }; type DirectoryDialogState = { open: boolean; id: number | null; parentId: number; name: string; remark: string; }; type DirectoryOption = { value: string; label: string }; function rootDirectoryOptions(items: KnowledgeDirectory[]): DirectoryOption[] { return items.map((item) => ({ value: String(item.id), label: item.name })); } function collectExpandedIds(items: KnowledgeDirectory[]) { const ids = new Set(); const walk = (nodes: KnowledgeDirectory[]) => { for (const node of nodes) { ids.add(node.id); walk(node.children || []); } }; walk(items); return ids; } export function KnowledgeDirectoryPanel({ knowledgeBaseId, selectedDirectoryId, onSelectDirectory, onChanged, }: KnowledgeDirectoryPanelProps) { const t = useI18n(); const [directories, setDirectories] = useState([]); const [expandedIds, setExpandedIds] = useState>(new Set()); const [contextMenuDirectoryId, setContextMenuDirectoryId] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [dialog, setDialog] = useState({ open: false, id: null, parentId: 0, name: "", remark: "", }); const parentOptions = useMemo( () => [ { value: "0", label: t("knowledge.rootDirectory") }, ...rootDirectoryOptions(directories).filter((item) => item.value !== String(dialog.id ?? "")), ], [directories, dialog.id, t], ); const loadDirectories = useCallback(async () => { setLoading(true); try { const data = await fetchKnowledgeDirectories(knowledgeBaseId); setDirectories(data); setExpandedIds(collectExpandedIds(data)); } catch (error) { toast.error(error instanceof Error ? error.message : t("knowledge.loadDirectoriesFailed")); } finally { setLoading(false); } }, [knowledgeBaseId, t]); useEffect(() => { void loadDirectories(); }, [loadDirectories]); function toggleDirectory(id: number) { setExpandedIds((current) => { const next = new Set(current); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); } function openCreate(parentId = 0) { setDialog({ open: true, id: null, parentId, name: "", remark: "" }); } function openEdit(item: KnowledgeDirectory) { setDialog({ open: true, id: item.id, parentId: item.parentId, name: item.name, remark: item.remark || "", }); } async function handleSubmit() { const name = dialog.name.trim(); if (!name) { toast.error(t("knowledge.directoryNameRequired")); return; } setSaving(true); try { if (dialog.id) { await updateKnowledgeDirectory({ id: dialog.id, knowledgeBaseId, parentId: dialog.parentId, name, remark: dialog.remark.trim(), }); toast.success(t("knowledge.directoryUpdated", { name })); } else { await createKnowledgeDirectory({ knowledgeBaseId, parentId: dialog.parentId, name, remark: dialog.remark.trim(), }); toast.success(t("knowledge.directoryCreated", { name })); } setDialog((current) => ({ ...current, open: false })); await loadDirectories(); onChanged?.(); } catch (error) { toast.error(error instanceof Error ? error.message : t("knowledge.directorySaveFailed")); } finally { setSaving(false); } } async function handleDelete(item: KnowledgeDirectory) { setSaving(true); try { await deleteKnowledgeDirectory(item.id); if (selectedDirectoryId === item.id) { onSelectDirectory(null); } toast.success(t("knowledge.directoryDeleted", { name: item.name })); await loadDirectories(); onChanged?.(); } catch (error) { toast.error(error instanceof Error ? error.message : t("knowledge.directoryDeleteFailed")); } finally { setSaving(false); } } return ( <>
{t("knowledge.directory")}
} label={t("knowledge.allContent")} selected={selectedDirectoryId === null} onClick={() => onSelectDirectory(null)} /> } label={t("knowledge.rootContent")} selected={selectedDirectoryId === 0} onClick={() => onSelectDirectory(0)} /> {directories.map((item) => ( setContextMenuDirectoryId(open ? directoryId : null) } onCreate={openCreate} onEdit={openEdit} onDelete={(directory) => void handleDelete(directory)} t={t} /> ))}
setDialog((current) => ({ ...current, open }))} title={dialog.id ? t("knowledge.editDirectory") : t("knowledge.createDirectory")} size="sm" footer={ <> } >
{t("knowledge.parentDirectory")} setDialog((current) => ({ ...current, parentId: Number(value ?? 0) })) } options={parentOptions} placeholder={t("knowledge.selectDirectory")} searchPlaceholder={t("knowledge.searchDirectory")} emptyText={t("knowledge.emptyDirectory")} /> {t("knowledge.directoryName")} setDialog((current) => ({ ...current, name: event.target.value })) } placeholder={t("knowledge.directoryNamePlaceholder")} /> {t("knowledge.remark")}